処理系ベンチマーク環境としての Azure VM vs EC2 vs GCE

ISUCON の練習で、Azureの「開発者プログラム特典」という1年間限定で毎月3000円の無料枠を使い始めたのと、副賞でさらに無料枠を貰えそうなので、最近一番 IaaS を使ってる Pythonベンチマーク環境として Azure も使ってみています。

ラップトップPCだけで生活してる人が、時間の掛かるベンチマークをするためだけに IaaS を時間借りするというニッチな視点で、既に使った経験のあるEC2, GCE と Azure を雑に比べてみます。

Hyper Threading

EC2 と GCE は Hyper Threading が有効です。 EC2 の c4 系も、 GCE の n1-highcpu 系もコア数は偶数になっています。両者とも汎用系だと HyperThreading の仮想1コアのマシンが作れるみたいなので、間違ってもベンチマーク用途でそういったインスタンスを使わないようにしましょう。

一方 Azure は Hyper Threading が無効みたいです。CPU重視の F や Dv2 系インスタンスも1コア単位です。

性能の安定性

2回 pyperformance run -b 2to3 した結果です。インスタンスガチャはしてないので、絶対性能は無視して安定性に注目してください。

Azure VM (F1):
Median +- std dev: 622 ms +- 17 ms
Median +- std dev: 644 ms +- 21 ms

EC2 (c4.large):
Median +- std dev: 537 ms +- 4 ms
Median +- std dev: 540 ms +- 4 ms

GCE (n1-highcpu-2):
Median +- std dev: 819 ms +- 7 ms
Median +- std dev: 817 ms +- 7 ms

Azure が 3% 程度、EC2とGCEは1%程度のばらつきがあります。 インスタンスの停止&起動を繰り返していないので断言できませんが、 Azure は EC2 より性能が安定しない感じがしていたので、この結果通りなんじゃないかなと思います。

3% のチューニングを10回すれば25%以上の性能向上になるので、ベンチマークの誤差が3%あるのはかなり痛いです。

その他

Azure VM

Azure は固定IPをしなくても固定DNS名を用意してくれるので、固定IPを買ったりDynamicDNSを設定したりsshの設定を毎回切り替えたりしなくても良いのがとても便利です。

新規マシンの作成はちょっと遅い気がしますが、停止・起動はEC2と同程度な気がします。 時間指定してシャットダウンするオプションを管理コンソールから簡単に設定できて停止忘れ対策になるのがとてもうれしい一方、 ssh してる状態で shutdown -h コマンドで停止してもインスタンスが開放されず課金が続くのは若干面倒。

あと、SSDがGB単位で使えないのが痛すぎます。一番小さい P10 で128GBです。

EC2

インスタンスガチャをしてみないと分かりませんが、Pythonベンチマークではマルチコアを有効に使えないので、コアあたりの性能が良さそうな EC2 c4 インスタンスが一番魅力的です。

ちょっと奮発して c4.8xlarge を使えば、 Turbo Boost を切ることができてさらにベンチマークの安定性が増します。

一方、新しいインスタンスVPCから作ろうとすると、 ssh できるようになるまでにネットワーク周りで設定しないといけないことが多くて手順覚えてられないのが難点。

GCE

管理画面があっさりしていて、仮想マシンを作るのがとても楽です。ウィザードに従って設定しても、 「gcloud コマンドでこれをするには」がコピペできるので、コマンドの使い方を調べる手間が要らないのと、そのコマンドを使うための環境をローカルに用意しなくても Cloud shell に造ってしまえるのがとても楽。

あと、プリエンプティブインスタンスが停止されてもデータが消えないでまた起動して続きからできるのがとても楽。EC2のスポットインスタンスはちょっと新規インスタンス立てて何かのベンチマークを実行しようってときに使う気にならないけれども、GCEだと積極的に使える。

結論

