最近の Python-dev (2017-02)
バックナンバー:
新しいコア開発者
主にドキュメントの改善について継続的に活動されていることが評価されて、 Mariatta さんがコア開発者に加わりました。
僕は大きなパッチで目立ってコア開発者になってしまったので、このBlogの読者に間違った印象を与えてしまわないように強調しておきたいのですが、コア開発者に重要なのは他のコア開発者と協調してルールを守って貢献することです。
小さい簡単な修正や他人の pull request のレビューなどでも、継続的にコア開発者とやりとりする機会があれば、結構簡単にコア開発者になれるはずです。 後述する Github 移行によって敷居が下がったはずなので、この記事を読んでくださっている方もぜひ狙ってみてください。
Github 移行
2/11 (アメリカ時間なので日本では2/12)に、Python のリポジトリが https://github.com/python/cpython に移行しました。
以前は mercurial を利用していたのでリポジトリの変換が必要になるのですが、以前 python/cpython にあったミラーとは別物でコミットのハッシュが変わったので、ミラーを利用していた人は注意が必要です。 ミラーリポジトリは python/cpython-mirror に移動しましたが、今後削除されるかもしれません。
今までコミットログは "Issue #12345: " で始まっていたのですが、この #12345
は将来 Github にこの番号のプルリクエストが作られてもリンクされないらしいです。 Github えらいですね。
今後のコミットでは bugs.python.org の issue を参照するときは "bpo-12345" のように書くことになります。 Github の pull request を参照するときも #1234
ではなく GH-1234
が推奨されます。(これでもちゃんと Github で自動リンクされます)
と、ここまでは良いんですが、 issue トラッカーを Github 外に置いてる他のプロジェクトの経験者がGreasemonkey使って bpo-12345 をリンク化するようにしたら便利だよー、という話題が発展して、リポジトリ変換時に過去のコミットログの (Issue|issue) #12345
を bpo-12345
に変換しようという話がマイグレーションの決行数日前に盛り上がってしまいました。
僕は直前にそんな変更入れるのはありえないと思ってコミットログ書き換えには -1 してたのですが、実際にコミットログを書き換えて変換したリポジトリを見て問題が無いかチェックしはじめたりして当日までどうなるか分からない状況でした。 最終的には、コミットログはたくさんのコミッターのもので、ほんの2,3日の議論だけで、議論に参加できなかったコミッターを無視して勝手に書き換えて良い物ではないという結論になって、当初の予定通りコミットログ書き換えはナシになりました。
まだまだ問題点も多く、マイグレーション後に開発される予定になっている bot がまだできてないので作業的には今までより楽になった気はしないのですが、開発に参加する敷居は確実に下がっていて pull request をして Contributer Agreement にサインしてくれる人がたくさんいるので、これから新しいコア開発者が増えることに期待しています。
siphash-1-3
最近のruby-core (2017年1月) を読んで siphash-1-3 について知り、 Python にも 提案してみました。
一旦今の siphash-2-4 をそのまま書き換えるパッチを投げたのですが、 configure 時に選択できるようにしてくれた人がいたので、今はその人が pull request を作ってくれるのを待っています。
パフォーマンスへの影響ですが、 Python の文字列オブジェクトは immutable でハッシュ値は一度計算されるとオブジェクトの中にキャッシュされるので、ハッシュ関数が数割速くなった程度ではパフォーマンスには大きい影響はありません。 JSON をデコードするときなど、新しい文字列オブジェクトを dict のキーに追加する処理が集中するときにはちょっと差が出るのですが、標準ライブラリのJSONはそこまでカリカリにチューンされてない (文字列のエスケープ処理など部分的にCを使ってるけど全体は Python で書かれてる) のでそれでも2~3%程度しか差が出ませんでした。
とはいえ、今後も 「最近のruby-core」と「最近のPython-dev」でお互いにいい影響を与えあっていけたらなぁと思います。
re performance
Go の開発者の一人でもある Russ Cox さんの、 PCRE などの正規表現エンジンが (a?){n}a{n} に "a"*n (n=3 なら a?a?a?a{3} と aaa) をマッチさせるのに O(2^n) の計算量がかかってしまうけど、バックトラックじゃなくてNFA使えば O(n^2) にできるよねって記事があります。 https://swtch.com/~rsc/regexp/regexp1.html
これは新しい記事ではないんですが、 Python の標準ライブラリの re がずっとこの遅い方式のままだよねということがMLで話題になりました。
Python の re を置き換えることを目的に開発されていた regex ではこの例の正規表現が遅くならないけど、置き換えどうすんの?やっぱり Python 本体と別にバージョンアップできる現状維持でいいや。 requests みたいにPythonの re のドキュメントからオススメサードパーティーライブラリとして regex へのリンクを書いておけば良いんじゃない? regex はいくつかチェック入れてるからこの記事の例で遅くならないけれども、 NFA じゃなくてバックトラックなのは変わらないよ、NFA使いたいなら re2 を使おう。という感じの議論がされました。
ちなみにGoの正規表現は遅いと昨年のISUCONで話題になりましたが、C実装のre2よりは遅いもののちゃんとNFAになっていて、 Python などの言語よりは redos に強くなっています。
Python で web 開発している人など、外部入力に対して正規表現を使う場面があるなら、 Facebook製の re2 binding である fb-re2 か、その fork で re との互換性を重視してる re2 を使ってみてはいかがでしょう?
Investigating Python memory footprint of one real Web application
Instagram の開発者による、 Dismissing Python Garbage Collection at Instagram という記事が、 Python の「たとえばGCを止める」案件だと一部で話題になりました。
この記事はパフォーマンスというよりは prefork の copy on write 効果でメモリ節約したいのに循環参照GCのせいでメモリが共有されなくなるからという理由で GC を止めていますし、そもそも循環参照GCの手前にある参照カウントGCまで止めるという話ではないので、Ruby on Rails の「たとえばGCを止める」 とは全く別の話です。
Perl や昔の php と同じく、参照カウントGCだけでもワリと普通に動きますし、 循環参照GCは適切にチューニングするのも難しくありません。
それはさておき、弊社ではCPUバウンドのプロセス数はコア数の2倍程度に絞る事が多いのでプロダクション環境でメモリ不足に成ることはほとんどなくて、 AWS の c4 インスタンスでもメモリが全然余ってたりするのですが、世の中には別の思想や条件で設計・構成されてるアプリもあるわけで、メモリ使用量は少ないに越したことはありません。
そこで弊社の開発中のとある案件のコードを拝借して、起動後のメモリ使用量の内訳を解析してみたのがこのMLのスレッドになります。 (解析方法については別の機会に紹介します。) 内容を幾つか紹介しておきます。
弊社のコードがPython の type hinting を多用していて -> List[User]
みたいなのが大量にあるんだけど、この List などが Python の ABC (実際に継承していないクラス間にサブクラス関係を定義できるようにする仕組み) を継承して実装されていて、それが SQLAlchemy の ABC の使い方と相性が悪く、サブクラス関係の判定を高速化するためのネガティブキャッシュを大量に生成してしまっていました。 これは typing
モジュールが List[X]
と List
でキャッシュを共有するという最適化を導入したので、3月リリース予定の Python 3.6.1 で改善されます。
その他、 type hinting によるメモリ使用量のオーバーヘッドは、大きくもないけど、状況によっては無視もできなさそうだから、 docstring を読み込まない -O2
オプションみたいにランタイムに読み込まない最適化オプションがあった方が良いかもしれない。
型ごとのメモリ使用量は、 str > dict, tuple > その他。 str は特に SQLAlchemy の docstring が大きい。 -O2
を使えば dict, tuple > str > その他になり、 dict と tuple がそれぞれ 10% ずつメモリを使っている。 -O2
は docstring だけでなく assert も消えてしまうので、最適化を個別に制御するオプションが欲しい。
その他、Python 3.6 で compact になった dict をもう1段小さくしたり、 tuple を減らすような実装上のアイデアも出したので、今年末に feature freeze になる Python 3.7 までにできるだけ試していきたい。
続・FASTCALL
先月の記事で tp_call
の FASTCALL 版の tp_fastcall
を追加する話をしたのですが、このスロットと呼ばれる関数ポインタの追加は、ABI互換性 (限定されたAPIではABIまで後方互換性が確保されていて、古い Python 向けにビルドされた拡張モジュールが新しい Python からも使える) などの関係もあり結構大変です。
そこで、どうせ追加するなら同時に tp_new
, tp_init
というスロットも FASTCALL 対応したバージョンの tp_fastnew
, tp_fastinit
を追加してしまえと、 Victor さんがすごく頑張りました。頑張ったんですが、、、マイクロベンチは速くなるものの実際のライブラリを使ったマクロベンチでは大きな速度差が観測できず、一方で型システムのコア部分に手を入れるパッチなので僕がレビューしても「これで想定しているケースで動くのは分かる、でもサードパーティーの行儀悪い拡張とかとの相性で起こる副作用は予想できない」という感じなので、ペンディングになりました。
それ以外の部分では FASTCALL の適用範囲は地道に増えています。例えば、 Python で __getitem__
などの特殊メソッドを実装してスロットを埋めたとき、スロットからその Python 関数を呼び出すのに FASTCALL を使うようになったのと、 Python 3.6 でメソッド呼び出しを高速化したテクニックを組み合わせることで、マイクロベンチで最大で30%の高速化ができました。
@methane