すごく重要な点として、どの仮想マシンも、CPUのパフォーマンスカウンタが見えません。キャッシュミスとかの統計が取れない。つらい。 できるだけ管理するもの(物理)を減らしてスッキリしたかったのですが、仕方ないので会社にラップトップと別に好きにいじれる物理PCを用意してもらうことにしました。

それさえなければ、大抵のCPUを使うプログラムのベンチマークは、シングルコア性能が良い EC2 c4 か、手軽な GCE で良いと思います。 そしてせっかく無料枠あるのに Azure を使うモチベーションが…何に使おう。

CPython の Core Developer になりました

Python 3.6 に取り込まれた dict の新実装などでコアコミッターに興味を持ってもらい、 Core Developer (要するにコミッター) に推薦しようか?という提案をもらいました。

最初はコミッターとか面倒そうだし、コミットメッセージとかNEWSエントリー(通常パッチをコミットするときにコミッターが書く)とかを英語で書くのも英語が得意な人がやったほうがいいだろうし、とりあえず github に移行するまでは様子見しておこうと思ってたのですが、 dict 関係のパッチがいくつもレビュー待ちでなかなかコミットされないのを見て「やっぱりアクティブなコミッターが全然足りてない」と考え直し、志願することに。

で、先月末にコミット権をもらった(というか push できる権限を持った hg アカウントに ssh 鍵を登録してもらった)のですが、新米コミッターは簡単なパッチでも他のコアコミッターのLGTMなしにコミットしちゃダメだよと釘をさされたので、結局レビュー待ち行列状態は変わらず。1週間以上経って今日始めてコミットを push できました。 (コミット)

コミットしたのは、会社のBlog記事 DSAS開発者の部屋:Python の dict の実装詳解 で言及していた次の問題です。

しかし、この方法ではハッシュテーブル内の密度の偏りの影響を避けられるものの、ハッシュ値の下位ビットが衝突する key は同じ順番に巡回するので線形探索になってしまい効率が悪いという欠点があります。そこで、先程の関数を改造して、次のようにしています。

    for (perturb = hash; ; perturb >>= PERTURB_SHIFT) {  
        i = i * 5 + perturb + 1;

これで、最初のうちはハッシュ値の上位ビットを使って次の位置を決定していき、ハッシュ値を右シフト仕切ったら先ほどのアルゴリズムで確実に巡回する、というハイブリッド型の巡回アルゴリズムになります。 (ちなみに、このforループには最初の衝突のあとにくるので、 perturb の初期値が hash のままだと、hash 値の下位ビットが衝突する key と1つ目だけでなく2つ目も衝突してしまいますね…)

例えば要素数が5以下の場合、ハッシュテーブルの大きさは8なので、5要素中3要素のhashの下位3bitが同じだった場合に、2段階のコンフリクトが発生していました。これが最初の衝突後すぐにハッシュのより上位のビットが使われるようになって、その上位bitが異なれば衝突は最初の1回だけで済むようになった形です。とてもレアケースだと思うので実際のアプリで計測誤差以上の速度差は出ないでしょう。

これからも他人のパッチを自分のLGTMだけでコミットできる真のコミッタになることを目指して気長に徳を積んでいきます。

Python 3.6 の(個人的に)注目の変更点

Python 3.6b1 がリリースされましたね。(フライング)

beta1 ということで、 3.6 に向けた新機能の追加は (provisional package を除いて) 終了です。ただし、仕様が確定したと言うわけではなくて、beta版に対するフィードバックを元に新機能を修正したり、最悪 revert して 3.7 に持ち越しにされる可能性もあります。

なお、 3.6b1 が出る前の1週間が core dev sprint があり、そこでめちゃくちゃ大量に大きめの変更が入りました。なので、常用環境には全くオススメできませんが、OSS開発者だったら .travis.ymlpython: "nightly" を追加してリグレッションの発見に貢献したり(←これめっちゃ有り難いです)、それ以外の人も 3.6 を試してみて早めにフィードバックをしてもらえると、年末の 3.6 がより完成度が高いもになると思います。

では行きましょう

dict が省メモリかつ挿入順を保持するようになった

僕がパッチを投げていた、 CPython の dict を PyPy の dict と同じような仕組みにするパッチが取り込まれました。 php, Ruby につづいて Python もデフォルトで dict が挿入順を維持するようになりました。

とはいえ、パッチが取り込まれたのが金曜日で、まだどこまでが言語仕様でどこからが CPython の実装詳細なのかは微妙なところです。 今のところの Guido の宣言では、 dict の順序は実装依存で、キーワード引数や名前空間__dict__ は言語仕様として挿入順を維持する (他の Python 3.6 言語実装では dict ではなく OrderedDict などのサブクラスを返すかもしれない) ということになりそうです。

しかし、「そんなことしたら仕様確認せずに動作だけ見て思い込みで実装依存なコードが増えるじゃないか!」「いやいや、 CPython と PyPy がメジャーな実装で、その2つで動けばいいって判断したならそれに頼るのはOKだろ」「micropython はもともと性能(衝突頻度)よりも省メモリ重視でハッシュテーブルをそんなにスカスカにしてないからこの実装に追従してもコンパクトにならないんだけど」などといろんな意見が飛び出していて予断を許しません。

個人的には Guido の方針に賛成です。特に IronPythonJython などの GIL がない実装が Python 3.6 に追いついたときに、マルチスレッドで性能が出やすい実装を選ぶ余地を残しておいて欲しいからです。

ということで、みなさんは「順序を維持してくれたほうが(ログが見やすくなったりとかで)使い勝手いいけど、必須ではない」というところだけ dict の挿入順維持に頼ってください。

なお、 OrderedDict が新しい dict の実装を利用してより省メモリにできるはずですが、 dict の新実装が取り込まれたのが金曜日なので、月曜日に間に合わせるのは妻子持ちには無理だと最初から諦めてましたごめんなさい。

f-string

"{foo} and {bar}".format(foo=foo, bar=bar) を、 f"{foo} and {bar}" と書けるようになりました。 {} の中にはちゃんと式が書けます。やったね! phpRuby に追いついたね!

なお、 f-string 内でのエスケープシーケンスの使い方については制限があります。詳しくは言語仕様 を参照してください。

pathlib が使いやすくなった

pathlib.Path 経由で直接ファイルを読み書きするときは良かったのですが、他のライブラリとかにパスを渡すときは、今までは渡すときに文字列に変換してあげないといけないことが多くてあまり使い勝手が良くありませんでした。

Python 3.6 では os のいろんな関数が pathlib.Path をサポートするようになったので、そのライブラリが引数をそのまま os の関数に渡しているようなときには、文字列への変換が不要になりました。ダックタイピング万歳!

ただし、ライブラリが引数に対して isinstance(file, str) とかして、引数が文字列ならファイルを開いてファイルオブジェクトなら直接使うみたいな分岐をしている場合は、そのライブラリの対応を待たないといけません。

Windows でファイル名やコンソール入出力が utf-8

Pythonは基本的に unicode 推しで、特に Windowsバイト列でファイル名を利用すると Warning を出しつつ、内部では Windows*AAPI を利用していました。

しかし、 Mercurial などのように生まれも育ちも設計思想も unix な、ファイル名をバイト列として扱うツールを Windows で動かすときに、今さらレガシーなエンコーディング使うのは嫌だという話があり、ファイル名とコンソール入出力をバイト列で行ったときは utf-8 <> utf-16 変換を行って W 系 API を呼ぶようになりました。

これで後方互換性に問題がでるアプリのために、環境変数 PYTHONLEGACYWINDOWSSTDIO, PYTHONLEGACYWINDOWSFSENCODING で元の挙動に戻すこともできます。

あと、 Windows 10 Anniversary Update で MAX_PATH の 260 文字制限を超えられるようになりましたが、 Python も問題のある Windows API を使ってなかったので、これに対応するように Manifest が書かれました。 260文字超えたパスを扱えます。

C99 解禁

Python プログラマーには全く関係のない話ですが、 Python を実装するときのC言語の仕様で C99 の幾つかの機能がやーーーっと解禁されました。 cflags にも -std=c99 が追加されます。

例えば、1行コメントが // で書けるようになったとか、 Py_LOCAL_INLINE(void) foo(void)static inline void foo(void) になったりとか、 int16_t が使えるようになったりとか、ローカル変数が関数やブロックの先頭以外の(初期化する位置で)宣言できるようになったりとか、etcetc、新しい Python ハッカーにとっては大幅にリーダビリティが上がったと思います。君も Python ハッカーにならないか?

なお、早速 Python のバグトラッカーに、 CentOS 5 のデフォルトの GCC でビルドが通らない!って報告が来てます。とても Python らしいですね。新しい gcc インストールしてください。というか CentOS 5 とか RHEL 5 とか捨ててください。

高速化

PythonからCの関数を呼び出す時、今まではVMのスタックから引数を取り出してタプルに入れてC関数に渡していました。 それが新しく加わった呼び出し規約では、VMスタックから直接引数を取り出せるようになり、一時オブジェクトとしてのタプルが不要になりました。

この呼び出し規約は人間が直接書くことはあまり意図されていず、 Argument Clinic という、シグネチャを決められたシンタックスで書けば引数解析コードを自動生成してくれるツールを使って対応することになります。 CPython の多くのビルトイン関数はすでにこのツールを使っているので、いろんなCで書かれた関数の呼び出しが速くなったはずです。

また、バイトコードのフォーマットが大幅にかわり、1つの命令が2バイト使うようになりました。(新しいフォーマットを区別するときはワードコードと呼ばれてます。) これにより Python 3.6 でも少し高速化されてるはずですが、本格的に新しいフォーマットの力を引き出すような高速化は 3.7 に持ち越しです。

他にも将来の高速化へ向けた仕込みとして、 dict に(CPython内部からしか使えないプライベートな)バージョン番号が入って名前解決結果をバージョンが変わるまでキャッシュできるようになったり、バイトコードを格納しているコードオブジェクトに co_extra というフィールドが追加されてサードパーティーのプロファイラーやJITエンジンが利用できるようにしたりしてます。

Ruby 3x3 が話題ですが、 Python の高速化からも目が離せません。

Python と Ruby と typing

この話題について補足しておきます。なお、僕はTAPL脱落組なのであまり正確性は期待しないでください。

背景

Ruby Kaigi で Matz が Ruby3 に向けて考え中の静的型について話されたようです。

少し前から、 Python でも Guido が Dropbox での大量のコードベースを改善していくために type hinting がほしいということで PEP 484 を始めました。

ここでは、 Java などが採用している一番よく知られた nominal subtyping, Go などが採用している structural subtyping, PythonRuby の duck typing, そして PEP 484 が採用した Gradual Typing について、違いをかんたんに紹介していきます。

Nominal Subtyping

まずは次のコードを見てください。

interface IFoo {
  void Foo(String s);
}

class Foo implements IFoo {
  public void Foo(String s) {}
}

class Main {
  public static void main(String[] args) {
    IFoo f = new Foo();
    f.Foo("hello");
  }
}

IFoo f = new Foo() では、 IFoo 型の変数 f を宣言し、 Foo 型のインスタンスで初期化しています。 静的に(実行しなくても)変数に型が付いているので、これは静的型付けです。

IFoo 型の変数に、 Foo 型のインスタンスを代入していますが、これは Foo が IFoo のサブタイプだからです。(ちなみにサブタイプの逆はスーパータイプです。)

サブタイプは、スーパータイプが定義しているAPIをすべて提供している必要があります。例えば、スーパータイプが Foo(string) というメソッドを定義しているなら、サブタイプもそのメソッドを提供しなければなりません。 スーパータイプに対して可能な操作がすべてサブタイプに対しても可能なので、スーパータイプの型の変数にサブタイプの値を代入してもスーパータイプと同じように操作できることが保証されます。つまり、 f.Foo("hello")コンパイル時に安全性が検証され、実行時に fFoo(String) というメソッドを持ってないというエラーは起こりません。

さて、ここまでの説明は静的型付けと subtyping についてでした。では nominal subtyping の nominal とは何でしょうか? それは、 Foo implements IFoo のように、サブタイプ関係を宣言によって定義するという意味です。そして、 nominal subtyping と対になるのが structural subtyping です。

Structural Subtyping (構造的部分型)

Nominal Subtyping が implements や extends といった宣言により部分型関係が成り立っていたのに対して、 Structural Subtyping では構造によって部分型関係が成り立ちます。次の例を見てください。

package main

import (
    "fmt"
    "io"
)

type Reader interface {
    Read(buf []byte) (int, error)
}

type StringReader struct {
    s string
}

func NewStringReader(s string) *StringReader {
    return &StringReader{s}
}

func (r *StringReader) Read(buf []byte) (int, error) {
    l := len(r.s)
    if len(buf) < l {
        copy(buf, r.s)
        r.s = r.s[len(buf):]
        return len(buf), nil
    }
    copy(buf[:l], r.s)
    r.s = ""
    return l, io.EOF
}

func ReadPacket(r Reader) ([]byte, error) {
    header := make([]byte, 1)
    if _, err := r.Read(header); err != nil {
        return nil, err
    }
    l := int(header[0])
    data := make([]byte, l)
    n, err := r.Read(data)
    return data[:n], err
}

func main() {
    r := NewStringReader("\x03foo")
    packet, _ := ReadPacket(r)
    fmt.Printf("%s\n", packet)
}

StringReader 型は Reader 型を明示的に継承していませんが、 ReadPacket(r Reader) に *StringReader 型の値を渡せています。

Go のコンパイラは、 *StringReaderReader インターフェースが宣言しているメソッドを全部定義しているので、サブタイプだと判断しています。これが構造による部分型です。

Structural Subtyping の Nominal Subtyping に対するメリットを考えてみましょう。

今回書きたかった関数 ReadPacket(r Reader) が、本当は os.File 型からパケットを読み込むための関数で、 *StringReader は単に ReadPacket をテストするためのモックだったとします。 もし os.File が何もインターフェイスを継承していなかったとしたら、 nominal typing では、 ReadPacket() の引数の型を、 os.File とモックの両方を直接受け取れるように定義することができません。自分で os.File をラップするクラスを作って Reader インターフェイスを継承しないと行けないので面倒です。 また、 os.FileRead(), Write(), Close(), Seek() を定義したインターフェイスを実装していた場合、 ReadPacket()Read() しか使っていないのに、 *StringReader は3つの使ってないメソッドのダミー実装をしないといけませんでした。

構造的部分型は、このように実際に利用しているメソッドだけを定義したスーパータイプを後付けできるのが魅力です。 ライブラリを提供する人も、実装している型が一つだけなのにユーザーがモックを定義しやすくするためにインターフェイスを作らなくてよくなるので、ライブラリのAPIの見通しがよくなるといったメリットもあります。

Duck Typing

duck typing は動的型付け言語による、「とりあえずやってみてエラー無く動いたらオッケー」という型システムです。(というかこれは型システムなのか?)

次のコードを見てください。

def read_packet(s):
    n = s.recv(1)[0]
    return s.recv(n)

このコードは、ソケットのようなオブジェクトを引数に取ることを期待しています。そのオブジェクトは .recv(int) というメソッドを実装していて、バイト列を返すことを期待しています。バイト列に対して [0] するとその整数値を取り、また s.recv(int) を呼び出して、その戻り値のバイト列を返す、という想定です。

このコードは、次のようにして呼び出してもエラー無く実行することができます。

class S:
    def recv(self, x):
        return [x]

print(read_packet(S()))  # => [1]

戻り値の型は bytes を期待していたのに、配列が返ってきましたね。でも、エラーなく実行できたんだから、これでOKというのが duck typing です。 利用者のメンタルモデルとしては、 インターフェイスの定義を省略した structural subtyping (実際にインターフェイスの型の定義を省略できる structural typing の言語があります。後述) のような型がふんわりと頭の中にあると思いますが、実際の duck typing はもっとガバガバです。

この例だけで、一番のデメリットである脆弱性(型が実行時エラーから何も守ってくれない&実行時エラーにすらならず出力が壊れる危険もある)は十分伝わったと思います。

ですが、このデメリットは非実用的というほどは大きくありません。まず、こんなパズル的なコードを書かない限り、ちゃんと実行時エラーで丁寧なエラーメッセージとバックトレースを吐いて止まります。なので、気軽になんども実行できるようなスクリプトであれば、実行時エラーを見ながら修正するサイクルは、ビルドが遅い言語でコンパイルエラーを見ながら修正するサイクルより早いことだってありえます。

一方のメリットは、インターフェイスの定義が要らない分だけコードが簡潔になることです。 Javaインターフェイスを見る時間で、 PythonRuby なら実装を読めてしまうことだってあります。

とはいえ、プログラムが大規模化してくると、リファクタリングツールや静解析ツール等に十分なヒントを与えられないために改修コストがどんどん膨れ上がる危険性があります。 Python が type hinting を導入したのもそれが理由です。 Python 自体が型を使うのではなく(将来そういうモードが追加される可能性はありますが)、 mypy, Jedi, PyCharm などの静解析、コード補完、IDEが型情報を使うので、 typing ではなく type hinting と呼んでいます。

Python の Gradual Typing

Python の type hinting で使われているシステムが PEP 483 で紹介されている Gradual Typing です。 漸進的型付けとは何か も良い紹介記事なので参考にしてください。

ざっくりと言えば、 Gradual Typing とは、 通常の Subtyping に Any という特殊な型を追加したものです。

通常の部分型関係は順序関係になっていて、推移律が成り立ちます。つまり、 B が A のサブクラス、 C が B のサブクラスなら、 C は A のサブクラスでもあります。 (C > B > A) なので、 C 型の値を A 型にアップキャスト(サブタイプからスーパータイプへのキャスト)が可能です。

一方で Any 型は部分型関係の外にいて、すべての型から Any にキャストできますし、 Any からすべての型にキャストできます。ですが部分型関係の外にいるので推移律も当てはまらず、 A が Any にキャストできて、 Any が C にキャストできるからといって、直接 A から C へのキャストが許される訳ではありません。

この Any は type hint がまだついてないとか、任意の型を取りうるという意味になります。例えば JSON の object を dict にするとき、がんばらない型ヒントは Dict[string, Any] になります。(頑張ると Any ではなく Union という「幾つかの型のどれか」型になるのですが、その Union が取り得る型の1つが JSON object を表す dict 型になるので、再帰的な定義をすることになります。)

さて、type hinting をどんどん書いていくと、結局静的型付け言語と同じくらいコードが増えそうな気がします。

しかし実際にはそこまで増えないことが多そうです。というのは、上の例であったような「モックにすげ替えるためだけにインターフェイス型を用意する」必要がないからです。 モックオブジェクトを Any 型にしてしまえば、type hint に具象型を直接書いてる引数にでもモックを突っ込めますし、そもそも mypy などの静解析ツールの対象からテストディレクトリを外してしまうという手だってあります。

Python の type hinting はまだまだ開発途中で実用段階とは言い難いですが、だいたいこんな感じで、動的型の特徴を殺すこと無く静的型のメリットの一つである手厚いツールの支援を取り込む方向で進んでいます。

Ruby Kaigi で語られた構想では、 Python よりももっとプログラマーが書く型情報が減る方向で考えられているようなので、どうなっていくのか期待して見守りたいと思います。

追記: 静的な duck typing

上の例では structural subtyping と duck typing の比較として Go と Python を比べていましたが、実際には structural subtyping は単に部分型に関するルールでしかないので、直接比較できるようなものではありません。

実際に、引数の型をコード上に書かなくても静的な duck typing とよべるような型導出を行ってくれる言語 (OCaml が有名) もあります。つまり、

def read_packet(s):
    n = s.recv(1)[0]
    return s.recv(n)

このコードから、 "s の型は、 .recv(int) というメソッドを持っていて、そのメソッドの戻り地は添字アクセス (x[0]) できるような型で、..." という暗黙の型を実行せずに静解析で導出します。あとはこの関数を呼び出すときの実引数の型が、導出された暗黙の型のサブタイプかどうかを structural subtyping でテストします。

このコードから暗黙の型を導出するステップは静的な duck typing の用にみえますし、動的な duck typing でプログラムしているプログラマーの頭の中にふんわりと存在する型ともかなり一致すると思います。たぶん Matz の soft-typing 構想にも近くて、これが冒頭の Matz の発言につながったのだと思います。

動的な duck typing はもっとガバガバ(引数の型ではなく値によって戻り値の型が変わるなんてこともザラ)なので、この静的な duck typing は動的 duck typing に対する完全な静的型チェックにはならないですが、 Python の Gradual Typing が Any を導入したのと同じようになんらかのクッションを挟むことでうまく適用できる可能性があります。

Python 3.6 に向けて新しい dict 実装の人柱募集

会社のBlogに書いた通り、現在新しい dict の実装を試しています。

また、 shared key を削除して実装を削れた分、別の効率のいい特殊化 dict を実装して、 compact + shared よりも高い効率を狙うこともできます。 今はあるアイデアのPOCを実装中なので、採用されたらまた紹介します。

と書いたのですが、とりあえず Python のテストは完走するようになりました。

github.com

Python-Dev に投稿したメール の通り、 SphinxPython の Doc を make html するのにかかるメモリ使用量は、 Python 3.6a2+ (Python 3.5 と同等の key sharing dict) では 176MB, 上の key sharing を外した新 dict 実装ブランチでは 160MB で、 key sharing を外しても十分に省メモリかできている事が判ります。

Python-Dev では、アプリケーションによってはほとんどの dict がインスタンス__dict__ なんじゃないか、その場合は無視できないリグレッションになるのではないか、という意見がでていて、説得するためにリアルなOOPで書かれたアプリケーションにおけるメモリ使用量のデータをもっと集めたいところです。

そこで、興味があって計測に適したアプリケーションを用意できる人は interned-dict ブランチ を試してみていただけないでしょうか?

計測に適したアプリケーションは次のとおりです。

  • OOPで書かれていて、かなりの数のインスタンスを作る
  • Python 3.6a2 と上記のブランチの2つのプロセスで同じ状況を再現でき、その時のメモリ使用量を計測できる
  • 繰り返して安定した結果を得られる

また、 Sphinx のビルドのように CPU バウンドで数分で実行できて実行時間が安定しているアプリケーションであれば、 /usr/bin/time を使って maxrss と同時に実行時間も計測していただけると助かります。 (その際、比較対照の 3.6a2 は同じ configure オプション、同じ環境でビルドしたものでおねがいします。)

計測結果は、

  • どんな種類のアプリケーションか (Webアプリ、GUIアプリ等)
  • どんなライブラリ・フレームワークを使っているか
  • どうやって計測したか
  • (アプリケーションを公開可能な場合)完全な再現手順

をつけて、

[Python-Dev] Idea: more compact, interned string key only dict for namespace.

に返信するか、上記のプルリクエストにコメントしてもらえれば私が転載します。

また、万が一クラッシュしてしまった場合も、上のプルリクエストに (再現可能なら faulthandler を有効にした状態で得たスタックトレースをつけて) コメントしてください。

このブログに乗せているコードは引用を除き CC0 1.0 で提供します。