Python

2018年06月29日

最近のPython-dev(2018-06)

はてなブックマークに登録

バックナンバー:

Python 3.7

日本時間の6/28に Python 3.7 がリリースされました。 終盤に駆け込みで2つ大き目の変更が入りました。

  • Unicode 11 対応
  • ASTの変更の revert

Unicode 11 はデータの更新だけなので危険が少ないし、1.5年後の3.8まちにはしたくないということでGoサインが出ました。

AST は、僕が中心で行ったAST段階での定数畳み込みの準備として、 docstring とそうでない文字列の区別をAST段階でつきやすくために変更していました。

その変更によりいくつかのライブラリで問題が報告されていたのですが、AST はもともと後方互換性を保証していないからとライブラリ側での対処がされていました。しかし、RCフェーズに入ってから IPython が他と異なる壊れ方をしていて、今のASTに関するAPIが IPython のニーズを満たしておらず想定外の対処が必要になっていたことから、一旦元に戻してまたAPIから考え直そうということになりました。

もちろんこのASTの変更を元に定数畳み込みが作られていたので、定数畳み込み後に docstring と文字列定数の区別がつかなくならないように Python 側にハックが必要になるのですが、僕が週末作業できないあいだに Serhiy がやってくれました。 https://github.com/python/cpython/pull/7121/files/8ee9227599583c460ff8faf9c1ab1670559a1224#r191080934

Compact GC Header

Python 3.8 に向けた大きめの改善の1つめに、2000年以来変更されてない循環参照GC用のメモリオーバーヘッドの削減に挑戦しています。

ちょうど 6/25 に、低レベルプログラミングが話題になることが多い Turing Complete FM のリスナーが集まる ミートアップイベンント があったので、そこで発表してきました。

基本的にはつぎのような戦略で、GC用ヘッダのメモリ使用量をポインタ3つ分から2つ分に削減しています。

  • 双方向リンクリストの逆方向リンクを、一時的に潰してGC用の参照カウントに使い、必要になる前に戻す。
  • ポインタの下位3bitを他の用途に使う (tagged pointer)。参照カウントに使うときは左に3ビットシフトする。

一旦動くようになったものの、問題がありました。

  • 参照カウントを3ビットシフトしたら、ポインタサイズが4バイトの32bitアーキテクチャでオーバーフローする危険がある。
  • マイナーなアーキテクチャ上のマイナーな malloc 実装が8バイトアラインしなかった場合、ポインタの下位3bitを使うのも危険。

その後、2種類の実装のメンテナンスはなるべく避けたいので、必要な期間が一番短かったビットを逆方向リンクのポインタではなく順方向リンクのポインタの下位ビットに押し込めるという改良を行いました。現在レビュー待ちです。

プルリクエスト

FASTCALL を PyFunction, PyCFunction 以外でも利用できるように

Python 3.6 から段階的に、インタプリタ内部で関数の引数の受け渡し方を変更する FASTCALL 方式が導入されています。

これを利用するために、現在は Python で実装された(バイトコードをインタプリタが実装する)関数を表すクラスと、C言語で実装された関数/メソッドを表すクラスが特別扱いされています。

さて、 Cython はもちろんC/C++言語で関数を実装するのですが、トレースバックなどでソースコードを表示できるようにしたいなど、Python組み込みのC言語で実装されたクラスでは足りない機能を追加するためには独自の型を使う必要があり、そうすると FASTCALL が利用できません。

それを改善するためにいくつかの提案がされていて、最新のものが PEP 580 になります。

提案者の議論の仕方がちょーっと強引すぎて摩擦を感じてはいるのですが、技術的にはまぁ正しい方向だと思うし、ちゃんと動く参照実装も素早く作ってくれているので、個人的には前向きに捉えています。

とはいえ、そもそも FASTCALL ってまだ破壊的変更が入るかもしれない内部専用APIを Cython が先走って採用しているだけで、どう考えても PEP 580 は先走りすぎなんですよね。

まだ 3.8 の開発は始まったばかりなので、 FASTCALL を stable & public 扱いにできないかの議論を始めたりしてフォローに回っています。

PEP 572 (代入演算子)

PEP 572 の議論は大炎上しました。5月にLanguage Summit があってそこで議論のオーバーヒートをどう扱うかなどの話がされたのですが、 LWN.net がその まとめ を作ってくれたところから、またML上での議論が再開し長大なスレッドになっています。。。


@methane

songofacandy at 14:39|この記事のURLComments(0)
2018年04月27日

最近のPython-dev(2018-04)

はてなブックマークに登録

バックナンバー:

Python 3.7 がベータになり、大きな変更はなく安定期に入りました。 その間、Python の言語自体やエコシステムに関して重要な話題が幾つかありました。


pypi.python.org から pypi.org

長年 Python のエコシステムを支えてくれていた PyPI がリニューアルしました。

Python 3 への移行を始めとしてモダン化され、 Markdown で書いた README をレンダリングできるようになるなどの改善も入っています。

IRC から Zulip chat へ

freenode に python-dev という IRC チャンネルがあるのですが、新しい貢献者がコミュニケーションを取るのに今更IRCを使うのはハードルが高いんじゃないか。Slack や Discord みたいなモダンな環境を試してみないかというメールを投げた所、 Zulip を試用することになりました。

今のところは python-dev とその周辺に用途が限定されていますが、結果が好評なら将来的にもう少し広い用途にも開放されるかもしれません。

個人的には、UIやモバイルアプリの完成度は Slack や Discord にかなわないものの、サブチャンネル的な topic という機能が便利で、複数の話題が並列したときに1つの話題に集中でき、なおかつ Slack のスレッドのように議論が見えにくくなることもないという点にメリットを感じています。

RHEL 7.5 が Python 2 を deprecate

RHEL 7.5 がリリースされ、リリースノートで Python 2 が deprecate されました。つぎのメジャーバージョン (RHEL 8?) で Python 2 が削除され Python 3 だけがサポートされるようです。

Ubuntu 18.04 LTS の方は Python 2 を main リポジトリから外せませんでしたが、 20.04 LTS までには外すでしょうから、(サポート終了後のマシンが一部残るものの) Python 2 を使った環境はだいたい 2025 年ごろにリタイアすることになると思われます。

PEP 394 アップデート

PEP 394 -- The "python" Command on Unix-Like Systems

この PEP は、 Python 2 と3 の非互換による苦痛を緩和するためのコマンド名とshebangについてのガイドラインを提供しています。大雑把に言うと次のようになっています。

  • Python 2 が利用可能なら python2 コマンドを、 Python 3 が利用可能なら python3 コマンドを用意する。
  • python コマンドは python2 コマンドと同じ Python を起動するようにする。
  • Python 2 にしか対応しないスクリプトは shebang で python2 を使う。 Python 3 にしか対応しないスクリプトは shebang で python3 を使う。両対応のスクリプトが shebang に python を使う。

これはあくまでもガイドラインであり、 Gentoo や Arch のように python コマンドがデフォルトで Python 3 になっているディストリビューションもあるし、 macOS は python2 コマンドを用意してくれませんが、それでも Debian, Ubuntu, Fedora, Red Hat の開発者がこのガイドラインにより足並みを揃えてくれるだけでも無いよりはマシです。

さて、このガイドラインは時々更新されることになっていて、 Fedora / RHEL が Python 2 を捨てるのに合わせてどうするかが話題になりました。現在の Guido の考えはだいたいこんな感じです。

  • Python 2.7.10 が出ると共に、「2桁バージョン嫌い」を克服したので、Python 3.9 の次は多分 3.10 になる。 "python3" というコマンド名を使い続けるのに問題はない。 Python 4 は GIL の卒業といった大きな変更が入るときになるだろう。
  • (Dropbox では) まだ Python 2 と 3 の両方を使っているので、 "python" が常に Python 2 であって欲しい。
  • "python" コマンドが存在しないのは問題ない。
  • Python 3 で venv を作ったときに python というコマンドをオーバーライドしてしまうのは間違いだった。

Python 2 を捨てるのを機に "python" コマンドを Python 3 にしたかった Fedora / Red Hat のメンテナにとっては水を差された形です。

一方ですでに "python" コマンドが Python 3 になっているディストリや venv は現存するので、強引に "python" コマンドが Python 3 を指すことを禁止することもしません。結論として、今回のアップデートの大きな変更点は次のような変更になります。

  • "python" コマンドは存在しなくて良い (存在する場合は、今まで通り "python2" コマンドと同一であるべき)
  • (この PEP が有効な期間中に) "python" コマンドが "python3" になることを期待させる段落を削除

ということで、「早くどこでも python というコマンドが Python 3 にならないかなー」という希望は捨てて、 "python3" が正式な推奨されるコマンド名なんだということに慣れたほうが良さそうです。

PEP 572 -- Assignment Expressions

Python はずっと代入を「文 (statement)」としてきましたが、新しく := という演算子で代入式を追加しようという提案です。

例えば次のようなコードが書けるようになります。

# 572 があるとき
if m := re.match(pat, s):
    # do something with m

# ないとき
m = re.match(pat, s)
if m:
    # do something with m

メールの量が膨大すぎるのと娘の誕生日があったので完全には議論を追えてないんですが、最初のモチベーションはリスト内包表記で値の再利用をしたいということだったと思います。

# foo() を2回計算してしまっている
ys = [foo(x) for x in xs if foo(x) is not None]

# 一回でやりたい
ys = [z for x in xs if (z := foo(x)) is not None]

内包表記に限った構文拡張や、新たにブロックスコープを追加するなど、いろんなアイデアが出ましたが、シンプルさと便利さのバランスで今の提案に落ち着きました。

とはいえ、 Python はずっと「代入は文」という制約と共にあった言語なので、この提案は制約がもたらす可読性を失わせるものでもあります。

今までは複雑な式を読み飛ばしても代入を見逃す危険は無かったのが、PEP 572 が承認された場合は、自分が代入を見逃していないか疑いながらコードを読む必要が出てきます。

なので「追加されれば便利に使うだろうけれども今の段階では +1 はしない」という慎重派も多いですし、反対の人もいます。私も慎重派の一人で、メリットを質だけじゃなくて量(頻度)でも説明してほしいなと思っています。

参照実装もあるので興味のある人は試してみてください。(でもMLでの議論に参加するときは冷静にお願いします。)


@methane

songofacandy at 12:40|この記事のURLComments(0)
2018年01月30日

最近のPython-dev(2018-01)

はてなブックマークに登録

バックナンバー:

Python 3.7b1 が29日の予定です。問題なければ日本時間の今日中に出るはずですが、buildbotやTravisが不安定なので少し遅れるかもしれません。これで feature freeze なので新機能追加やパフォーマンス向上などは基本的に終わりです。

beta 前の駆け込みで、先月号で紹介したAcceptされたPEPを実装する大型コミットがたくさんマージされました。(間に合わなかったものは無いはず。たぶん。)

今回は私が関係していたところを中心に紹介していきます。


dict の順序の言語仕様化

Python 3.6 から dict が挿入順序を保存するようになりましたが、この挙動は実装詳細であり、まだ言語仕様ではありませんでした。そのため、他の Python 実装で同じ挙動になることは保証されません。具体的には PyPy は同じ挙動(というかCPythonがPyPy実装を真似した)ですが、 MicroPython は違います。

この順序に依存しないように、内部では維持している順序をイテレート時にランダム化するような改造をするか、便利だしいっその事言語仕様にしてしまうかという話題があって、結果として 3.7 からは言語仕様になることになりました。

これに関連して、僕が collections.OrderedDict からリンクリストを削ってメモリ使用量を半減する実装を試していたのですが、挿入順を維持するためだけなら dict を使えるのでこの実装はディスコンになりました。

hasattr(), getattr() の高速化

https://github.com/python/cpython/pull/5173

hasattr(), getattr() は探索する属性が見つからない場合、内部で一度 AttributeError を発生してからそれを握りつぶして、 hasattr なら False, getattr なら第三引数に渡されたデフォルト値を返すという挙動をしていました。

この例外を発生させる+潰すの組み合わせが若干のオーバーヘッドになっているのでなんとかしたいのですが、すべてのケースで回避することはできません。 Python で __getattr__ を定義しているときにも AttributeError を投げるのが決まり(プロトコル)ですし、Cレベルでこれに相当する tp_getattro というスロットも同じプロトコルにもとづいています。

しかし、属性アクセスについて何らかのカスタマイズが不要な場合、 tp_getattro にはデフォルトで PyObject_GenericGetAttr() という関数ポインタが設定されています。 tp_getattro を呼び出す前に、それが PyObject_GenericGetAttr であれば、属性が見つからなかったことを例外を使わずに返す特殊化された関数を呼ぶようにしました。

これで、 getattr をカスタマイズしていない多くの型で getattr() や hasattr() が高速になりました。 例えば Django のテンプレートで hasattr(s, '__html__') としているところがあり、かなりの回数で False を返すので、少し高速化できたことを確認しています。

私は hasattr(), getattr() だけを高速化していたのですが、さらに Serhiy さんが _PyObject_Lookup という名前の新プライベートAPIとして整備して、 Python 内部で PyObject_GetAttr() を呼んでから AttributeError を消していたすべての呼び出し元を新APIを使うように書き換えてくれました。

定数畳み込み

例えば 1 + 2 というコードがあったとき、1 と 2 という定数をスタックに積んでから BINARY_ADD するというバイトコードが一旦作られます。

その後、 peephole と呼ばれているバイトコードレベルの最適化で、連続する LOAD_CONST のあとに BINARY_ADD が来たときは、1 + 2 を計算してしまって結果の 3 を定数テーブルに登録し、1つの LOAD_CONST に置き換えるという最適化をしていました。これを定数畳み込みといいます。

しかし従来の方法には欠点がありました。畳み込み前にテーブルに入っていた定数が不要になるかもしれないのに、そのまま残ってしまっていたのです。次のサンプルコードでは、定数3しか利用していないのに、1と2がテーブルに残っている例です。 (なお、co_consts の最初にある None は docstring なので消せません)

>>> import dis
>>> def x():
...     return 1+2
...
>>> x.__code__.co_consts
(None, 1, 2, 3)
>>> dis.dis(x)
  2           0 LOAD_CONST               3 (3)
              2 RETURN_VALUE

定数畳み込みのあとに定数テーブルを再構築するという最適化が提案されたこともありますが、定数テーブルを2回作り直すよりも定数畳み込みをバイトコード生成前のASTの段階でやってしまう方が筋が良いだろうということで却下されていました。

そしてASTレベルの定数畳み込みは、Eugene Toderさんが実装されていたのですが、それも何年も放置されていました。 それを見つけて今のバージョンで動くように手直して、 Python 3.7 に入りました。 Python 3.7 では上の例はこうなります。

>>> x.__code__.co_consts
(None, 3)
>>> dis.dis(x)
  2           0 LOAD_CONST               1 (3)
              2 RETURN_VALUE

TextIOWrapper の encoding を変更可能に

「Python 3.7 でテキストファイルのエンコーディングを初期化後に変更可能になります」 という記事で紹介したのでそちらを参照してください。

lru_cache の省メモリ化

functools.lru_cache は dict と双方向リンクリストを使って LRU を実装しています。

dict と双方向リンクリストという組み合わせは OrderedDict と同じです。同じ実装が2種類あるのは無駄だなぁと思って一度 OrderedDict を使う形で書き直してみたのですが、パフォーマンスが落ちてしまいました。 OrderedDict は dict を継承しているしがらみが強くて、要素を削除する時などに内部で探索をなんどかやり直していることがあり、 dict + 双方向リンクリストとしての実装は lru_cache の内部実装の方が圧倒的に素直だったのです。

ということでこの試みは失敗したのですが、その際に lru_cache の双方向リストの各エントリがGC可能オブジェクトになっていることを発見しました。これにより、一要素ごとに3ワード (64bit マシンでは 24byte) のオーバーヘッドがかかり、GCの処理時間も長くなります。

Python の巡回参照GCを動作させるには、各エントリを個別に走査してもらわなくても lru_cache 本体が走査されるときにリンクリストを舐めて行けばすむということを思いつき、各エントリのGCヘッダを削除することができました。これでキャッシュされている要素数 * 24バイトの削減と、lru_cacheをヘビーに使っているアプリでのGC速度の向上が見込めます。

ABCのC実装

https://github.com/python/cpython/pull/5273

Python の起動時間を遅くする3大要因が ABC と Enum と正規表現です。(独断と偏見)

起動時にほぼ確実に import されるモジュールに _collections_abc があるのですが、これを見ると、たくさんの ABC が定義され、たくさんの型がそこに register されているのがわかります。

以前に紹介した importtime オプションで確認してみます。

$ local/py37/bin/python3 -Ximporttime -c 42
import time: self [us] | cumulative | imported package
...
import time:        74 |         74 |       _stat
import time:       202 |        275 |     stat
import time:       166 |        166 |       genericpath
import time:       244 |        409 |     posixpath
import time:      1696 |       1696 |     _collections_abc
import time:       725 |       3105 |   os
import time:       218 |        218 |   _sitebuiltins
import time:       160 |        160 |     _locale
import time:       155 |        315 |   _bootlocale
import time:       500 |        500 |   sitecustomize
import time:       220 |        220 |   usercustomize
import time:       692 |       5047 | site

ABCの定義とregisterの両方を根本的に高速化するために、Ivan Levkivskyi さん (つづりが難しい) がC実装を作ってくれたのですが、色々問題があって協力して改善中です。

量としては多くないんだけどメタクラスと weakref を扱うので正しく実装するのが面倒なんですよね。。。

なんとか 3.7b1 までにマージしても問題なく動きそうなところまでは持っていったのですが、 3.7 のリリースマネージャーが b1 後にしても良いよと例外を認めてくれたので、一旦落ち着いて実装を磨き直す予定です。

これがマージされると、 Python の起動時間 (python -c 42) はこんな感じになります。

バージョン起動時間(ms)
3.616.4
3.7b114.4
3.7b1 + C-ABC13.9

また、 python -c 'import asyncio' の時間も、 Python 3.6, 3.7b1, C-ABC で 88.2ms -> 58.7ms -> 57.6ms と早くなって来ています。

(追記) str, bytes, bytearray に isascii() メソッドを追加

Python の str の isdigit() などのメソッドは Unicode 対応しているために非ASCII文字に対しても True を返してしまいます。また int() も Unicode 対応しています。

>>> s = "123"
>>> print(ascii(s))
'\uff11\uff12\uff13'
>>> s.isdigit()
True
>>> int(s)
123

しかし、ASCII文字だけを受け付けたいというケースもあるでしょう。その時に .encode('ascii') が例外を投げないかチェックするのは手間です。

そこで str.isascii() メソッドの追加を提案しました。これを使えば s.isascii() and s.isdigit() で ASCII の整数だけが使われているかをチェックできます。

いろいろな意見はあったものの基本的に反対意見はなくて、Guidoがbytesとbytearrayにも同じメソッドを追加しておいてというメッセージで決まりました。

実装の話として、 str は内部で ASCII フラグを持っている (このフラグで UTF-8 へのエンコードがスキップできる) ので O(1) ですが、 bytes と bytearray はバイト列を舐めて最上位ビットが立っていないかチェックしているので文字列長nに対して O(n) になります。

(追記) PEP 567 -- Context Variables

引数を使わずに関数呼び出しを超えて状態を持ち回りたい事があります。

例えばWebアプリケーションだとしたら、ログを書く時に現在のリクエストのIDとかユーザーIDを残したいけれども、ログを書く全ての関数の引数にそれらを追加したくないなどです。

Python の標準ライブラリで言えば、 decimal モジュールが演算精度などをコンテキストとして管理しています。

こういった状態は、マルチスレッドではスレッドごとにコンテキストが異なるので、従来はスレッドローカル変数に格納していました。

しかし、 asyncio などの非同期フレームワークでは、複数のコンテキストが1つのスレッドで動くので、スレッドローカルでのコンテキスト管理がうまく行きません。そこでスレッドと独立したコンテキスト管理のためのライブラリが PEP 567 で追加されました。

これを使えば、各スレッドが「現在のコンテキスト」を持っているものの、スレッド内でコンテキストの切り替えが明示的にできるので、 asyncio だとイベントループが関数呼出しする毎にそのコンテキストを復元してやることができます。

MLでの議論は長すぎて私は見てなかったのですが、そろそろ Accept されそうだというところで依頼されて実装の方のレビューをしていました。

内部実装としては HAMT と呼ばれるアルゴリズムを利用していて、名前は聞いたことがあったもの詳細は知らなかったので、それを理解しながらC実装をレビューするのは楽しかったです。

songofacandy at 16:01|この記事のURLComments(0)
2018年01月23日

Pythonアプリの起動を高速化する

はてなブックマークに登録

pipenv 9.0.2 のリリースでCLIの大幅な高速化をしたというアナウンスを見かけました。

興味を持ってすぐに試してみたのですが、あまり速く感じられませんでした。そこで Python 3.7 の新機能を使って速度を調査することにしました。

この記事ではその新機能と実際の使い方を紹介します。

起動時間 ≒ import時間

pipenv -h のようなコマンドの実行時間は、実際にヘルプメッセージを表示するための時間よりもずっと長くなります。

アプリケーションが起動するときには、設定ファイルの読み込みなど一定の処理が必要になるものです。

しかし、Pythonアプリケーションの場合、モジュールのインポートが起動時間の殆どをしめる事が多いです。

たとえば私の環境では pipenv --version は 800ms かかったのですが、そのうち 700ms は import pipenv に使われていました。

$ time local/py37/bin/python3 -c 'import pipenv'

real    0m0.696s
user    0m0.648s
sys     0m0.048s

$ time local/py37/bin/pipenv --version
pipenv, version 9.0.3

real    0m0.812s
user    0m0.761s
sys     0m0.052s

モジュールのimportにかかった時間を表示する

Python 3.7 ではモジュールのimportにかかった時間を表示する機能が追加されました。

この機能は -X importtime オプションか、 PYTHONPROFILEIMPORTTIME=1 と環境変数に設定することで有効になります。

たとえば pipenv を調査したい時は次の2つの方法があります。

$ python3.7 -X importtime -c 'import pipenv' 2> pipenv-imports
または
$ PYTHONPROFILEIMPORTTIME=1 pipenv --version 2>pipenv-imports

前者は import pipenv だけを調査し、後者は pipenv コマンド実行中の全ての import を調査するという違いがあります。

後者の出力結果の例が こちら です。

import時間を解析する

この出力結果の最後の部分を見てみます。

import time: self [us] | cumulative | imported package
...
import time:      3246 |     578972 |   pipenv.cli
import time:       507 |     579479 | pipenv

最後の行の右側の 579479 は、 import pipenv にトータルで 579479us かかったことを意味しています。

pipenv を import する間に、たくさんの他のモジュールが import されます。上の例で言えば、 pipenv は pipenv.cli を import しています。(サブ import は2スペースでインデントされます)

最後の行の左側の 507 は、 pipenv モジュール単体の import (モジュールのグローバルの実行を含む) にたった 507us しか掛かっていないことを意味しています。579 479 - 507 = 578 972us がサブimportに掛かっています。

遅い部分を見つける

先程の出力から、時間を使っている部分木を探しましょう。幾つかの行を抜粋します。

import time: self [us] | cumulative | imported package
...
import time:     86500 |     179327 | pkg_resources
...
import time:       385 |     236655 |             IPython
import time:        22 |     236677 |           IPython.core
import time:        26 |     236703 |         IPython.core.magic
import time:       978 |     237680 |       dotenv.ipython
import time:       199 |     239032 |     dotenv
...
...
import time:      3246 |     578972 |   pipenv.cli
import time:       507 |     579479 | pipenv

pkg_resources

最初に見つけたのが pkg_resources です。注目してほしいのは、このモジュールがインデントされていないことです。つまり、 pipenv モジュールから間接的に import されているわけではないのです。

実際、 pkg_resourcespipenv スクリプト から import されていました。

$ cat local/py37/bin/pipenv
#!/home/inada-n/local/py37/bin/python3.7
# EASY-INSTALL-ENTRY-SCRIPT: 'pipenv==9.0.3','console_scripts','pipenv'
__requires__ = 'pipenv==9.0.3'
import re
import sys
from pkg_resources import load_entry_point

if __name__ == '__main__':
    sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
    sys.exit(
        load_entry_point('pipenv==9.0.3', 'console_scripts', 'pipenv')()
    )

pkg_resources の import が遅いのは既知の問題で、後方互換性を壊さずに修正するのが難しい問題です。

しかし、 pkg_resources の import は回避することができます。

$ local/py37/bin/pip install wheel
$ local/py37/bin/pip install -U --force-reinstall pipenv
$ time local/py37/bin/pipenv --version
pipenv, version 9.0.3

real    0m0.704s
user    0m0.653s
sys     0m0.052s

これで、 pipenv --version コマンドの実行時間はほとんど import pipenv の時間と等しくなりました。

wheel がインストールされている時、 pip はソースパッケージ (.tar.gz など) をダウンロードしてきても、一旦 wheel (.whl) をビルドして、 wheel からインストールします。 そして wheel からインストールする時とソースパッケージからインストールする時でスクリプトを生成する処理が異なり、 wheel からインストールした場合には pkg_resources が使われないのです。

$ cat local/py37/bin/pipenv
#!/home/inada-n/local/py37/bin/python3.7

# -*- coding: utf-8 -*-
import re
import sys

from pipenv import cli

if __name__ == '__main__':
    sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
    sys.exit(cli())

つまり、 wheel をインストールしておけば、ソースパッケージしか配布していないツールをインストールする時にかかる余計な時間(私の環境ではおおよそ100ms程度)を回避することができます。

IPython

次に見つけた遅い部分はここです。

import time:       385 |     236655 |             IPython
import time:        22 |     236677 |           IPython.core
import time:        26 |     236703 |         IPython.core.magic
import time:       978 |     237680 |       dotenv.ipython
import time:       199 |     239032 |     dotenv

pipenvdotenv を import して、 dotenvdotenv.ipython を、 dotenv.ipythonIPython を import しています。

これが、私の環境 (IPython がインストールされていた) で pipenv の起動が遅く感じた最大の理由のようです。

しかしなぜ IPython が import されるのでしょう? dotenv のソースを読んでみた所、どうやら IPython extension のためようです。

もちろん、 pipenv やその他の多くの dotenv のユースケースでは IPython extension は使いません。なので dotenv がオンデマンドで IPython を import するような プルリクエスト を送っておきました。

また、 pipenv は dotenv にパッチを当てた独自コピーを埋め込んで利用しているので、そこから dotenv.ipython を取り除く プルリクエスト も送っておきました。

まとめ

pipenv --version の実行時間を 800ms から 500ms に削減することができました。

$ time local/py37/bin/pipenv --version
pipenv, version 9.0.3

real    0m0.503s
user    0m0.463s
sys     0m0.040s

このように、Python 3.7 の import 時間プロファイルは、CLIアプリケーションの起動時間を高速化するためにとても便利です。


@methane

songofacandy at 16:29|この記事のURLComments(0)
2017年12月21日

Python 3.7 でテキストファイルのエンコーディングを初期化後に変更可能になります

はてなブックマークに登録

Python のテキストファイル (厳密には io.TextIOWrapper) はいままでコンストラクタでしかエンコーディングを指定することができませんでした。

特に標準入出力 (sys.stdin, sys.stdout, sys.stderr) のエンコーディングを設定するには環境変数 PYTHONIOENCODING を利用するしかなく、アプリケーションが設定ファイルなどに基づいてこれらのエンコーディングを変更するには sys.stdin を sys.stdin.fileno() や sys.stdin.buffer から作り直すなどのハック的な方法しか使えませんでした。

Python 3.7 からは TextIOWrapper に reconfigure() メソッド メソッドが追加され、一部のコンストラクタで設定するオプションを変更可能になります。 そして昨日このメソッドに encoding, errors, newline のサポートを追加しました。 (コミット)

これにより sys.stdin.reconfigure(encoding='EUC-JP') のようにしてエンコーディングを変更することができます。

Python 3.7 は PEP 538 と 540 によって UTF-8 を使う限りにおいてはほぼ不満ない状態になったと思いますが、これで UTF-8 以外が必要な場合についても「one obvious way」を提供できるようになったと思います。

注意点として、read側については、少しでも read していると変更できず、エラーになります。これは TextIOWrapper 内部で、下側(bufferd IO)からある程度のかたまりでバイト列を受け取り、それをバッファに置いてデコードして改行で区切って返しているので、とくに状態をもつエンコーディングのデコーダを使っている場合に「返したところまで」と「それ以降」のバイト単位での区切りをつけるのが難しいからです。

書き込み側については、単に flush() してから新しいエンコーディングのエンコーダを作り直せばいいので、途中からのエンコーディングの変更にも対応しています。


@methane

songofacandy at 17:40|この記事のURLComments(0)
2017年12月13日

最近のPython-dev(2017-12)

はてなブックマークに登録

バックナンバー: 9月号 | 8月号 | 6月号 | 5月号 | 4月号 | 3月号 | 2月号 | 1月号

@methane です。 ISUCON があってしばらく間が空いてしまいました。コミットやML上の議論も追えてないのですが、1月末にPython 3.7のbeta1 (=feature freeze)が予定されているために、Python 3.7 を目標にしている PEP たちがたくさんacceptされたので、それらを紹介しておきます。

PEP 540 UTF-8 mode

https://www.python.org/dev/peps/pep-0540/

PEP 538 (locale coercion) とセットで、私が BDFL-delegate (PEP を accept する責任者) になった PEP です。

この PEP は当初はかなりのボリュームが有ったのですが、すでに PEP 538 を accept したので、それを補完する機能として大幅にシンプル化しました。

PEP 538 では、起動時に locale が C であったときに、 LC_CTYPE を C.UTF-8 などへの変更を試みます。 また、C locale では標準入出力のエラーハンドラが surrogateescape になり、例えば stdin から読んだ文字列をそのまま stdout に書く場合などに非ASCII文字に対してもバイト透過な振る舞いをするのですが、それを C.UTF-8 などの coercion ターゲットとなる locale にも適用します。

PEP 540 も、 locale を変更しない以外は全く同じ振る舞いをするようになりました。具体的には次のとおりです。

  • stdin, stdout の encoding/error handler が UTF-8/surrogateescape になる
  • sys.getfilesystemencoding() と locale.getpreferredencoding() が UTF-8 を返す

PEP 538 と違って実際の locale は変更されないので、例えば readline で日本語入力はできないままですが、C locale 以外存在しないコンテナ等で Python を動かすときにデフォルトでUTF-8を使ってほしいというような用途にはこれで十分です。

PEP 563 Postponed Evaluation of Annotations

https://www.python.org/dev/peps/pep-0563/

from __future__ import annotations を書くことで、関数アノテーションが評価されず、ただの文字列になります。とはいえ、ソースを読むときに構文としてはチェックされるので、任意の文字列がかけるわけではありません。

これにより、アノテーションを書くことによる性能のオーバーヘッドを減らす効果があるのと、アノテーション部分の名前解決のための forward references が不要になって書くのが楽になるという効果があります。

この動作は Python 4 からデフォルトになる予定なので、 Python 3.7 に移行した人は早めにこの動作を有効にすることをおすすめします。

個人的には、実行時に評価されなくなることで、Python の構文を実行時には許されない形で利用したり、あるいはアノテーション部分でしか利用できない構文を追加するという進化への道が開けたという点でも期待しています。例えば現在 Union[int, str] と書いている部分を int or str あるいは int | str と書けるようにする提案ができるかもしれません。(前者は評価するとただの int になり、後者は評価すると | が処理できずに TypeError になる)

PEP 560 Core support for typing module and generic types

https://www.python.org/dev/peps/pep-0560/

いままで type hint は Python 3 で追加された関数アノテーション以外には特別な Python に対する機能追加を必要としないように設計されてきましたが、 typing がある程度の成功を収めて来ているので、そろそろ typing の問題を解決するために Python 自体に手を入れてもいれようというのがこの PEP です。

例えば、 typing.Listclass List(list, MutableSequence[T], extra=list): ... として宣言されています。 この MutableSequence[T] の部分ですが、親クラスになるためにクラスでないといけないという制限があります。そのために実際に親クラスになってしまうので実際にメソッドを提供していないクラスが大量にMROに入りメソッド呼び出し性能のオーバーヘッドが大きくなるという問題があります。また、 MutableSequence 自体もクラスなので、それに対して [T] と書けるようにするためにメタクラスが使われています。

このために現在の typing は大量のメタクラスハックを必要とし、実行時オーバーヘッドもかかり、 import typing も遅くなり、また他のメタクラスとの衝突解決の手間が発生するという欠点を背負っています。

これを解決するために、 Python に次の機能を追加します。

  • class 文の親クラスリスト部分に、 type オブジェクトではない __mro_entries__ メソッドを持つオブジェクトを書くことができる
  • __class_getitem__ メソッドを定義すると、メタクラスを使わなくても MyClass[int] のようにクラスに添え字を書くことができる

これらの機能は typing モジュール以外から使えないというわけではありませんが、 typing 以外の用途での利用は非推奨になっています。

とはいえ、 __class_getitem__ については、最近のメタクラスを使わなくてもクラスの振る舞いをカスタマイズできる流れに添っていて黒魔術感も比較的少なめなので、本当にクラスオブジェクトに添え字アクセスが必要な場面であれば、typing 以外で使っても良いんじゃないかな。

PEP 561 -- Distributing and Packaging Type Information

https://www.python.org/dev/peps/pep-0561/

Typing が本格的に使われていくためには、サードパーティーライブラリの型情報をどうやって配布・利用するかを決めなければなりません。ということでそれを決めたのがこの PEP です。

PEP 562 -- Module getattr and dir

https://www.python.org/dev/peps/pep-0562/

モジュールに __getattr__ 関数を定義して遅延ロードや利用時warningなどを実現する仕組みです。

また、遅延ロードされる名前を提供するために __dir__ を利用することもできます。

個人的には、 import asyncio で芋づる式に multiprocessing まで import されているのを、 concurrent.futures.ProcessPoolExecutor を遅延ロードすることで解消したいと思っています。

PEP 565 -- Show DeprecationWarning in main

https://www.python.org/dev/peps/pep-0565/

Python には廃止予定のAPIについて警告するための DeprecatedWarning がありますが、これはPython製アプリケーションのユーザーにとってはほぼ無意味で混乱させるものなので、現在はデフォルトで表示されないようになっています。

しかし、 Python 開発者でもこの警告を有効にしていない人が多いために、DeprecationWarning に気づかれないという悩ましい状況も発生しています。

PEP 565 はこのバランスを少しだけ調整する提案です。 __main__ モジュールにおいてだけ、 DeprecationWarning をデフォルトで表示するようにします。

__main__ モジュールとは、インタラクティブシェルや Python インタプリタに渡された実行ファイルのことです。そこで直接廃止予定のAPIが呼ばれたときだけ DeprecationWarning が表示されるようになります。

これにより、開発者がAPIの使い方を調べるなどの目的でインタラクティブシェルで廃止予定のAPIを実行したときに Warning に気づけるようになると期待できます。

とはいえ、これの効果は限定的なので、Python開発者は -Wdefault オプションを使うか、 PYTHONWARNINGS=default と環境変数を設定しておきましょう。

-X dev option

https://docs.python.org/dev/using/cmdline.html#id5

上で紹介した -Wdefaultオプションに加えて、 Python 拡張モジュール開発者向けのものも含めて、幾つかの開発者向けオプションをまとめて有効にするオプションとして -X dev オプションが追加されました。

また、 PYTHONDEVMODE=1 という環境変数でも dev mode を有効にできるようになります。

PEP 557 -- Data Classes

https://www.python.org/dev/peps/pep-0557/

ちょうど Qiita に紹介記事があったのでそちらを参照してください。

Python3.7の新機能 Data Classes

songofacandy at 12:04|この記事のURLComments(0)
2017年09月28日

最近のPython-dev(2017-09)

はてなブックマークに登録

バックナンバー: 8月号 | 6月号 | 5月号 | 4月号 | 3月号 | 2月号 | 1月号

今月は Sprint がありました。去年の Sprint はベータ版直前だったのでたくさんの実装が入りましたが、次の Python 3.7 のベータは来年のはじめなので、今回は実装よりも提案(PEP)が多めです。とても全部は紹介しきれない(そもそも一部を除いて議論を追えていない)ので、今月からは提案については受理されたものや受理間近のものだけ紹介していきます。

namedtuple 生成の高速化

bpo-28638: Optimize namedtuple() creation time by minimizing use of exec()

namedtuple という、タプルの要素に整数の添字ではなく属性名でアクセスできるようにするデータ構造があります。 例えば次のようにして使われます。

_CacheInfo = namedtuple("CacheInfo", ["hits", "misses", "maxsize", "currsize"])

これは動的にクラスを作るのですが、そのためにクラス宣言の Python コードを文字列置換で生成し、 eval() することでクラスオブジェクトを生成していました。このメタプログラミングの方式は、リフレクション的な機能を使ってクラスを生成するよりも、何をやっているのかが判りやすいというメリットがあります。

しかし、通常のPythonのコードはバイトコードにコンパイルされて pyc ファイルにキャッシュされるのに対し、この方式ではクラスを生成するコードは都度コンパイルされてしまいます。そのため、 namedtuple をたくさん使っているモジュールの読み込みは遅くなります。

そこで、より一般的な type() 関数を使う方式で namedtuple が書き直されました。とはいえ、生成されたクラスのインスタンス生成速度に影響する __new__ メソッドだけは、性能を落とさずに完全な後方互換性を維持できなかったので、1関数だけのごく短いコードに対するevalはまだ残っています。それでも、CPythonでもPyPyでも数倍速くなっているので、namedtupleをたくさん利用するライブラリをインポートする時間が短縮されます。

その他、複数の namedtuple で作ったクラス間で共有できる部分を共有するなどの最適化が盛り込まれ、(インスタンスではなくて)生成されたクラスオブジェクトのメモリ使用量も削減できています。

なお、従来は生成されたクラスに ._source という属性があり、 eval 対象になったクラス定義のソースコードが入っていたのですが、今回の改良でなくなりました。

OrderedDict のコンパクト化

去年私が dict をコンパクト、かつ挿入順を維持する実装をしたのですが、まだ collections モジュールの OrderedDict は dict と別にキーの双方向リンクリストを持っていて、 dict の倍のメモリを利用します。

OrderedDict の典型的な用途は単に json などで順序を維持したいというものですが、その用途なら dict を使ったほうが、メモリ使用量は半分になり、構築も列挙も高速です。しかし、 dict の順序は実装依存であり、CPythonとPyPyは順序を維持するものの、それに依存するのはお行儀が悪いです。

そこで、OrderedDictをdictの構造をそのまま利用するように書き換えて見ました。メリットとしては典型的な操作の性能があれこれ上がっているのと、メモリ使用量が1/2に、そしてソースコードも1000行以上削ることができました。

しかし、デメリットとして OrderedDict と dict が密結合する(今は分離されているソースコードを1つにマージしてしまう)ことと、 OrderedDict だけに存在する move_to_end() というメソッドの速度が数割落ちています。他にも、要素の移動や追加削除の平均計算量は O(1) のままだけれども、最悪計算量が O(n) になってしまうようなパターンが増えている可能性もあります。

この部分のエキスパートである Raymond Hettinger さんは特に大きな書き換えに厳しい人なので、説得するには実装の磨き上げとより詳しい検証が必要です。多分 ISUCON 後になるけれど、 Python 3.7 に間に合わせたい。

PEP 539 v3: A new C API for Thread-Local Storage in CPython

Yamamoto Masayuki さんが活動されていた、スレッドローカルストレージを利用するCレベルの新APIの提案が受理されました。

旧APIはかなり古くからあるのですが、TLS key として int 型を使っていて、LinuxやメジャーなUnix、Windowsでは問題ないもののPOSIXには準拠していませんでした。そのためCygwinなどで問題が有ったらしいです。

PEP 557: Data Classes

主にデータの入れ物となることを目的としたクラスをより手軽に作れるようにするためのAPIが提案されています。

@dataclass
class InventoryItem:
     name: str
     unit_price: float
     quantity_on_hand: int = 0

     def total_cost(self) -> float:
         return self.unit_price * self.quantity_on_hand

のように宣言すると、

   def __init__(self, name: str, unit_price: float, quantity_on_hand: int = 0) -> None:
       self.name = name
       self.unit_price = unit_price
       self.quantity_on_hand = quantity_on_hand
   def __repr__(self):
       return f'InventoryItem(name={self.name!r},unit_price={self.unit_price!r},quantity_on_hand={self.quantity_on_hand!r})'

のようなメソッドが自動で生成されます。

namedtuple と目的が被っているものの、 namedtuple はタプルであるために一部の用途には向かない事があります。例えば immutable だとか、普通のタプルと比較可能であるなどです。

Data Class はタプルではなく、そのためより柔軟です。同一性でなく同値性による比較可能にするかどうかなどが選べます。構文も Python 3.6 からの新しいスタイルを活用したもので、例えば namedtuple だと独自のメソッドを追加するにはもう1段継承したクラスを作るなどの面倒があったのですが、こちらは上の例の total_cost() メソッドのように普通に書くことができます。

このPEPは受理一歩手前で、あとはもう名前を決めるだけです。個人的には Record がいいな。

PEP 552 -- Deterministic pycs

pyc ファイルには py ファイルのタイムスタンプが格納されていて、 py ファイルが更新されると自動的に pyc ファイルも作り直されるようになっています。

このためにpyファイルが同じでもpycはバイナリレベルでは一致しません。これが最近の ビルド再現性(Reproducible Builds) というプラクティスには都合がよくありません。 (Debian/Ubuntu では pyc はインストール後にビルドされますが、 pyc を一緒にパッケージに含めているディストリビューションもあります)

また、Bazelというビルドシステムでもこれは都合が悪いらしいです。

この PEP ではヘッダーの形式を追加し、従来通りのタイムスタンプ+自動ビルド方式の他に、pyファイルのハッシュ値を持つ事ができるようになります。この場合、 pyc を読み込むときに py ファイルのハッシュ値を計算するコストが気になるところですが、新しいヘッダはpy ファイルのハッシュをチェックして自動リビルドするか無条件に pyc を使うかを選択するフラグを持っています。

pyファイルをチェックしない方を選択した場合も、 import 時にチェックしないだけで、コマンドを使って明示的にチェックしたりリビルドすることはできます。なので root しか書き込めないディレクトリに Python やライブラリをインストールする場合など、ユーザーがうっかり py ファイルを変更する危険が無い場合は問題ないでしょう。

このPEPはAccept直前(他に意見がなかったらAcceptするよとGuidoが宣言中)です。なお、このPEPはファイルフォーマットレベルでの問題を修正するだけで、それ以外にも pyc ファイルが一定にならない実装上の理由は幾つかあります。しかしこのPEPがAcceptされたということは、 deterministic pyc をサポートするという方向性が決まったことでもあるので、実装面の課題も今後修正されていくと思います。

起動高速化

インタプリタの起動高速化は難しい問題なのですが、実際のアプリケーションの起動ではその何十、何百倍の時間が、ライブラリのロードに消費されています。

あるモジュールを import しても、そのモジュールの全部の機能を使うわけではありません。そのモジュールの中で、あまり使用頻度が高くないと思われる関数でだけ使われる依存関係は、モジュールの先頭ではなくその関数で import することで、アプリケーションの起動時間を減らすことができるかもしれません。

一方で、それは PEP 8 違反(明確な理由やメリットがあったら違反しても良いです)ですし、その関数の実行時間は若干遅くなってしまいます。だから、利用頻度が少なく、かつその利用頻度に比べて import が重いモジュールに絞って書き換えが進んでいます。

argparse が直接、間接的に import しているモジュールが減らされました。(マージ済み) https://github.com/python/cpython/pull/1269

functools の singledispatch でしか使われないモジュールを singledispatch 内に移動します。 (accept待ち) https://github.com/python/cpython/pull/3757

uuid モジュールが、 uuid1 のために libuuid や UuidCreateSequential をロードする処理が重いのでそれを遅延する提案。(これからレビューします)

https://github.com/python/cpython/pull/3684

また、重い import を見つけるために私がローカルで使っていた import に掛かる時間を表示するパッチを、だれでも(特にライブラリやフレームワークの作者が)簡単に使えるように -X importtime オプションとして提案しています。今はその出力フォーマットについて自転車置き場の議論をしています。 https://github.com/python/cpython/pull/3490

余談ですが、昔幾つかのサードパーティーライブラリの import 時間を調べて居た所、 Jinja2 の import がすごく遅いことに気づきました。

実は今年のはじめにメモリ使用量を解析していたところ、 Jinja2 は Python 3 ではユニコードのシンボルも(テンプレートエンジン内の変数名として)使えるようにしようと、シンボル名として有効な名前を表現する正規表現をかなり強引な方法で作っていたのを見つけて報告していたのですが、その正規表現のコンパイルが import 時に実行されていて遅かったのです。

すでに Jinja2 の開発ブランチではずっと良い実装に切り替わっているので、次の Jinja2 のリリースを楽しみにしています。

songofacandy at 18:52|この記事のURLComments(0)
2017年09月01日

最近のPython-dev(2017-08)

はてなブックマークに登録

バックナンバー:

https://docs.python.org/ja/3/

docs.python.org に言語スイッチのドロップダウンリストが追加されました。docs.python.org は Fastly を使っているので、 docs.python.jp よりも高速に閲覧できると思います。

docs.python.jp にあるセクション単位での英語ドキュメントへのリンク機能などがまだなくて単純な翻訳でしか無いので、すぐには docs.python.jp を止めるつもりはありませんが、将来的には docs.python.jp は docs.python.org/ja/ にリダイレクトすることを考えています。

PEP 550: Execution Context

Flask などのフレームワークではスレッドローカルストレージを利用して「コンテキスト」を作り、現在のユーザーの情報とかリクエストIDとかをそこに格納する事があります。しかし asyncio などのコルーチンではこれが使えません。

そこで、もっと高度に抽象化された、コンテキスト変数を扱うための仕組みが提案されています。

Hide implementation details in the C API

サードパーティーの拡張モジュールが利用している Python/C API が、 Stable ABI 以外は公開/非公開、マクロ/C関数が混ざり合っていて、 Cython なんかは非公開APIも積極的に使って性能を稼いだりしています。

現状のデメリットとして、 Stable ABI は使いにくいので余り使われていないのに対して、 API は雑然としていて他の Python 実装が Python/C API 互換の API を提供するハードルが高くなってしまっていたり、意図せずに実装詳細に依存したサードパーティーのライブラリが増えて将来の性能向上のための内部の大幅な書き換えをする場合に互換性の問題が起こる確率が増えてしまっています。

そこで、 Stable ABI と #include <Python.h> の間を埋める、整理された Python/C API を用意しようという提案がされています。

個人的には、これによって今までマクロだったAPIがC言語の関数になり、RustやGoなどC言語以外の言語から Python/C API を呼び出しやすくなることに期待しています。

PEP 539: A new C API for Thread-Local Storage in CPython

Python の TLS のための C API が pthread_key_t を(90年代から) int 型にキャストして使っているのですが、これは posix 準拠ではありません。メジャーな環境では問題になっていないのですが、 Cygwin や CloudABI で問題になっていたそうです。

しかし、ABIレベルの後方互換性の問題があり、簡単に変えることもできません。ということで新しいAPIが提案されていたのですが、最近 Yamamoto Masayuki (ma8ma)氏がこの PEP に最近取り組んでいてもうそろそろ解決しそうです。

Cレベルの話で Python からは見えないのですが、日本人の活躍情報でした。

songofacandy at 17:27|この記事のURLComments(0)
2017年06月29日

最近のPython-dev(2017-06)

はてなブックマークに登録

バックナンバー:

PEP 546 -- Backport ssl.MemoryBIO and ssl.SSLObject to Python 2.7

2014年に Python 2.7 をセキュアな状態に保つため、過去に PEP 466 でセキュリティ関連の Python 3.4 の機能が Python 2.7 にバックポートされました。これには ssl モジュールも含まれており、 Python 2.7.9 からは TLS のホストを自動で検証するようになったりシステムの証明書ストアを利用できるようになりました。

今回の PEP 546 は、さらに Python 3.5 で追加された ssl.MemoryBIO と ssl.SSLObject も Python 2.7 にバックポートするものです。これらの API を利用すると、 socket をラップするのではなく、メモリ上のデータに対して TLS の処理ができるようになります。 Windows で winsock2 ではなく IOCP を使ったり、その他非同期IOフレームワークを使うときに socket と TLS を分離できることは重要です。

Python 2.7 自体のメンテは 2020 年に終了しますが、 pip が Python 2.7 のサポートを終了する時期は未定です。実際に今までも、pip はユーザーがほとんどゼロになるまでかなり長い間古い Python をサポートしてきました。

一方で pip が http クライアントとして使っている requests は、将来内部で非同期IOベースにしていくことを検討しています。そして Python 2.7 のためだけに複数の実装をメンテナンスすることには否定的です。

PEP 546 により、 pip と requests が Python 2.7 のサポートを完全に切る前でも、(今年後半にリリースされる) 2.7.14 以降のみのサポートに限定するという形で requests の内部に非同期IO化を実現する余地ができます。

実際にこのシナリオ通りに進まない可能性もあったので、本当に必要なのかどうか議論が白熱しましたが、バックポートが提案されているコードのサイズはCとPythonを足してほんの数百行でしかありません。

最終的に Python 2.7 のリリースマネージャーの Benjamin Peterson さんが、 PEP 466 のときにこのAPIがあったら一緒にバックポートされてたんだし、反対する強い理由は無いよね、といって Accept しました。

PEP 538 warning at startup: please remove it

先月僕が Accept した PEP 538 (C locale のとき LC_COLLATE を C.UTF-8 などに上書きする) が実装され取り込まれました。

さて、この新機能は、 locale の変更に成功したときにはユーザーに最初からUTF-8のロケールを設定するよう促すための warning を、失敗したときは C locale を使ってるから非ASCII文字で問題が出るよという warning を表示していました。

しかし、この warning が単純に鬱陶しいのと、子プロセスを起動して標準入出力を見るようなテストが幾つかのマイナーなプラットフォームで壊れてしまってメンテが面倒になってきました。

ということで、どちらの warning もデフォルトで無効になりました。環境変数に PYTHONCOERCECLOCALE=warn と設定したら有効になるのですが、まぁ誰も使わないでしょうね。

LC_COLLATE を変更して UTF-8 を使えるようにしても問題になることはめったに無いですし、 C locale で非ASCII文字の扱いに制限があるのは Python 3.6 と同じで何かが悪くなったわけは無いので、 warning が無くなることで困るユーザーはほとんど居ないと踏んでいます。

PEP 544 -- Protocols: Structural subtyping (static duck typing)

Python の typing モジュールに structural subtyping のサポートが追加されました。

簡単に説明すると、 typing.Protocol クラスを継承したクラスを定義すると、それがプロトコルになります。プロトコルクラスのメソッドと同じシグネチャのメソッドを持つクラスは、そのプロトコルクラスを継承していなくても、静的チェッカはサブクラスと判断するようになります。

PEP から分かりやすい例を引用します。次の例は、ユーザー定義型 BucketSizedIterable だと型チェッカに教えるために継承を使っています。

from typing import Sized, Iterable, Iterator

class Bucket(Sized, Iterable[int]):
    ...
    def __len__(self) -> int: ...
    def __iter__(self) -> Iterator[int]: ...

PEP 544 が承認されると、 Sized や Iterable が Protocol になるので、この Bucket は継承を外しても SizedIterable[int] の部分型だと型チェッカが判断するようになります。

個人的には、 ABC を継承すると起動が遅くなる(クラスの生成が遅くなる)ので継承しなくても良くなるのは嬉しい一方、最初から typing が ABC を使わないでいてくれたらもっと軽量化できたのになーという気持ちもあります。

typing のオーバーヘッドについては先月の Language Summit でも話題になったらしく、MLなどでも議論されているので、これから最適化オプションで削るなどの改善が提案されるかもしれません。

起動高速化

3月号で紹介したのですが、Pythonの起動を高速化するためのプルリクエストを2件出していて、片方 は3月中にマージされ、もう片方 を一昨日マージしました。特に後者は macOS 上での性能向上が顕著で、クリーンインストールされた Python で起動時間が30msくらいだったのが20msくらいに減ります。

site という、 (Python の -S オプションを使わなかったときに) 自動で読み込まれるモジュールがあり、これが sys.path というライブラリを import するときに探す場所などを準備しています。 pip とか apt-get とかでインストールしたモジュールがすぐに使えるようになるのはこの site のおかげです。

この site が、いくつかのディレクトリの場所を決定するために sysconfig という Python の環境情報を集めたライブラリを読み込んでいるのですが、それがそこそこ重いです。そしてさらに macOS の場合、 Framework 内のディレクトリ名 ("Python" だったり "Python3.7" だったり) を調べるために sysconfig から _sysconfigdata という、 Makefilepyconfig.h から大量の変数をかき集めた dict を持つモジュールを利用していました。

Framework 名を sys._framework に入れて _sysconfigdata の必要性をなくし、さらに sysconfig から必要なごく一部分だけを site 内にコピーすることで、起動時間の短縮だけでなく、 mac の場合は多分起動直後のメモリ使用量も1MB程度減っています。

また、 site からのモジュールの import を減らすことによる最適化は、そのモジュールが functools みたいによく使われるものであれば結局アプリケーションの起動時間には寄与しない事も多いのですが、 sysconfig を使うのは site 以外には pipdistutils などのパッケージング関連のツールやライブラリだけなので、今回の最適化は多くのアプリケーションの起動時間・メモリ使用量にもそのまま寄与するはずです。

songofacandy at 22:29|この記事のURLComments(0)TrackBack(0)
2017年05月31日

最近のPython-dev (2017-05)

はてなブックマークに登録

バックナンバー:

PEP 545 -- Python Documentation Translations

3月号で紹介した、 Python 公式サイトで翻訳ドキュメントをホストする提案が受理されました。

PHP のドキュメントのように言語切替のUIが用意されて、より多くの人が母語に翻訳されたドキュメントにアクセスできるようにしていくことが目標です。

docs.python.jp で公開している日本語訳も、準備が整ったら docs.python.org に移管していくつもりです。

PEP 538 -- Coercing the legacy C locale to a UTF-8 based locale

1月号で紹介した C locale において UTF-8 を使うための提案である PEP 538 と PEP 540 ですが、私が BDFL delegate になり、先週 PEP 538 を受理しました。

BDFL とは言語仕様についての最終決定権を持つ Guido のことなのですが、実際にはすべてを Guido が決めているわけではなく、適切な人に委譲されることも多く、それを BDFL delegate と呼んでいます。

もともと PEP 538 と 540 には別の BDFL delegate がいたのですが、その方が時間が取れなくなったということで、 (PEP 538 の著者と PEP 540 の著者はふたりとも Red Hat の人なので) Red Hat と無関係な、議論に参加していた、非ASCII文字を日常的に使っているコア開発者として白羽の矢が立ちました。

PEP 540 の著者はしばらく別のことに忙しくされているのですが、 PEP 538 の著者の Nick Coghlan さんは活発に PEP を更新されているので、私も頑張ってレビューして、先週末に受理しました。

簡単に PEP 538 の動作を説明すると、 LC_CTYPE が C のとき、環境変数 LC_CTYPE に C.UTF-8, C.utf8, UTF-8 の3つを順に設定していき、最初に利用可能だった設定をそのまま使うというものです。成功すれば Python から利用する他のライブラリ (readline など) や環境変数を引き継ぐ子プロセスでもなるべく一貫して UTF-8 が利用できるようになります。

環境変数を上書きするとき警告されますし、上書きに失敗した場合も C locale を利用していることに対する警告が出力されます。警告を消したい人は、 locale -a コマンドで利用可能なロケールを調べて、 LANG か LC_CTYPE に C.UTF-8 や en_US.UTF-8 などのロケールを設定してください。 また、どうしてもどうしても C locale を使いたい、UTF-8を使いたくない人は、 LC_ALL=C を設定すれば、 C locale を利用できます。

古い Linux だと C.UTF-8 ロケールが利用できず、 LANG=en_US.UTF-8 や LANG=ja_JP.UTF-8 してしまうと LC_COLLATE なども影響を受けて sort コマンドなどの動作が変わってしまいますが、 LC_CTYPE はエンコーディングの指定に使われるカテゴリなので、 LC_CTYPE=ja_JP.UTF-8 などの用に設定しても man が日本語でマニュアルを表示したり sort が遅くなったり ls の順序が変わったりといった C locale ユーザーが嫌う副作用を伴わずに、 Python, readline, vim などで UTF-8 を利用できるようになります。

[Python-Dev] Is adding support for os.PathLike an enhancement or bugfix?

Python 3.6 で os.PathLike というインターフェイスが追加されて pathlib がずっと使いやすくなったのですが、標準ライブラリのAPIの中にはまだこれに対応していなくてパスを文字列で渡さないといけないものがいくらか残っています。

通常、 3.6.x ではバグ修正のみを行って機能追加は 3.7 になるのですが、 PathLike サポートの追加を 3.6.x で入れていいかどうかについて話題になりました。結論としては、ケースバイケースで x が小さい間はいくらか 3.7 からバックポートされることになります。

PEP 484 (static type hinting) の改良

PEP 484 update proposal: annotating decorated declarations

関数にデコレータを適用すると、シグネチャが変わる事があります。

# デコレート後の型は Callable[[str], ContextManager[DatabaseSession]]
@contextmanager
def session(url: str) -> Iterator[DatabaseSession]:
    s = DatabaseSession(url)
    try:
        yield s
    finally:
        s.close()

このとき、デコレータ自体に type hint が書かれていたら、静的チェッカーはデコレートされた後の型は導出できます。しかし、型を書くのはプログラムの静的検査のためだけではなく、人間へのドキュメントという側面もあります。

導出後の型を宣言するための方法を追加して、それに矛盾がないことを静的チェッカーが確認できるようにしようということで、そのための構文・APIが議論中です。

PEP 484 proposal: don't default to Optional if argument default is None

今の PEP 484 では、def handle_employee(e: Employee = None): のようにデフォルト値に None を使っている場合、 e の型は暗黙的に Optional[Employee] になります。 この省略記法が他と一貫性がないなどの理由でやめようということになりました。

タイプ数は増えてしまうのですが、今後は明示的に Optional[Employee] と書かなければなりません。

明示的な簡略記法は 改めて議論 されることになりそうです。

PyCon US

PyCon US が開催されました。 YouTubeのチャンネルでたくさんの動画を見ることができます。

私はリスニングがダメで英語の発表はあまり理解できないのですが、 YouTube で字幕をオンにすると字幕が間違う部分(主にその発表に関する特別な単語)と自分が聞き取れない部分があまりかぶらないので、リスニングの訓練をしながら発表を見ることができるのでオススメです。

また、 Language Summit という、コア開発者を中心に招待された人だけが集まるイベントも PyCon の中で行われました。 このイベントの要約は LWN.net で閲覧することができます。(6/2まではサブスクリプションが必要)

例えば、先月号で Python 2 の命日について「最後の Python 2.7 をリリースした日が EOL」と書いたばかりなのですが、象徴というか目標となる日付として 2020年の PyCon US ということになりました。

また、 CPython を改造して GIL を取り除く実験的なプロジェクト "Gilectomy" についての現状報告も行われました。

songofacandy at 21:12|この記事のURLComments(0)TrackBack(0)
2017年04月17日

最近のPython-dev(2017-04)

はてなブックマークに登録

バックナンバー:

NEWS (changelog) の作り方

Mercurial時代からNEWSファイル (changelog) の扱いは面倒だったのですが、Githubに移行したことでよりコンフリクトが起こりやすくなり面倒さに拍車がかかりました。 また、コンフリクトせずに間違った状態でマージされるというかなり致命的な事故も起こってしまっています。 (ワークフローが cherry-pick になったためにマージ時に履歴が考慮されなくなったのか、それともMercurialよりもGitの方がマージがバカなのか、詳細は把握してません。)

それで、1つの大きなNEWSファイルにエントリを追記していく代わりに、1つのエントリだけを含む小さいファイルを追加していき、ツールでそれらのファイルからNEWSファイルを生成する仕組みへの移行が急務となり、ツールの選定のためにコンペが行われました。

Generate Misc/NEWS from individual files

コンペに参加したのは reno, towncrier, blurb の3つです。

reno は OpenStack で使われているツールで、エントリファイルのフォーマットがYamlだったりとちょっとクセがあります。

towncrier も既存のツールで、ファイルの拡張子やディレクトリ名で必要なメタデータ (機能追加かバグフィックスかパフォーマンス向上かなど) を表現するのでファイルの中身はただのテキストになります。また、新規エントリを追加するのにコマンドを使わずたんにファイルを追加するだけで済むのも魅力です。NEWSをビルドするときに towncrier を必要とするものの、貢献者は towncrier を利用せずに済むからです。

blurb は Python のために新規に作られたツールで、 towncrier と reno の間くらいです。 最終的に、 Python の NEWS をより良くしていくためには独自のツールをメンテナンスする価値があるという判断で blurb が選ばれました。

PyCon US までに移行を完了する事を目標に、 NEWSエントリの順序をだいたいで良いので時系列順にソートするために reST のコメント形式でメタデータを入れるなど、細部を詰めているところです。

プルリクエストのブランチに push --force(-with-lease) をするのをやめよう

git で commit --amendrebase -i origin/master が好きな人いますよね。僕も好きです。でも、一人しか見ていないブランチはともかく、プルリクエストを出しているブランチで push -f が必要な操作をするときはよく考えましょう。

このプルリクエスト を出した人が、レビューで指摘された部分を修正したのにその後なかなかレビューしてもらえないとMLでレビューアを募ったのですが、最初にレビューした人が指摘事項がちゃんと直ってるのか Github でレビューするのが面倒だから後回しにしていた事をレスして、 push -f を非推奨にしようという話になりました。

スレッド: Regarding reviewing test cases written for tabnanny module

Github 移行前には Rietvelt を使っていたのですが、そこでは貢献者が新しいパッチをアップロードしたとき、レビューアはパッチ全体をレビューすることも、前のバージョンのパッチとの差分だけをレビューすることもできました。

Github もプルリクエストの diff 画面で "Changes from -- Show changes since your last review" を選択すれば、前回レビューしたコミットとプルリクエストの最新のコミットの差分をレビューすることができます。ですが今回のプルリクエストの著者は commit --amendrebase -i origin/master でコミットをまとめてしまったようで、その機能が使えなくなってしまいました。

python/cpython リポジトリではプルリクエストをマージするときに利用できる方法を "Squash and merge" だけに限定しています。なのでどれだけプルリクエスト内の履歴が汚くても、それはマージするときに消えるので気にする必要はありません。一方で push --force が必要な操作は、レビューアがレビューしていたコミットを消して余計な負担を増やす危険があります。(これは push --force-with-lease オプションでも同じです。)

この件は Python にかぎらず Github 上で運用されている OSS に貢献する時すべてに当てはまる話です。特に "Squash and merge" やコミット単位のレビューは去年登場したばかりの機能なので、(私を含め)昔から Github に慣れている人もそういった機能が使えなくなるデメリットにあまり気づいていないかもしれません。私も commit --amendrebase -i origin/master を気軽に使ってしまう方だったので気をつけます。

Python 2 の命日

Exact date of Python 2 EOL?

Python 2 の Python Core Developer によるメンテナンスは 2020 年で終了します。しかし、 2020 年の何月何日かは決められていません。

pythonclock.org というサイトで Python 2 の EOL カウントダウンがされていますが、とりあえず 4/1 と仮定しています。 Python の開発者向けのドキュメントでは 1/1 を仮 EOL date にしていました。

この日付についてオフィシャルの予定日を決めない?という話題が持ち上がったのですが、 Python 2.7 のリリースマネージャーの Benjamin Peterson さんが最後の Python 2.7 をリリースした日が EOL ということで特定の日付は決められませんでした。

PyCharmのデバッガが40倍高速に

PyCharm debugger became 40x faster on Python 3.6 thanks to PEP 523

PyCharm のこの Blog 記事  が ML で紹介されました。 PyCharm 2017.1 & Python 3.6 の環境でデバッガが最大30倍速くなったようです。

Python 標準のデバッガ (pdbとその低レイヤーのbdb) は、 Python が提供しているトレーシング機能を使っています。これは毎行コールバックが呼ばれてその中でブレークポイントかどうかの判定を行っているので、かなりオーバーヘッドが大きくなります。

さて、Python 3.6 の目立たない新機能として、外部から CPython にパッチを当てることなく JIT を追加できるようにすることを目的にして、フレームを評価する関数を置き換えられるようなAPIが追加されました。これは Microsoft の Pyjion という Python に JIT を追加するプロジェクトからの提案です。

PyCharm はその新機能を使い、ブレークポイントを設定するときにバイトコードを埋め込む方式のデバッガを開発することでこのスピードアップを実現したようです。

ただし、どうやら pdb が遅い一番の原因はパフォーマンスに大きく影響する部分が Python で書かれているからのようで、 trace ベースのまま大幅なスピードアップを実現している pdb-clone というプロジェクトもMLで紹介されていました。

さておき、この API を提案してくれた Microsoft と、そのAPIを利用した高速デバッガを実用レベルに持っていった JetBrains はグッジョブです。

Python 3.6.1 におけるバイナリ互換性問題

Python 3.6.1 でビルドした拡張モジュールが 3.6.0 で利用できないという互換性の問題が発生してしまいました。

What version is an extension module binary compatible with

元になったのは bpo-27867 で、 __index__ という特殊メソッドを悪用することでメモリ不安全なコードが書けるという問題です。 (Python は ctypes を使う場合などを除いて普通はメモリ安全になるようにという指針で開発されています。)

この問題を修正する過程で、 PySlice_GetIndicesEx() という公開APIの実装が _PySlice_Unpack(), _PySlice_AdjustIndices() という内部APIに分解された上で、 Py_LIMITED_API というマクロが定義されていないときは PySlice_GetIndicesEx() という同名のマクロでAPI関数を覆って、そのマクロが内部APIを直接呼ぶことで呼び出しのオーバーヘッドが削減される形になりました。

ところが、この Py_LIMITED_API というマクロは普通 Python 3.4 と 3.6 のようなマイナーバージョンが違う CPython で互換性のあるAPI, ABIを利用するために利用されるもので、多くのライブラリの拡張モジュールはこのマクロを使わずに 3.6.0 と 3.6.1 のようなマイクロバージョンでの互換性を期待しています。

Python 3.6.1 で PySlice_GetIndicesEx を使った拡張モジュールをビルドすると、 3.6.1 で追加された _PySlice_Unpack, _PySlice_AdjustIndices を参照してしまうので、 3.6.0 ではロードできなくなってしまいます。

binary wheel を配布しているライブラリの開発者は、当面の対策として、次のようにしてマクロをキャンセルすれば 3.6.0 でも 3.6.1 でも存在する関数を直接呼び出せるはずです。 (または 3.6.0 でビルドするという手もあります)

#ifdef PySlice_GetIndicesEx
#undef PySlice_GetIndicesEx
#endif

なお、まだ報告が1件だけということもあり、今のところ 3.6.2 の緊急リリース予定はありません。

Python 3.6 の set のメモリ利用量のリグレッション

Program uses twice as much memory in Python 3.6 than in Python 3.5

あるアプリケーションで Python 3.6 のメモリ使用量が 3.5 の2倍になるという報告がありました。 3.5 だと メモリが32GB のマシンで普通に終わる処理が、 3.6 だとスワップを使いだして数十倍時間がかかるというのです。

再現コードは OpenCL など僕が利用経験のない依存関係が多く、再現するのに32GBのRAMを乗せたマシンが必要で、1回の実行に30分以上かかり、しかもアプリケーションはモジュールを動的に利用する形になっていてタグジャンプやgrepでソースを追うのも面倒という感じだったので、ほぼ1日がかりで問題の箇所を特定しました。

ちなみに、今回まず大まかにアタリをつけるために使った方法は、そのプログラムの最初で別スレッドを起動し、そのスレッドで30秒に一回メモリ使用量 (今回は swap 起こすので getrusage(2) ではなく /proc/self/statusVmRSSVmSwap を参照しました) とスタックトレースを表示するというものです。これでアプリケーションの構成に対する予備知識がほぼゼロの状態でも、プログラムのどの部分を実行するときにどれくらいメモリ使用量が増えているのかを大まかに把握することができました。

結果として 、 set (または forzenset) のコンストラクタ引数に set (または frozenset) を渡したときに、その set のメモリ使用量が増えてしまっていることが分かりました。 bpo-29949

set のコードを整理するときにミスがあり、 set.add(x) で要素を追加するときに内部で起こるリサイズ(set.add() が繰り返し呼ばれる可能性があるので、ハッシュテーブルがかなり余裕をもった大きさにリサイズする)と同じ計算が set をコピーするときに利用されてしまい、メモリ使用量がほぼ倍になるというリグレッションでした。すでに 3.6 ブランチで修正済みなので 3.6.2 で直るはずです。

Python 3.6.1 (default, Mar 23 2017, 16:39:01)
[GCC 6.2.0 20161005] on linux
>>> import sys
>>> s = set(range(10))
>>> sys.getsizeof(s)
736
>>> t = set(s)
>>> sys.getsizeof(t)
1248  # 同じ set なのに約1.7倍!

脱線ですが豆知識を紹介しておきます。 dict が1エントリに key と value の2値を記憶するのに対して set は1エントリ1値なので、同じ要素数なら set の方が省メモリになりそうに思えますよね?実は Python 3.6 では(このリグレッションを抜きにしても)メモリ使用量は逆転しています。

>>> d = dict.fromkeys(range(10))
>>> sys.getsizeof(d)
368  # set(range(10)) に比べて半分!

これは次のような理由です。

  1. ハッシュ値をハッシュテーブルのエントリに格納しているので、 1エントリあたりの大きさの比は 2 : 1 ではなく 3 : 2
  2. しかも compact orderd dict が採用されて dict のメモリ使用量が約 2/3 になっているので、ハッシュテーブルの大きさが同じならメモリ使用量は同等
  3. さらに set の方が dict よりもよりハッシュテーブルをアグレッシブに大きくするのでメモリ使用量が逆転する

ハッシュテーブルのサイズに関して dict と set で方針が異なるのには理由があります。dict は名前空間として利用されるので検索がヒットすることが多いのに対し、 set は集合演算として利用されるのでミスヒットが多いケースの重要度が dict よりも高くなります。ハッシュテーブルからの検索がヒットする時は衝突チェインの平均長の半分が探索コストの期待値になりますが、ミスヒットするときは衝突チェインの長さが期待値になるので、ざっくりと言えば衝突のコストが倍になるのです。そのため、より衝突を減らそうと set はハッシュテーブルをアグレッシブに大きくしているのです。

ハッシュテーブルの密度が低いということは、compact ordered dict と同じ方式を採用すれば省メモリ化できる余地が大きいということでもあります。ですが、今回のリグレッションの件でリグレッション以外の部分についてもハッシュテーブルのサイズのバランスについてあれこれ意見を言って、set職人のRaymondさんに「俺にゆっくり考える時間をくれ」と釘をさされてしまったので、また機を改めて提案してみます。


@metahne

songofacandy at 19:53|この記事のURLComments(0)TrackBack(0)
2017年03月16日

最近の Python-dev (2017-03)

はてなブックマークに登録

バックナンバー:

Python 3.6.1rc1

Python 3.6.1rc1 がリリースされました。大きな問題がなければ 3.6.1 は 3/20 にリリースされる予定です

3.6.1 は Github に移行してから初めてのリリースになります。なにか問題がないか確認するために、いつものRC版以上にソース形式・バイナリ形式両方の配布物のテストが必要なので、可能な方は協力をお願いします。

Github 移行後日談

以降から1ヶ月が経ちました。開発者からのフィードバックはおおむね好評です。私は、気軽にプルリクエストをくれた人がCLAにサインしないまま放置して大量のマージできないプルリクエストが貯まることを懸念していたのですが、今までののところそれも大丈夫そうです。

一方で Misc/NEWS といういわゆる changelog にあたるファイルが頻繁にコンフリクトを起こしていて解決する新ツールが開発中だったり、プルリクエストをマージするときのコミットログのフォーマットなどが話し合いでルール決まったけどまだドキュメント化されてなくて Github のデフォルトのままマージされるケースが多いとか、細かい問題はまだまだ続いています。

また、Travis-CI でのテストをパスすることをマージボタンを押すのに必須の設定にしているのですが、それがよく遅延する問題がありました。簡単な修正のバックポートでいちいち待ってられない。既に Travis-CI に (幾つかの Python 関連 organization 共有で) 並列25jobを割り当ててもらっていてこれ以上は望めない。どれかテストを削ろう。という話し合いをしていたら、「あ、並列25jobの件、今日アクティベートされたから。」という書き込みがあり、実際その日以降 Travis-CI のテストは快適になりました。まだアクティベートされてなかったなんて誰も知らなかったよ…とはいえ Travis-CI さんには100万の感謝を。

CPython 3.7 is now faster than CPython 2.7 on most benchmarks

いろんなアプリを集めたベンチマークで Python 2.7 と 3.5 を比べた時は速くなってるのと遅くなってるのが半々くらいだったのですが、 Python 3.6 やそれ以降の高速化によってそろそろ Python 3 の方が速いと胸を張って言える感じになってきました。具体的には10%以上遅くなったベンチが12件に対して、10%以上速くなったベンチが23件あります。

ちなみに遅くなってるベンチのトップ2は Python コマンドの起動 (`-S` オプションありなしの2種類) です。例えば Python 2 では組み込みの open 関数は C 言語の FILE* をラップしたオブジェクトを返していて、完全に Python インタプリタ内で実装されていたのですが、 Python 3 では FILE* が使われなくなり代わりに io モジュールが利用されるようになったため、起動時に io モジュールとその依存モジュールがインポートされるようになっているのが遅くなってる原因の1つです。

起動が遅くなったと言っても数ミリ秒ですし、起動時にインポートされるモジュールは利用頻度が高いモジュールなので、 Python 本体の起動は遅くなっていても Python で書かれたアプリの起動時間には(結局アプリがそのモジュールをインポートする可能性が高いために)影響しないことが多いはずです。

とはいえ起動は1ミリ秒でも早いに越したことは無いですし、中二病な vim プラグインで有名な方が Python の起動が (特に Lua と比べて) 遅いと愚痴をこぼしているのを Twitter で見る機会もよくあったので、 メンテナンスコストとのバランスが取れた範囲での最適化 をしているところです。

PEP 543 -- A Unified TLS API for Python

Python には昔から ssl という標準ライブラリがあるのですが、これは OpenSSL を Python から使うためのものです。

最近は一層TLSの重要性が増しているのですが、 Windows などで OpenSSL をバンドルして配布してる環境で OpenSSL のセキュリティーアップデートを配布する必要が出てきてしまいます。 macOS でも、 Apple がシステムの OpenSSL を deprecate してしまってもうちゃんとメンテしてくれてないので、 Windows と同じ問題が発生しています。 (PyPI が CDN に fastly を使っている関係で、 macOS のシステムの OpenSSL が対応してない TLS v1.3 が必須になるという一大イベントも最近ありました。)

OpenSSL をやめて macOS では SecureTransport, Windows では SChannel を使えば、 OpenSSL に依存せずに、 Apple や Microsoft がメンテナンスしてくれている証明書ストアとTLS実装を使うことができます。また最近は OpenSSL 以外の実用的なTLS 実装も増えてきています。 OpenSSL に依存せずに簡単に TLS を使えるような仕組みがあると多くの人が幸せになれます。

今回提案された PEP 543 は、そのはじめの一歩として、TLSのライブラリが実装するべき標準のAPIを定義して、 ssl モジュールにその API を実装させるというものです。これができれば、同じAPIに準拠した SChannel をラップするなどの別実装モジュールを PyPI で配布し、TLSを利用するライブラリは ssl とサードパーティライブラリの間でスイッチしやすくなるはずです。

提案はされたものの、実際に実装・利用することなくこれをレビューして議論できる人がいないので、試験的に実装して問題点がないか洗い出す方向で進んでいるはずです。

多分これが Python 3.7 で一番重要な新機能になるんじゃないかな。

Translated Python documentation

Python のドキュメントの翻訳が一番進んでいるのが、僕も参加している日本語訳(古い Python の changelog とか拡張を書く人しか必要としない Python/C APIのドキュメントも全部含めて85%以上!)で、次に翻訳が進んでいるのがフランス語(翻訳率は10%台だけどチュートリアルはカバーされている)です。

そんなフランス語の翻訳をされている Julien Palard さんが、 docs.python.org/<lang>/ 配下で翻訳ドキュメントを公開できるようにしたいと 去年 python-ideas MLで提案していて、僕も翻訳の管理、自動ビルド、ホスティングなんかの下回りを共通化すれば他の言語でももっと翻訳がやりやすくなるだろうなと思って応援しています。

さて、 Python のドキュメントは Sphinx というツールで構築されており、翻訳も Sphinx が持つ機能を使っています。具体的には Sphinx が英文をパラグラフ毎に分けて gettext の POT 形式で出力したり、そのパラグラフに対応する翻訳文を mo 形式のファイルから読み込んで英文と差し替えることで、他の gettext を使ったOSSと同じような手順で翻訳ができるようになっています。

本家のドキュメントはもちろんこの機能を使わずに構築されているのですが、この機能を使うと問題が出る部分があり、日本語プロジェクトでは fork に修正を当てた状態で利用しています。幾つかの修正は日本語以外の言語の翻訳でも共通して必要なものです。ということで本家で修正するプルリクエストを作ったのですが、ドキュメント翻訳に否定的なメンバーから Reject されてしまいます。しかしその後 Victor Stinner さん(この1年ではダントツでトップのコミッター) が Accept してくれて、形式上2対1になりました。

一応マージボタンを押してもいい状況ではあるものの、別に急ぐ必要はないし様子見で放って置いたら、 Victor さんがさらに「翻訳を支援しようぜ」っていうメールを Python-Dev に投稿してくれ、多くの賛同のレスが得られました。 Victor さんマジイケメン。

そんなこんなで、問題のプルリクエストはマージしました。これからは Julien さん達と協力して、日仏以外の Python コミュニティの人も翻訳しやすい仕組みを作っていけたらなと思います。

ちなみに、現在 Python のドキュメント翻訳は Transifex というサイト (OSS向けの無料プランあり) で行っているのですが、今後のことを考えて Red Hat が作っている Zanata を試してみました。 Transifex が持っている重要な機能はほぼカバーしているし、 Transifex には無いバージョン機能(翻訳文のバージョン管理じゃなくて、同じプロジェクトの原文を複数バージョン持てる)があったり、サポートのレスポンスもすごく早くて良かったです。 また、 gettext 等の形式ではなく Web サイトを WYSIWYG で翻訳するなら、 Mozilla の Pontoon が良さそうでした。 何か OSS で翻訳プロジェクトを始めようとされている方は参考にしてください。


@methane
songofacandy at 18:40|この記事のURLComments(0)TrackBack(0)
2017年02月16日

最近の 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) #12345bpo-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

songofacandy at 10:46|この記事のURLComments(0)TrackBack(0)
2017年01月23日

最近の Python-dev (2017-01)

はてなブックマークに登録

@methane です。 compact dict が Python 3.6 が9月(ベータになる直前)にマージされ、それのおかげで推薦をもらい 10月ごろから Python の Core Developer になりました

「PythonのフルタイムコミッタとしてKLabに雇われている」という訳ではないのですが、 もともと自己裁量で業務時間の大半をOSSへの貢献やコードを読むことに費やし、特にこの3ヶ月位は Python ばかり触っていたので、実質的には近い状態です。

そちらでの活動をあまり日本で共有する機会がないので、 Money Forward の卜部さんが書かれている 最近の ruby-core という記事をリスペクトして、 最近の Python の開発状況を紹介する記事を書いてみたいと思います。

Python 3.6 リリース

12/23 に Python 3.6 がリリースされました。紹介しきれないほどたくさんの重要な改善がされていますが、 多分 (Python を普段使わない人にも) 一番わかり易いのは f-string でしょう。

f-string は文字列の中に式を書くことができる機能です。多くのLLが持っている機能なのですが、乱用すると簡単にメンテしにくいコードが書けてしまうので、 従来 "{foo.name} = {foo.value}".format(foo=foo) と書いていたのを f"{foo.name} = {foo.value}" とするように、 純粋に .format(name=name) の置き換えとして使い始めることをおすすめします。

私が主に貢献したのは compact dict と asyncio の高速化です。余談ですが、 Python 3.6 の2日後にリリースされた Ruby 2.4.0 にも compact dict とほぼ同一の新しいハッシュの実装が導入されました。偶然ですね。

(主に) C locale での UTF-8 サポート改善

Linux では Python はターミナル、標準入出力、ファイルパスなどのエンコーディングを locale を見て決定します。

ですが、 POSIX で C locale (POSIX locale とも言う) は ASCII を使うと決められているために、非 ASCII 文字を使うと UnicodeEncodeError を発生させてしまいます。標準入出力に関しては PYTHONIOENCODING という環境変数で制御できるのですが、 コマンドライン引数とファイルパスのエンコーディングは設定できませんでした。

C locale はデフォルトのロケールなので、 crontab に指定しなかったとき、 ssh が LANG=ja_JP.UTF-8 を送った接続先の サーバーが ja_JP.UTF-8 ロケールを持っていなかったときなどに意図せず使われます。翻訳されたエラーメッセージなんて (英語で報告しにくいから)見たくないとか、コマンドの挙動が変わるのが嫌だからという理由で意図的に C locale を使うエンジニアも多いでしょう。コンテナや組み込みなどの小さなLinux環境を作るときは容量削減のために C 以外のロケールが そもそも存在しないこともあります。

そういうさまざまな理由で C locale 上で Python を使うと、 UnicodeEncodeError が発生する事があり、特に Python 開発者ではない Python 製のツールを使ってるだけの人を困らせることになります。

Python は広いユーザー層を抱えているので locale の使われ方も様々なのですが、それでも本当に ASCII だけを使いたくて C locale を使っているユーザーはほとんどいないだろうということで、 C locale でデフォルトで UTF-8 を使うための提案がされています。

(まだ提案段階なので、 Python の開発ブランチをチェックアウトしても使えません)

PEP 540: Add a new UTF-8 mode

Python に UTF-8 mode を追加しようという提案です。 UTF-8 mode では locale が指定するエンコーディングを無視して、 ファイルパスと標準入出力が UTF-8 になります。

このモードは厳密に言えば disabled, enabled, strict の3つの状態があります。 enabled と strict の違いは、 UTF-8 でない バイト列を透過的に扱うための surrogate escape を使うか、 UTF-8 でない バイト列はエラーにするかです。

C locale ではデフォルトでこのモードが enabled になり、UTF-8でないファイルパスや標準入出力への読み書きが可能になります。 今時はめったに使わないかも知れないですが、外部のファイルシステムをマウントしたときとかに嬉しいはずです。

C 以外の locale では locale 指定のエンコーディングが strict で使われるので、 UTF-8 以外のデータはエラーを発生させます。 こちらは UTF-8 以外は除去したいときに、誤って文字化けしたデータを作るのを早めに気づいて止めたいときに便利です。

このモードは PYTHONUTF8 という環境変数や -X utf-8 というオプションで制御できるので、 locale を無視したい人は .bashrc などに export PYTHONUTF8=1 と書くか、 /etc/environmentPYTHONUTF8=1 と書いておくと良いでしょう。

PEP 538: Coercing the legacy C locale to C.UTF-8

こちらは、環境変数が指定しているのが C locale だったときに、 locale を C.UTF-8 に(システムが対応していれば)変更してしまおうという提案です。

これにより、 Python 本体だけでなく、別のライブラリも ASCII や latin1 ではなく UTF-8 で動作するようになることが期待されます。

例えば Python の REPL でも使われている readline が該当するので、 Android 上の Python の REPL で 何の設定をしなくても快適に UTF-8 を扱えるようになったという報告があります。

新しい呼び出し規約の拡大 (METH_FASTCALL)

従来、Cの世界から見た Python の関数呼び出しとは、順序引数をタプルで、キーワード引数を dict で渡すものでした。

Python 3.6 でタプルの代わりにただの配列の先頭ポインタ+順序引数の数を渡す呼び出す規約が登場し、呼び出し側が スタックに積んだ引数をタプルに詰めなくてもそのまま渡せるようになりました。

現在、C言語で作った「Pythonから呼べる関数」をこの新しい呼び出し規約に対応させる作業が進んでいて、重要な部分はあらかた対応が終わりました。

また、かなり内部の話になるのですが、 PyMethodDef (名前, 呼び出し規約を示すフラグ, 関数ポインタでなる構造体) で作成する関数やメソッド以外に、 オブジェクト自体が呼び出し可能 (Python に詳しい人なら、 operator.itemgetter() が返すオブジェクトといえば伝わるかも知れません) な型があり、そちらは型のメタデータとなる構造体に関数ポインタ tp_call が含まれています。

PyMethodDef の方は従来からフラグを使って複数の呼び出し規約に対応していたので、C言語から呼ぶときは専用のAPIを使っていました。 一方tp_call の方は今までタプルと dict を受け取る標準の呼び出し規約しかサポートしていなかったので、外部のライブラリがAPIを経由せずに関数ポインタを直接呼び出ししてしまっている可能性があります。

そこで互換性維持のために tp_fastcall という関数ポインタを新たに追加し、 tp_fastcall を用意している型には自動的に tp_call に変換するための関数を埋めるようにするパッチがレビュー中です。

細かいようですが、 Python の世界から見たら1つの呼び出しの引数が、C言語の世界では複数の関数の間で引き渡される事があるので、内部に新方式と従来方式が混ざっていると余計な変換が何度も発生する可能性があります。

Python 3.7 では内部での引数の渡し方が新方式に統一されて新しい呼び出し規約の実力が発揮できると思います。

Github へのマイグレーション

去年の1月にアナウンスされてから着々と準備が進んできていた、 Mercurial から Git、 hg.python.org から Github への移行ですが、着々と準備が進んでいるようです。

1/17 に Python 3.4.6 と 3.5.3 がリリースされ、 migration window と宣言されていた期間が始まったので、そろそろ実際に移行するスケジュールがアナウンスされると思います。1月中に移行できると良いなー。

songofacandy at 16:57|この記事のURLComments(0)TrackBack(0)
2016年08月23日

pixiv private isucon 2016 Python 版実装を用意しました

はてなブックマークに登録

今年の ISUCON でも Python 実装が提供されることが 発表されました。

Python での練習は過去の予選問題でも可能ですが、今年の出題チームが準備した問題で 練習できるように Python 版の実装を用意しました。とりあえずベンチマーカーが完走する ところまでは確認してあります。

リポジトリ

用意したのはアプリの実装だけなので、これを使って練習する際は pixiv さんが公開されている AMIwebapp ディレクトリ配下に python という名前で git clone し、 systemd などの設定は練習の一環として自前で行ってください。

また時間があるときに自分でチューニングしてみて、ミドルウェアの選定や ツール・テクニックなどを公開したいと思います。


@methane
songofacandy at 20:55|この記事のURLComments(0)TrackBack(0)
2016年06月29日

vmprof-flamegraph を作りました

はてなブックマークに登録

vmprof-flamegraph を作ったのでその紹介をしておきます。

まず、サンプルとして Sphinx を使って Python のドキュメントをビルドしたときの vmprof 結果を flamegraph にしてみたので、どんなものかはこちらを見てみてください。

http://dsas.blog.klab.org/img_up/sphinx-prof.svg

flamegraph について

flamegraph の一般的な紹介については省略して、リンクだけ置いておきます。

公式サイト: http://www.brendangregg.com/FlameGraphs/cpuflamegraphs.html

日本語の紹介記事: http://d.hatena.ne.jp/yohei-a/20150706/1436208007

他にプロファイル結果をビジュアライズするツールとしては cachegrind 系 (kcachegrind, wincachegrind など) がありますが、 flamegraph の方がスタックトレースで 遅い部分が分かりやすいので最近のお気に入りです。

たとえばWebアプリで、ORMのクエリオブジェクトから実際のクエリを生成してそれを実行して結果をインスタンス化する処理が重い事がわかったとします。 その場合、 cachegrind 系でもどこから呼び出されているのが重いのかまではだいたい分かります。しかし flamegraph を使った場合は、 ここから呼び出された場合はインスタンス生成が重い(大量にオブジェクトを取得しているなど)、こっちから呼び出された場合はクエリビルドが重い (単純にクエリの実行回数が多いなど)、までひと目で把握することができます。

他のメリットとして、GUIアプリが不要でブラウザで見ることができるとか、 flamegraph.pl に食わせるデータファイルのテキスト形式が grep などのツールと親和性が高い (興味がある関数で grep してから食わせることができる) などのメリットがあり、使いやすいです。

このテキストファイルのフォーマットはシンプルで、各行が次のようになっています。

トップレベルの関数名;1段深い関数名;一番深い関数名  サンプル数

関数名としているところは実際にはなんでも良く、サンプル数も整数であれば何でも良いです。なのでCPU時間以外にもメモリ使用量とかいろんな目的で利用することができます。

vmprof について

PyPy プロジェクトで開発されている、 PyPy と CPython 用のサンプリングプロファイラです。

サンプリングプロファイラなので、サンプル間隔を長めにすればオーバーヘッドを小さくすることができ、本番環境で動かすことも可能です。

まだまだ開発中のプロジェクトですが、サンプリングプロファイラ好きなので積極的に使っています。

vmprof はビジュアライザとして独自の Web アプリを開発していて、実際に http://vmprof.com/ で動いています。 特に PyPy のプロファイル結果を見る場合、 JIT のウォームアップなども見ることができます。

この Web アプリは https://github.com/vmprof/vmprof-server を見れば(ある程度のPythonの知識があれば)簡単に動かすことができます。

vmprof-flamegraph について

簡単に動かせると言っても、プロファイル結果を見るためだけに社内でWebアプリを運用するのも面倒です。 CPython で動かしているアプリのプロファイルを見るだけなら flamegraph を使うほうがお手軽です。

ということで、 vmprof が生成するバイナリ形式のプロファイル結果を、 flamegraph.pl の入力フォーマットのテキストファイルに変換するのが vmprof-flamegraph になります。

インストール方法は、 flamegraph.pl を PATH が通ったディレクトリに置いておき、 vmprof をインストールした Python の環境で pip install vmprof-flamegraph するだけです。

その Python 環境の bin ディレクトリに vmprof-flamegraph.py がインストールされます。

使い方は次のような感じになります。

$ python3 -m vmprof -o profile.data <Pythonスクリプト>
$ vmprof-flamegraph.py profile.data | flamegraph.pl > profile.svg
$ open profile.svg  # Mac の場合

詳細は vmprof-flamegraph や flamegraph.pl のヘルプを見てください。


@methane

songofacandy at 17:27|この記事のURLComments(0)TrackBack(0)
2016年06月24日

Python に現在実装中の Compact dict の紹介

はてなブックマークに登録

背景

2015年1月に、 PyPy の開発者Blogにこんな記事がポストされました。

Faster, more memory efficient and more ordered dictionaries on PyPy

その後リリースされた PyPy 2.5.1 から dict は挿入順を維持するようになり、メモリ使用量も削減されました。

一方 CPython では、 PEP 468 で、キーワード引数を **kwargs という形式の仮引数で受け取るときに、引数の順序を保存しようという提案がされました。

例えば、 SQLAlchemy のクエリーで .filter_by(name="methane", age=32) と書いたときに生成されるクエリーが WHERE name = "methane" AND age = 32 になるか WHERE age = 32 AND name="methane" になるか不定だったのが、ちゃんと順序を維持するようになるといったメリットがあります。

(filter_by は等式専用のショートカット関数であって、キーワード引数を使わない filter というメソッドを使えば順序も維持されます。)

この提案者は pure Python だった OrderedDict クラスをCで再実装して、 Python 3.5 から OrderedDict がより高速かつ省メモリになりました。 (dict の方の修正を避けたのは、それだけ dict が Python インタプリタのために複雑に最適化されているからです。)

しかし、Cで再実装されたとは言え、双方向リンクリストで順序を管理された OrderedDict はそれなりにオーバーヘッドがあります。特にメモリ使用量については、倍近い差があります。

Python 3.5.1 (default, Dec  7 2015, 17:23:22)
[GCC 4.2.1 Compatible Apple LLVM 7.0.0 (clang-700.1.76)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> d = {i:i for i in range(100)}
>>> from collections import OrderedDict
>>> od = OrderedDict((i,i) for i in range(100))
>>> sys.getsizeof(d), sys.getsizeof(od)
(6240, 11816)

そのせいもあり、まだ PEP 468 は止まったままになっています。

私も、一部のユースケースで便利だからといって、全てのキーワード引数の性能を落とす可能性がある変更には抵抗があり、また PyPy の実装した dict に興味があったので、 Python 3.6 にギリギリ間に合う今のうちに挑戦することにしました。

(予定では9月前半に beta リリースされ機能追加できなくなるので、それまでに実装、評価検証、MLでの議論などを経てマージされる必要があります)

データ構造

変更した構造体は PyDictKeysObject だけです。ただし、以前よりメモリレイアウトがより動的になります。

struct _dictkeysobject {
    Py_ssize_t dk_refcnt;
    Py_ssize_t dk_size;
    dict_lookup_func dk_lookup;
    Py_ssize_t dk_usable;
    Py_ssize_t dk_nentries;  /* How many entries is used. */
    char dk_indices[8];      /* dynamically sized. 8 is minimum. */
};

#define DK_SIZE(dk) ((dk)->dk_size)
#define DK_IXSIZE(dk) (DK_SIZE(dk) <= 0xff ? 1 : DK_SIZE(dk) <= 0xffff ? 2 : \
                       DK_SIZE(dk) <= 0xffffffff ? 4 : sizeof(Py_ssize_t))
#define DK_ENTRIES(dk) ((PyDictKeyEntry*)(&(dk)->dk_indices[DK_SIZE(dk) * \
                        DK_IXSIZE(dk)]))

dk_get_index(PyDictKeysObject *keys, Py_ssize_t i)
{
    Py_ssize_t s = DK_SIZE(keys);
    if (s <= 0xff) {
        return ((char*) &keys->dk_indices[0])[i];
    }
    else if (s <= 0xffff) {
        return ((PY_INT16_T*)&keys->dk_indices[0])[i];
    }
    else if (s <= 0xffffffff) {
        return ((PY_INT32_T*)&keys->dk_indices[0])[i];
    }
    else {
        return ((Py_ssize_t*)&keys->dk_indices[0])[i];
    }
}

dk_set_index(PyDictKeysObject *keys, Py_ssize_t i)
{
...

以前のハッシュテーブルは 3word (hash, key, value) の PyDictKeyEntry 構造体の配列でしたが、こちらの方式ではハッシュテーブルの要素をただの整数型にしています。 構造体の宣言では char dk_index[8] になっていますが、これは最小の dk_size が8の時の大きさで、実際にはアロケート時により大きいサイズを確保します。さらに、この整数型自体も dk_size が 128 までは char ですが 256 からは int16_t になります。このようにしてギリギリまでハッシュテーブルのサイズを小さくします。

さらに、 dk_indices のサイズが動的なので構造体に直接宣言できませんが、この構造体の後ろに PyDictKeyEntry 構造体の配列を置いています。この配列のサイズは dk_size ではなくその 2/3 (前回の記事で紹介した、このハッシュテーブルに挿入可能な最大要素数) にしています。新しい要素を挿入するときは、この配列に追記していき、そのインデックスを dk_indices に保存します。 dk_nentries は配列中の要素数になります。

挿入時の操作を、同じキーがまだ存在しないと仮定した擬似コードで示すとこうなります。

// dk_indices 内の挿入位置を検索
pos = lookup(keys, key, hash);

// エントリ配列にエントリを追記する
DK_ENTRIES(mp)[keys->dk_nentries].me_hash = hash;
DK_ENTRIES(mp)[keys->dk_nentries].me_key = key;
DK_ENTRIES(mp)[keys->dk_nentries].me_value = value;

// dk_indices にそのエントリのインデックスを保存
dk_set_index(keys, pos, keys->dk_nentries);

// 最後に使用済みエントリ数をインクリメント
mp->dk_nentries++;

削除

この dict からアイテムを削除するには、 dk_indices の該当位置にダミー要素を代入しておきます。 (各インデックスが1バイトで扱えるエントリー数が256までではなく128までなのは、マイナスの値をダミーと空を表すために利用しているからです。)

エントリーからの削除については2つの方式があります。

最初に compact dict のアイデアが Python の開発者MLに投稿されたときは、最後の要素を削除された要素があった位置に移動することで、エントリー配列を密に保っていました。この方式では最後の要素が前に来るので、「挿入順を保存する」という特性が要素を削除したときに失われます。

一方、 PyPy や今回僕が採用したのは、単に空いた場所に NULL を入れておくというものです。

// dk_indices 内の削除する要素のインデックスがある位置を検索
pos = lookup(keys, key, hash);
// 削除する要素のエントリー配列内の位置を取得する
index = dk_get_index(keys, pos);

// 要素を削除する
DK_ENTRIES(mp)[index].me_key = NULL;
DK_ENTRIES(mp)[index].me_value = NULL;

// dk_indices にダミーを登録
dk_set_index(keys, pos, DUMMY);

こちらの方式は、挿入と削除を繰り返したときにエントリー配列がダミーでいっぱいになってコンパクションを実行する必要があるというデメリットがあります。 しかし、実は最初に提案された方式でも、挿入と削除を繰り返すうちにハッシュテーブルがダミーで埋まってしまい検索ができなくなってしまう可能性があるので、どちらにしろコンパクションは必要になります。そのため、挿入順を維持する方が良いと判断しました。

ちなみに、 .popitem() は、エントリー配列のうち最後の要素を削除し、 dk_nentries をデクリメントすることで、平均計算量を O(1) に保っています。 この場合も dk_usable という「残り挿入可能数」をインクリメントしないので、削除と挿入を繰り返すとコンパクションを実行してハッシュテーブルを再構成します。

Shared-Key dict

さて、問題の shared key dict です。

最初は、 compact dict を実装する前と同じように、ハッシュテーブルにダミー要素を挿入せず、エントリー配列側が NULL になっていたらダミーと判断すれば良いと思っていました。

しかし、これでは shared key に最初に要素を追加した dict の挿入順しか保存することができません。

>>> class A:
...     pass
...
>>> a = A()
>>> b = A()
>>> a.a = 1
>>> a.b = 2
>>> b.b = 1
>>> b.a = 2
>>> a.__dict__.items()
dict_items([('a', 1), ('b', 2)])
>>> b.__dict__.items()  # 挿入順は b, a なのに、、、
dict_items([('a', 2), ('b', 1)])

この問題について、次の3つの方針を考えていますが、MLで議論した上でGuidoかGuidoが委任したコア開発者が最終決定するまでどれになるか分かりません。

(1) ありのままを受け入れる

今の Python の言語仕様では、 dict の順序は不定です。なので、「インスタンスの属性を管理する dict を除いて挿入順を保存する」という今の動作も、言語仕様的には問題ないことになります。

compact dict になると shared key dict も ma_values 配列のサイズが dk_keys からその 2/3 になってよりコンパクトになるので、その恩恵を完全に受ける事ができます。

一方、デメリットとしては、殆どのケースで挿入順を保存するように振る舞うので、言語仕様を確認しない人はそれが仕様だと誤解してしまうことがあります。 この問題は「誤解するほうが悪い」とするのは不親切です。 たとえば Go はこのデメリットを避けるために、 map をイテレートするときの順序を意図的に (高速な擬似乱数を使って) 不定にしています。

(2) 挿入順が違ったら shared key をやめる

shared key が持っている順序と違う順序で挿入されようとしたらすぐに shared key をやめるという方法があります。

一番無難な方法に見えますが、どれくらい shared key を維持できるのかわかりにくくてリソース消費が予測しにくくなるとか、稀に通るパスで挿入順が通常と違い、 shared key が解除されてしまうと、同じサイズの dict を同じくらい利用し続けてるのにメモリ使用量がじわじわ増えてくる、といった問題があります。

実行時間が長い Web アプリケーションなどのプログラムで、メモリ消費量が予測しづらく、じわじわ増えるのは、あまりうれしくありません。 なので私はこの方式に乗り気では無いです。

(3) Shared Key Dict をやめる

shared key dict は、ハマったときはとても効率がいいものの、 compact ordered dict の方が安定して効率がいいです。 しかも shared key dict をサポートするために、 dict の実装がだいぶ複雑になってしまっています。

実際に shared key dict を実装から削ってみた所、4100行中500行くらいを削除することができました。簡単に削除しただけなので、さらにリファクタリングして削れると思います。

一方効率は、 Python のドキュメントを Sphinx でビルドするときの maxrss を /usr/bin/time で計測した所、

  • shared: 176312k
  • compact + shared: 158104k
  • compact only: 166888k

という感じで、 shared key をやめても compact dict の効果によるメモリ削減の方が大きいという結果がでました。

(もちろんこれは1つのアプリケーションにおける結果でしか無いので、他に計測に適した、クラスやインスタンスをそこそこ使って実行時間とメモリ使用量が安定している現実のアプリケーションがあれば教えてください。)

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

OrderedDict

OrderedDict を compact dict を使って高速化する方法についても補足しておきます。

Python 3 には、 Python 2.7 には無かった move_to_end(key, last=True) というメソッドがあります。このキーワード引数がクセモノで、 move_to_end(key, last=False) とすると先頭に移動することができます。(機能はともかくメソッドの命名についてはとてもセンスが悪いと思います。 move_to_front(key) でええやん。)

この機能を実装するために、 dk_entries 配列をキャパシティ固定の動的配列ではなく、キャパシチィ固定の deque として扱うというアイデアを持っています。 つまり、今は dk_entries[0] から dk_entries[dk_nentries-1] までを使っているのですが、それに加えて先頭に要素を追加するときは dk_entries の後ろから先頭に向かって挿入していきます。

これを実現するには dk_nentries の反対版を作って、ハッシューテーブルの走査やリサイズがその部分を扱うように改造すれば良いです。 OrderedDict 1つあたり 1word (8byte) を追加するだけで、消費メモリを半減させることが可能なはずです。

ですが、Shared-Key 問題で手一杯なうえ、 dict が挿入順を保存するようになったら OrderedDict の利用機会も減ってしまうので、このアイデアを実装するモチベーションがありません。少なくとも Python 3.6 には(誰かが僕の代わりに実装してくれない限り)間に合わないでしょう。


@methane

songofacandy at 16:02|この記事のURLComments(0)TrackBack(0)
2016年06月22日

Python の dict の実装詳解

はてなブックマークに登録

@methane です。

最近 Python の dict をハックしているので、その紹介をしたいと思います。 ですが、まずこの記事では現在 (Python 3.6a2) の dict の実装を詳解します。

データ構造

基本となる構造体は3つです。(Python 3.6a2 のソースより引用)

typedef struct _dictkeysobject PyDictKeysObject;

typedef struct {
    PyObject_HEAD
    Py_ssize_t ma_used;
    PyDictKeysObject *ma_keys;
    PyObject **ma_values;
} PyDictObject;

typedef struct {
    /* Cached hash code of me_key. */
    Py_hash_t me_hash;
    PyObject *me_key;
    PyObject *me_value; /* This field is only meaningful for combined tables */
} PyDictKeyEntry;

typedef PyDictKeyEntry *(*dict_lookup_func)
(PyDictObject *mp, PyObject *key, Py_hash_t hash, PyObject ***value_addr);

struct _dictkeysobject {
    Py_ssize_t dk_refcnt;
    Py_ssize_t dk_size;
    dict_lookup_func dk_lookup;
    Py_ssize_t dk_usable;
    PyDictKeyEntry dk_entries[1];
};

PyDictObject が dict のインスタンスを表すオブジェクトです。 PyObject_HEAD は Python のオブジェクトすべてが持っている共通の管理用データ(参照カウント等)を定義するマクロで、通常ビルドでは3ワード(64bit環境では24バイト)になります。

ma_used がその dict に入っている要素数、 ma_keys が動的に確保されるハッシュテーブルです。 ma_values については後述します。

PyDictKeyEntry はハッシュテーブルの各エントリとなる構造体です。 me_key, me_value が名前のままキーと対応する値となる Python オブジェクトへのポインタで、 me_hash は me_key のハッシュ値です。探索やハッシュテーブルの再構築のときに必要になるので毎回計算するのではなくてエントリーに持ってしまっています。

PyDictKeysObject がハッシュテーブルです。 dk_size がハッシュテーブルのサイズ、 dk_usable があと何要素ハッシュテーブルに入れられるかを示すカウンタです。 dk_usable が 0 のときに挿入しようとするとハッシュテーブルが再構築され、必要に応じて拡張されます。 dk_refcnt については ma_values とともに後で解説します。

dk_lookup は探索関数への関数ポインタです。 dict は例えば関数呼び出しのときに名前から関数を探索したりと、主に文字列をキーとして Python の言語の基本を支えているので、 key の型が文字列の dict に特殊化して高速にした関数を最初に入れておき、文字列以外のキーを挿入した段階で汎用の関数へのポインタに差し替えるなどを行っています。

dk_entries はハッシュテーブルの実態です。構造体の宣言では1要素の配列になっていますが、実際には次のように PyDictKeysEntry をアロケートすることで dk_size の長さを持つ配列になります。

    dk = PyObject_MALLOC(sizeof(PyDictKeysObject) +
                      sizeof(PyDictKeyEntry) * (size-1));

オープンアドレス法

ハッシュテーブルのサイズ (dk_size) は 2^n になっています。また、そのハッシュテーブルに格納できる要素数 (dk_usable の初期値) は (2*dk_size+1)/3 になっています。

dk_size が 2^n なので、ハッシュテーブルに要素を挿入したり、探索する場合は、ハッシュ値 & (dk_size-1) で場所を決めます。 (dk_size が 8 なら2進数で表すと 0b1000 で、 dk_size-1 が 7 = 0b0111 になり、ハッシュ値の下位3bitを利用することになる)

dk_usable が 0 になりハッシュテーブルを再構築する際、(削除操作がなく純粋に追加してきた場合は)dk_sizeが2倍になり、ハッシュテーブルの「密度」は 2/3 から 1/3 になります。なのでハッシュテーブルの密度は下限がだいたい 1/3 、上限が 2/3 になります。

さて、ハッシュ値が綺麗に分散しているとしたら、挿入時に利用されるハッシュ値の下位ビットの衝突率は 1/3 と 2/3 の平均で 1/2 程度になるでしょう。 実際にはハッシュ値は綺麗に分散しているとは限らないので、これより悪くなるはずです。

ハッシュ法で衝突に対処する方法としては、要素をハッシュテーブル外のリストなどに置いてハッシュテーブルにはそこへの参照を格納する方法(オープンハッシュ法)と、ハッシュテーブル内の別の場所に格納する方法(クローズドハッシュ法、またはオープンアドレス法。「オープン」が全く逆のオープンハッシュ法と被るので紛らわしいです)がありますが、 Python はクローズドハッシュ法を採用しています。

クローズドハッシュ法では、「衝突したら次はどこを使うか」というアルゴリズムを決めておいて、挿入も探索も同じ方法を使わなければなりません。 メモリアクセスの効率を考えると衝突した場所の隣の場所を使うのが良いのですが、ハッシュ値が良くなくて衝突が頻発したときや、ハッシュテーブル内に偏りが発生した場合に、次も、その次も、その次も、という感じに衝突する可能性が高くなります。

ハッシュテーブル内を飛び飛びに巡回し、最終的にはテーブル内の全要素を1回ずつ走査する関数が欲しいのですが、その関数として Python は (5*i+1) mod dk_size を採用しています。 例えば dk_size が 8 で ハッシュ値の下位3bitが 3 なら、ハッシュテーブルを 3, 0, 1, 6, 7, 4, 5, 2, 3 という形で、 0~7 を1度ずつ通って最初の 3 に戻ってきます。

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

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

これで、最初のうちはハッシュ値の上位ビットを使って次の位置を決定していき、ハッシュ値を右シフト仕切ったら先ほどのアルゴリズムで確実に巡回する、というハイブリッド型の巡回アルゴリズムになります。

(ちなみに、このforループには最初の衝突のあとにくるので、 perturb の初期値が hash のままだと、hash 値の下位ビットが衝突する key と1つ目だけでなく2つ目も衝突してしまいますね…)

(なお、 dict よりも最近に改良された set 型では、最初はキャッシュ効率を重視して近くの要素を探すといった改良がされています。)

削除

オープンアドレス法での要素の削除には注意が必要です。というのは、探索や挿入のときの巡回は、同じキーか空きを発見したときに停止するからです。

あるキーAを挿入するときに別のキーBと衝突して別の場所に挿入したのに、その別のキーBが削除されて空きになってしまうと、キーAをまた探索するときに、キーBがあった場所に空きを見つけて、キーAはハッシュテーブルに存在しないと判断してしまいます。

これを避けるために、削除するときはハッシュテーブルを空きに戻すのではなく、ダミー要素に入れ替えます。

また、 popitem() という dict の中の任意の (key, value) を取得しつつ削除するメソッドの実装にも注意が必要です。 ハッシュテーブルを線形探索して、最初に見つけた要素を返し、その場所をダミーに置き換えるのですが、これを繰り返すとハッシュテーブルがダミー要素だらけになっていって、次のようなアルゴリズムの実行時間が O(n**2) になってしまいます。

while d:
    k, v = d.popitem()
    do_something(k, v)

popitem() の平均計算量を O(1) にするために、ハッシュテーブルの先頭に要素があればそれを返し、無かった場合は線形探索した後に、次に線形探索を開始する場所を(空、あるいはダミーkeyが入っている)先頭要素の me_value に格納しておきます。

PEP 412: Key-Sharing Dictionary

次のインタラクティブセッションでは、2つの空の dict のメモリ使用量を表示しています。

>>> d = {}
>>> class A: pass
...
>>> a = A()
>>> ad = a.__dict__  # a の属性を格納している dict のインスタンス
>>> d
{}
>>> ad
{}
>>> type(d), type(ad)
(<class 'dict' at 0x109c735b0>, <class 'dict' at 0x109c735b0>)
>>> import sys
>>> sys.getsizeof(d)
288
>>> sys.getsizeof(ad)
96

両方共確かに dict の空のインスタンスなのですが、サイズは 288 バイトと 96 バイトと、3倍近くの差があります。なぜでしょうか?

ここで、説明を省略していた PyDictKeysObject の dk_refcnt と PyDictObject の ma_values が登場します。

dk_refcnt は参照カウントになっていて、A のインスタンス a を作った際、 a の属性を管理する dict インスタンスを作った後、その PyDictKeysObject の dk_refcnt をインクリメントして、クラス A にポインタをもたせます。

その後、クラスAのインスタンスを作る際は、そのインスタンス用の dict を作るときに PyDictKeysObject を作るのではなく、クラスAが持っているオブジェクトを参照し、参照カウントをインクリメントします。このように、同じクラスの複数のインスタンス間で、 PyDictKeysObject は共有されます。

ハッシュテーブルを共有しているので、 me_value に各 dict が持つはずの値を格納することができません。 PyDictObject の ma_values に、 dk_size と同じ大きさの PyObject* の配列を持たせて、 dk_entries[i].me_value の代わりに ma_values[i] に値を格納します。

dk_entries は各要素がポインタ3つ分の大きさを持っている一方で、 ma_values はポインタ1つ分で済むので、これでかなりのサイズを削減できます。

また、 dk_usable の初期値が (dk_size*2)/3 ではなく (dk_size*2+1)/3 になっていたのにも秘密があります。

通常の dict では dk_size の最小値が 8 なのですが、 key sharing dict では dk_size が 4 になっているのです。 (4*2)/3 は 2 なのに対して、 (4*2+1)/3 は3になるので、この最小の key sharing dict は3つの要素を格納することができるのです。

この結果、属性が3つ以下のインスタンスは 64bit 環境では 96 バイトの dict で属性を管理することができるのです。

この key sharing dict は、 Python 3.4 で導入されました。 (ちなみに 3.4 ではバグでサブクラスで有効にならなかったのが、 3.4.1 で解消されました)

この素敵な新機能が、 dict をハックしていた僕を、単純に実装を複雑にしたり考えないといけないケースを増やすというだけでなく、かなり重い意味で苦しめることになります。

その話は次回の記事で解説します。


@methane

songofacandy at 17:01|この記事のURLComments(0)TrackBack(0)
2013年04月11日

グリーン破壊の破壊度をアップしました

はてなブックマークに登録

負荷試験ツール「グリーン破壊」を公開しました で紹介したグリーン破壊ですが、その後も KLab 内の案件で利用されつつ 地道に強化されています。 最近強化された点を2つ紹介します。

グリーン破壊(KLab/green-hakai)

分散攻撃

以前紹介した時のグリーン破壊は fork して複数プロセスから攻撃する方式だったのですが、 execnet というライブラリを用いて実際に攻撃を行う外部プロセスを作成するようにしました。

execnet は fork&exec だけでなく、 ssh 経由でリモートマシン上に外部プロセスを 作ることができます。グリーン破壊もこれを利用して分散攻撃ができるようになりました。

設定ファイル (YAML) に次のようなセクションを書くと、ローカルに2プロセス、 attacker ホスト上に 4 プロセスの合計6プロセスから攻撃を仕掛けることができます。

#破壊ノード
nodes:
    - host: localhost  # localhost の場合は ssh ではなくて popen します
      proc: 2  # この host で起動するプロセス数
    - host: ghakai@attacker   # ssh先
      proc: 4

注意点として、外部ホスト上で攻撃プロセスを立ち上げる時も、グリーン破壊を起動したマシンと同じ ディレクトリにある Python を利用するので、グリーン破壊をインストールしたディレクトリを rsync かなにかで同期しておく必要があります。

グリーン破壊を AWS で使う場合、1インスタンス当たりの性能は物理マシンほどでない ことが多いので、 Spot Instance と組み合わせて使うのがお勧めです。

名前解決

過去のグリーン破壊は普通に、名前を問い合わせて、結果を上から順番に利用するという形で名前解決をしていました。

しかし、 Keep-Alive が無効のサービスに対して負荷試験するときに名前解決がボトルネックになったり、 AWS の ELB を利用したプロジェクトに対して負荷試験するときにローカルのDNSキャッシュのせいで 攻撃先IPアドレスが分散しなかったりすることがありました。

そこで、最初に名前解決を実行し、解決結果を全て覚えておき、接続する時は上から順ではなくて ランダムに利用するという方式に変更しました。

ただし、長時間攻撃を継続する場合、 ELB がスケールアウトしてもグリーン破壊は追従しません。 もともと可能な限り Keep-Alive する上に、接続が切れても再接続時に名前解決をしないからです。 ELB 経由で負荷試験を行う場合は、一度グリーン破壊を実行してウォームアップして ELB をスケールアウトさせてから、グリーン破壊を再実行してください。

また、思ったように負荷がかからないでレスポンスタイムが低下するときに ELB の問題と アプリの問題を切り分けるとか、 ELB のウォームアップが面倒なときのため、負荷分散を 通さず直接攻撃する機能も搭載しました。

設定ファイルに domainaddresslist の両方を定義すると、接続先は addresslist からランダムに選ばれ、 HTTP の Host ヘッダには domain の内容が記述されます。

#攻撃先のドメイン. これ以外のドメインは攻撃しない.
# Host ヘッダにこのドメインが書かれる.
domain: "http://localhost:8889"

#接続先アドレスリスト. 省略した場合は domain を利用する.
addresslist:
    - 127.0.0.1:8889
    - localhost:8889

@methane
klab_gijutsu2 at 16:59|この記事のURLComments(0)TrackBack(0)
2012年03月29日

因縁の Google 独自言語対決! Go 1 vs PyPy

はてなブックマークに登録

エイプリルフールネタが思いつかないけど4/1は休日なんで問題ありません。 methane です。

とうとう ゴー のバージョン 1 がリリースされましたね。おめでとうございます。 まだRC2が出てからあまり時間が経ってない気がするのですが、パイソンにグーグル独自言語の称号を奪われそうになって慌てたのかもしれません。

前回のechoサーバー対決 から半年すこし経ったのですが、この間にどれくらい高速化したのか、早速ベンチマークをアップデートしました。

echoserver on github
Google Docs Spreadsheet

前回のスコアと比較してみると、 Go の r59 では

Throughput: 52087.16 [#/sec]
Throughput: 52070.02 [#/sec]
Throughput: 52068.27 [#/sec]
だったのが、 Go 1 では
Throughput: 55872.09 [#/sec]
Throughput: 55857.82 [#/sec]
Throughput: 55949.57 [#/sec]
と、順位に変動があるほどではありませんが、着実に速くなってます。一方、 PyPy 1.6では
Throughput: 79193.30 [#/sec]
Throughput: 81063.83 [#/sec]
Throughput: 81442.70 [#/sec]
だったのが、 PyPy 1.8 だと、、、
Throughput: 84852.55 [#/sec]
Throughput: 106760.88 [#/sec]
Throughput: 107032.43 [#/sec]

なんと、シングルスレッドのC++(epoll版)に迫る性能を叩き出すようになってます。

今回は Go と PyPy だけの更新ですが、夏までには軽量スレッド最速環境である Haskell の新しい Haskell Platform というディストリビューションが出ると思うので、それを待って全体的に更新したいと思います。


@methane
klab_gijutsu2 at 12:45|この記事のURLComments(0)TrackBack(0)
2011年12月21日

Ajax開発のテストツールとしてのPython

はてなブックマークに登録

KLab Advent Calendar 2011 「DSAS for Social を支える技術」 の15日目です。

最近はソーシャルゲームの開発案件も増えてきました。Android/iOSアプリの開発をはじめとしたAjax通信の事例が数多くなり、開発スタイルも様変わりしつつあります。以前はWebサイト・Webアプリ構築の場合はPCや携帯のブラウザをリロードしながらだったのが、スマートフォンが加わりつつあります。

スマートフォンにおいてはさらにアプリ開発が大きな意味を持ちつつあります。携帯時代にもJavaアプリやBREWアプリなど多様なアプリがありましたが、スマートフォンアプリでの違いは、サーバとの通信を非常に多く行うようになったことです。クライアントアプリは多くの場合自前かもしくはWebViewを用い、通信はAjaxが使われます。リクエストもGETだけでなくPOSTを使う事も多くあり、ブラウザだけでサーバ開発・デバッグを進めるのは厳しい事が多くあります。

そんなわけで今回、Ajax通信を行う簡単なテストツールがほしいと思って少々探してみたのですが、「Selenium」というブラウザベースのツール以外見当たらないようでした。私が欲しかったのは単に一回のAjaxリクエストとレスポンスのそれぞれ引数と返値を検査して、想定通りの処理がされてるかどうかを見たかったのですが、今回の用途にはオーバースペックでした。

最初はサーバの動作確認にはwgetやcurlといったコマンドラインツールを使用していました。しかし基本的な疎通確認やプログラムがエラーなく動作するかどうかくらいには使えても、Ajaxレスポンスが複雑になってくるにつれてレスポンスのJSONの値が本当に正しいかどうかの判定はできません。目視での確認も限界があります。クライアントアプリが送った認証要求を正しく判別してOK/NGを返すかどうかとか、ゲームロジックとして正しい値を返すかどうか、などなど。開発が進むにつれてAjaxサーバの行う処理も複雑化しますので、早めにダミークライアント、サーバテスト用のスクリプトが是非欲しいところです。

Pythonにはユニットテストモジュールunittestが標準ライブラリとして組み込まれています。さらにHTTP通信を行うurllib2やjsonモジュールも標準ですので、これらを組み合わせればAjaxサーバの開発を強力に援護することができます。

import urllib2
import json

import unittest

session = 'xxxxxxxx'
url = 'http://foo.bar/apps/?p={"session_id":"%s","action":"%s","params":"%s"}'

class TestAjaxProcess(unittest.TestCase):
    def test01(self):
        f = urllib2.urlopen(url % (session, 'hoge_action', 1)) 
        result = json.loads(f.read())
        print result

        # Ajaxレスポンスには { "result":"ok", "data":{"response_code":1, ... }} のような内容を想定
        self.assertEqual(result['result'], 'ok')
        self.assertEqual(result['data']['response_code'], 1)

    def test02(self):
        f = urllib2.urlopen(url % (session, 'hoge_action', 2)) 
        result = json.loads(f.read())
        print result

        # Ajaxレスポンスには { "result":"ng", "data":{"message": }} のような内容を想定
        self.assertEqual(result['result'], 'ng')
        self.assertEqual(result['data']['message'], '"params" invalid')

if __name__ == '__main__':
    unittest.main()

下は動作例です。

$ python tester.py
{ u'result':u'ok', u'data': {u'response_code": 1, u'value':100, u'extra_comment': u'hogehoge' }}
{ u'result':u'ng', u'data': {u'message': u'"params" invalid' }}
..
----------------------------------------------------------------------
Ran 2 tests in 0.160s

OK

標準ライブラリが充実したスクリプト言語であれば、このようにAjaxテストスクリプトは容易に組めます。素早い実装柔軟な変更を求められる開発現場において、こうしたAjaxテスト環境を揃えておくことは、今後さらに重要度が増す事でしょう。

klab_gijutsu2 at 12:00|この記事のURLComments(0)TrackBack(0)
2011年03月02日

ソーシャル携帯電波マップを作ろう2 〜そのログデータに用がある〜

はてなブックマークに登録

前回はiPhoneにデータ取得用の簡易アプリを作って送り込み、実測したところまででひとまず終わりとした。データは取ったからには使わなければ。そのログデータに用がある。まず既に作成した手元のログを確認してみたいわけですよ。

このPING結果を地図上に表示したい。もう一旦ファイル保存したものだし、PC上でGoogle Mapsを使用するのが最も簡単そうなのでそれで出すことにした。あとはこのログデータをどのように保持するかだが、Webアプリに模してAJAX的にクライアントに送られるようにしたいと考えている。本来ならばデータはDBにしまって、Webアプリによる処理を経た出力にしたいが、ひとまずJSONを静的ファイルにして対処するという段階的な実装で対応する。

そんなわけで、ログファイルの加工に入ろう。"LocationPinger[1785:307]"や、"latitude"、"longitude"といった冗長な語句をまずは除去する。一方でこのログには位置情報とPINGの2種類が入っているので、それぞれを区別しやすくする。そしてCSV形式に準じてプログラム上取り込みやすい形式にしよう。

% sed -e 's/ LocationPinger[[^]]*]/,/' 
          -e 's/latitude /latlng,/' 
          -e 's/longitude //' 
          -e 's/, #/,ping,/' 
          -e 's/ sent/, sent/' 
          -e 's/ received/, received/' 
          -e 's/ send/ , send/' 
          -e 's/, /,/g' < (log file)

CSVの2カラム目に、PING=pingと位置情報=laglngというそれぞれの区別を示すカラムを追加した。

また、第1カラムが時刻のままになっているが、これをデータの記録開始時点からの相対時間に置き換える。なぜそんなことをするかと言うと、位置情報の線形補間をやるためだ。

def at_location(t):
    i = 0
    while t > locations[i + 1][0] and i + 1 < len(locations):
        i += 1
    (t0, p0, q0) = locations[i]
    (t1, p1, q1) = locations[i + 1]

    dt = t1 - t0
    dp = p1 - p0
    dq = q1 - q0

    return (p0 + dp * (t - t0) /  dt, q0 + dq * (t - t0) / dt)

何しろ今回新幹線という高速移動しながらのデータ取得となるため、0コンマ何秒の間隔であってもかなりの距離を移動している計算になる。最も近いlatlngのデータで間に合わせてもいいが、すべてのlatlngデータにはタイムスタンプがあることを考えれば、そのタイムスタンプを入力にした関数 x = f(t), y = g(t) を定義すれば、任意の時点での位置をそこそこの手間で計算できることになる。それをやるのが(線形)補間だ。

import datetime
import sys

f = sys.stdin
dt0 = None

def __read_datetime(s):
    dt0 = s.split(' ')
    dt1 = dt0[1].split('.')
    fd = dt0[0].split('-')
    ft = dt1[0].split(':')

    da = [int(x) for x in fd + ft + [dt1[1] + '000']]
    dt = datetime.datetime(*da)
    return dt

for line in f.readlines():
    line = line.rstrip()
    fs = line.split(',', 2)
    dt = __read_datetime(fs[0])
    if not dt0:
        dt0 = dt
    d = dt - dt0
    print "%d,%s,%s" % (d.seconds * 1000 + d.microseconds / 1000, fs[1], fs[2])

ここまででログデータの整形は大分進んだ。さらにこれをプログラム内で扱う為のデータ構造に合わせて再編成する。最初のログ形式から1行分は単位として保持して来たが、PINGは送信と受信がそれぞれ紐づいているので、対応するログ同士をまとめて新たな一個のデータ単位となるようにしたい。

for line in sys.stdin.readlines():
    line = line.rstrip()

    fs = line.split(',')
    fs[0] = long(fs[0])

    if fs[1] == 'ping':
        d = None
        fs[2] = int(fs[2])
        if fs[2] in pings:
            d = pings[fs[2]]
        else:
            d = {}
            pings[fs[2]] = d
        if fs[3] in ('sent', 'received'):
            d[fs[3]] = fs[0]
    elif fs[1] == 'latlng':
        locations += [(fs[0], float(fs[2]), float(fs[3]))]

"ping"の行の第3カラムはPINGの送信シーケンスの番号を示している。"if fs[1] == 'ping':"の行以降で、同一のシーケンス番号を持つ"sent"と"received"の組を作り、一個の連想配列としてまとめている。まとめた連想配列は、さらにpings配列に逐次追加している。

さらに、位置情報のエントリも時間順にlocations配列に入れて、前述の線形補間のためのデータとして使った。PING送受信のそれぞれタイムスタンプを使用して各時点での位置情報を推定して取り込んだ。

for k in pings:
    p = pings[k]
    if 'sent' in p:
        p['sent'] = (p['sent'],) + at_location(p['sent'])
    if 'received' in p:
        p['received'] = (p['received'],) + at_location(p['received'])

あとは、これらを一個のJSON出力にして出来上がりである。

result = {"locations":locations, "events":list(pings[k] for k in sorted(pings.keys())) }

print json.dumps(result)
{"locations": [
  [0, 39.158541999999997, 141.18367799999999],
  [606, 39.149102999999997, 141.187062],
  [1641, 39.149863000000003, 141.18681799999999],
  [2629, 39.150553000000002, 141.186599],
  [4189, 39.151201, 141.18638999999999],
  [4668, 39.151829999999997, 141.18618599999999],
  ...
], "pings": [
  {"received": [734, 39.149196990338162, 141.18703182415459], "sent": [581, 39.149492397689762, 141.18692239603959]},
  {"received": [1689, 39.149896522267213, 141.18680736032388], "sent": [1584, 39.149821144927536, 141.18683143768115]},
  {"received": [2696, 39.150580830769236, 141.18659002371794], "sent": [2584, 39.150521572874496, 141.18660897469636]},
  {"received": [3674, 39.15098707692308, 141.18645899679487], "sent": [3584, 39.150949692307691, 141.18647105448719]},
  {"received": [4757, 39.151889238247861, 141.18616726816239], "sent": [4584, 39.151719695198324, 141.18622177453025]},
  {"received": [5766, 39.152556356000005, 141.18595579000001], "sent": [5584, 39.152439688034192, 141.18599320940172]},
  ...
]}

さあ、いよいよこれを使ってGoogle Maps上に表示してみようじゃないですか。

<html>
<head>
<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
<script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=false"></script>
<script type="text/javascript" src="jquery-1.5.1.min.js"></script>
<script type="text/javascript">

var map = null;
function initialize() {
  var y = (39.158542 + 39.701141) / 2;
  var x = (141.183678 + 141.136431) / 2;
  var latlng = new google.maps.LatLng(y, x);
  var myOptions = {
    zoom: 10,
    center: latlng,
    mapTypeId: google.maps.MapTypeId.ROADMAP
  };
  map = new google.maps.Map(document.getElementById("map_canvas"), myOptions);
}

$(document).ready(function() {
    initialize();

    $.ajax({
        url: 'pinglog.json',
        type: 'GET',
        dataType: 'json',
        success: function(data) {
            $.each(data['events'], function(i, ev) {
                var loc0 = null, loc1 = null, t0 = null, t1 = null, thickness = 1;
                if ('sent' in ev) {
                    loc0 = new google.maps.LatLng(ev['sent'][1], ev['sent'][2]);
                    t0 = ev['sent'][0];
                }
                if ('received' in ev) {
                    loc1 = new google.maps.LatLng(ev['received'][1], ev['received'][2]);
                    t1 = ev['received'][0];
                }

                if (loc0 != null && loc1 != null) {
                    var dt = t1 - t0;
                    if (dt <= 100) {
                        thickness = 20;
                    } else if (dt <= 200) {
                        thickness = 12;
                    } else if (dt <= 400) {
                        thickness = 7;
                    } else if (dt <= 1000) {
                        thickness = 4;
                    } else if (dt <= 2000) {
                        thickness = 2;
                    }

                    var path = new google.maps.Polyline({
                        path: [loc0, loc1],
                        strokeColor: "#FF0000",
                        strokeOpacity: 1.0,
                        strokeWeight: thickness 
                    });
                    path.setMap(map);
                    google.maps.event.addListener(path, 'click', function() {
                        alert('clicked')
                    });
                }
            });
        }
    });
});
</script>
</head>
<body>
  <div id="map_canvas"></div>
</body>
</html>

やりました!やりましたよ皆さん!

疎通が確認できた箇所ではPINGを送信した地点から受信した地点までの間に赤線を入れており、RTTの時間が短ければ短い程太い線にしている。これで見ると、水沢江刺駅から盛岡駅までの間、概ねどこでもSoftbank 3Gの電波を拾えていたことが分かる。しかし、ちょうど花巻市のあたりではっきり空白ができている。これは、新花巻駅南側にある花巻トンネルと高松トンネルという二つのトンネルのせいで電波が届かなくなったためであり、またこの両トンネル間の地上を走る間にはネットワークの到達性が復元できなかったことを意味している。おそらく、徒歩や車移動などで計測すれば異なる結果が出るだろうことが推測される。

また、他にもトンネルとは関係なく所々PING結果がプロットされていない箇所があることも分かると思う。航空写真と重ね合わせて見ると、人家が途切れ気味になったり森林地帯だったりしていて、基地局から遠かったのかとか局間のハンドオーバーに失敗したのだろうかとか、いろいろ推測してみるのも面白いだろう。ちなみに、水沢江刺から一関・仙台方面に向かうともっとトンネルが多いので、より変動の多い拾い甲斐のあるデータが取れることだろう。こんどやるときはもっと用意を進めてこちらでもデータが取れるようにしておきたいところ。

というわけで、ここまででデータの記録と出力はできるようになった。クライアントサイドの動作検証モデルが出来たことになる。残るはサーバへのデータ集積。ソーシャルって言うくらいなんですからさあ、皆さん、皆さんと僕らは、ずっと、ずっと、こうしてつながってたいわけですよおおお!!!…次回はいよいよそのへんの機能を実装します。どのへん?

(こうら ひろのぶ)

klab_gijutsu2 at 10:01|この記事のURLComments(0)TrackBack(0)
2010年08月31日

Pythonのプロファイル結果をGUIで解析する

はてなブックマークに登録

エキスパートPythonプログラミング
久しぶりのエキスパートPythonプログラミング補足記事です。今回はプロファイリングの補足です。

Pythonは標準ライブラリにプロファイラ(profile/cProfileモジュールのProfileクラス)を持っていて、 その結果を格納しているpstats.Statsクラスにはプロファイル結果を解析するのに必要な機能(実行時間でのソートや、呼び出し元/呼び出し先の表示など)が一通りそろっています。これらのクラスについてはエキスパートPythonプログラミングで使い方を解説してあります。

コマンドラインで作業するときにはIPythonからStatsオブジェクトを直接操作してプロファイル結果を調べられるのですが、やはりプロファイル結果はグラフィカルに表示してくれた方が見やすいものです。特に「エキスパートPythonプログラミング」の中でも言及されているKCacheGrindというGUIツールは非常に強力で、一度使うと手放せません。

KCacheGrindが対応しているcallgrind形式は、元々はValgrindのツールの一つであるCallgrindのものでしたが、 最近はプロファイル結果を保存するフォーマットとして広く用いられています。callgrind形式ファイルのブラウザとしてはKCacheGrind以外にも、Windowsで動くWinCacheGrind、PHP製でブラウザ上で表示するwebgrind 等がありますし、PHPのxdebugに含まれるプロファイラもcallgrind形式でプロファイル結果を保存します。

弊社でも日常的にcallgrind形式のプロファイル結果を共有していて、そのために社内のWebツールまで用意しています。(紹介記事: PHP Xdebug のProfileの手軽な共有ツールを作ったよ)

しかし、Statsクラスはcallgrind形式には対応しておらず、プロファイル結果を保存するときのファイルフォーマットは独自フォーマット(Statsの内部変数をmarshal形式で保存したもの)になっています。このままでは、Pythonのプロファイル結果をKCacheGrindで表示することができません。 そこで、Statsオブジェクトをcallgrind形式で保存するためにlsprofcalltree.pyを利用します。

lsprofcalltree.pyは、コマンドラインツールとして $ lsprofcalltree.py スクリプトファイル という使い方もできますし、アプリケーションに組み込んで使うこともできます。例えばBazaarでは $ bzr --lsprof-file callgrind.out statusのように実行するとプロファイル結果をcallgrind形式で出力するようになっています。

プロファイル結果を見やすくビジュアライズできる環境が整うと、アプリケーションのチューニングが楽しくなります。 KCacheGrind等のGUIツールとcallgrind形式のファイルを出力するプロファイラは、Pythonプログラマに限らずみんな常備しておくといいと思います。


@methane
klab_gijutsu2 at 21:21|この記事のURLComments(0)TrackBack(0)
2010年06月18日

Twisted vs Tornado vs Go で非同期Webサーバー対決

はてなブックマークに登録

昨日の takada-at の記事で「サーバー側では単純に100ms待ってからレスポンスを返すだけのページを用意しておき、」とあったのですが、今日はそのサーバー側の話をします。

もともとこのサーバーを作った動機は、takada-at が作成中の負荷試験システムがちゃんと並列に負荷をかけられるかどうかを検証するためでした。 すぐにレスポンスを返してしまうと、負荷試験スクリプトがきちんと並列に負荷をかけられなくても PV/sec が出てしまいます。 そこで、 epoll を使って高速に並列接続を扱えるTwistedフレームワークを使って、100msの遅延をしつつ数千PV/secに耐えるWebサーバーを作ってみました。 さらに、同じく epoll を使っている Tornado や Go にも興味があったので、こちらでも同じものを作成し、パフォーマンスを比較してみました。

コード

まずは、コードを見てみましょう。

Twisted Python:

from twisted.internet import epollreactor
epollreactor.install()

from twisted.web import server, resource
from twisted.internet import reactor

class Lazy(resource.Resource):
    isLeaf = True
    def __init__(self, delay=0.1):
        self._delay = delay

    def render_GET(self, request):
        def after():
            request.write('Hello, World!')
            request.finish()
        reactor.callLater(self._delay, after)
        return server.NOT_DONE_YET

if __name__ == '__main__':
    import sys
    delay = 0.1
    try:
        delay = float(sys.argv[1]) / 1000
    except Exception:
        pass
    site = server.Site(Lazy(delay))
    reactor.listenTCP(8000, site)
    reactor.run()

次は、Tornadoです。

from tornado import httpserver, ioloop, web

from time import time

DELAY = 0.1

class MainHandler(web.RequestHandler):
    @web.asynchronous
    def get(self):
        def _hello():
            self.write("Hello, World!")
            self.finish()
        ioloop.IOLoop.instance().add_timeout(time() + DELAY, _hello)

application = web.Application([
    (r"/", MainHandler),
])

if __name__ == "__main__":
    import sys
    try:
        DELAY = float(sys.argv[1]) / 1000
    except Exception:
        pass
    http_server = httpserver.HTTPServer(application)
    http_server.listen(8001)
    ioloop.IOLoop.instance().start()

最後に、Goです。

package main

import (
    "flag"
    "http"
    "io"
    "log"
    "time"
    )

var addr = flag.String("addr", ":9000", "http service address")
var delay = flag.Int64("delay", 100, "response delay[ms]")

func main() {
    flag.Int64Var(delay, "d", 100, "response delay[ms]")
    flag.Parse()
    http.Handle("/", http.HandlerFunc(hello))
    err := http.ListenAndServe(*addr, nil)
    if err != nil {
        log.Exit("ListenAndServe:", err)
    }
}

func hello(c *http.Conn, req *http.Request) {
    time.Sleep(int64(1000000) * int64(*delay))
    io.WriteString(c, "Hello, World!\n")
}

TwistedやTornadoは、ループに後で呼び出す関数を登録していていて明らかにイベントドリブンフレームワークという感じがします。 それに対してGoは Sleep() を使っていて、一見普通のスレッドべースの並列処理に見えます。 しかし、Goはネイティブスレッドではなくgoroutineという軽量なユーザーランドスレッドを利用しており、そのスケジューラは(Linuxの場合)epollを利用しています。通常のスレッドを使うよりも効率的に動作するはずです。

ベンチマーク

それでは、この3つにそれぞれ負荷をかけてみましょう。といっても、100msの遅延だと負荷をかけるのが大変なので、10msの遅延で実行し、ab -c100 -n10000で実験してみます。

Twisted:
Requests per second:    2620.31 [#/sec] (mean)
Time per request:       38.163 [ms] (mean)
Time per request:       0.382 [ms] (mean, across all concurrent requests)

Percentage of the requests served within a certain time (ms)
  50%     31
  66%     32
  75%     33
  80%     37
  90%     38
  95%     38
  98%     40
  99%     44
 100%   3082 (longest request)


Tornado:
Requests per second:    5299.04 [#/sec] (mean)
Time per request:       18.871 [ms] (mean)
Time per request:       0.189 [ms] (mean, across all concurrent requests)

Percentage of the requests served within a certain time (ms)
  50%     19
  66%     19
  75%     19
  80%     19
  90%     19
  95%     19
  98%     21
  99%     22
 100%     33 (longest request)


Go:
Requests per second:    4712.63 [#/sec] (mean)
Time per request:       21.220 [ms] (mean)
Time per request:       0.212 [ms] (mean, across all concurrent requests)

Percentage of the requests served within a certain time (ms)
  50%     19
  66%     20
  75%     20
  80%     21
  90%     32
  95%     40
  98%     42
  99%     42
 100%     48 (longest request)

TornadoやGoに比べると、Twistedが残念な結果になっています。 これは、Twistedが比較的汎用的で高機能なフレームワークということもありますが、遅延実行する関数をリストに格納していて、実行する関数があるかどうかをリストを全部チェックしているのが大きそうです。(Tornadoはソート済みリストを利用していてます)

Goも、コンパイラ型なのにTornadoに負けてるのが残念ですが、epollから直接イベントドリブンしているTornadoに比べると goroutine という抽象化レイヤを1枚はさんでいるので仕方ないかもしれません。

さらに高速化

まずはTornadoのJITによる高速化に挑戦してみます。PyPy-1.2が一番よさそうですが、セットアップが面倒なのでお手軽なpsycoを使ってみました。もとのソースコードに、次の2行を追加します。

import psyco
psyco.full()

結果はこうなりました。

Requests per second:    7503.16 [#/sec] (mean)
Time per request:       13.328 [ms] (mean)
Time per request:       0.133 [ms] (mean, across all concurrent requests)

Percentage of the requests served within a certain time (ms)
  50%     13
  66%     13
  75%     14
  80%     14
  90%     14
  95%     14
  98%     15
  99%     15
 100%     23 (longest request)

5299req/sec => 7503req/sec で、20%以上高速化できました。topで見ている限り、メモリ使用量(RES)も1MB以下です。Tornado+psycoは非常に優秀ですね。

次に、Goでマルチスレッド化を試してみます。goroutineは複数のネイティブスレッドを利用して並列に動作することができます。しかし、現時点ではまだスケジューラがよくないらしく、デフォルトではマルチスレッドを使わないようになっています。環境変数 GOMAXPROCS に並列数を指定して実験してみました。

GOMAXPROCS=6 の場合:
Requests per second:    5760.33 [#/sec] (mean)
Time per request:       17.360 [ms] (mean)
Time per request:       0.174 [ms] (mean, across all concurrent requests)

Percentage of the requests served within a certain time (ms)
  50%     16
  66%     17
  75%     17
  80%     18
  90%     28
  95%     33
  98%     37
  99%     40
 100%     53 (longest request)

実験マシンはCore2 Duoで2コアしか載っていない(Hyper Threadingもなし)なのですが、2を指定してもあまり性能が上がらず、そこから1つずつ数字を上げていったところ6までは少しずつ性能が向上していきました。しかも、topを見ている限りCPU使用率は最大の200%には全く届かず120%以下しか利用できていません。 CPUを原因は判りませんが、今回のようなイベントループをヘビーに使う用途にはスケジューラが最適化されておらず、もっとサーバー側でCPUを利用する計算を行う処理をしないとマルチコアを使いきれないのかもしれません。

Goは今回のケースではTornadoに負けてしまいましたが、IO多重化をイベントドリブンよりシンプルに記述でき、コンパイル型言語なので複雑な計算をPythonより圧倒的に高速に計算できそうなので、非常に期待しています。


@methane
klab_gijutsu2 at 18:37|この記事のURLComments(0)TrackBack(0)
2010年05月28日

Pythonの内包表記はなぜ速い?

はてなブックマークに登録

「エキスパートPythonプログラミング」の発売が、Amazonや一部の書店で始まりました。

エキスパートPythonプログラミングエキスパートPythonプログラミング
著者:Tarek Ziade
販売元:アスキー・メディアワークス
発売日:2010-05-28
クチコミを見る

今回は、「エキスパートPythonプログラミング」の2章から、リスト内包表記について補足します。 本書で、リスト内方表記が速い理由について、次のような訳注を書きました。

訳注:リストに要素を append() する場合、インタプリタは「リストから append 属性を取り出してそれを関数として呼び出す」という処理をしなければなりません。 それに対して、リスト内包表記を使うと、インタプリタに直接「リストに要素を追加する」という処理をさせることができます。インタプリタが解釈する命令数が減る、属性の取り出しが不要になる、関数呼び出しが不要になる、という3つの理由で、リスト内包表記を使うと速くなります。

今回はこの訳注の内容について、詳しく見ていきましょう。いきなりですが、内包表記を使う関数と使わない関数を用意して、実行速度とバイトコードを比較してみます。

In [1]: def sample_loop(n):
   ...:     L = []
   ...:     for i in xrange(n):
   ...:         L.append(i)
   ...:     return L
   ...:

In [2]: def sample_comprehension(n):
   ...:     return [i for i in xrange(n)]
   ...:

In [3]: %timeit sample_loop(10000)
1000 loops, best of 3: 1.03 ms per loop

In [4]: %timeit sample_comprehension(10000)
1000 loops, best of 3: 497 us per loop

In [5]: from dis import dis

In [6]: dis(sample_loop)
  2           0 BUILD_LIST               0
              3 STORE_FAST               1 (L)

  3           6 SETUP_LOOP              33 (to 42)
              9 LOAD_GLOBAL              0 (xrange)
             12 LOAD_FAST                0 (n)
             15 CALL_FUNCTION            1
             18 GET_ITER
        >>   19 FOR_ITER                19 (to 41)
             22 STORE_FAST               2 (i)

  4          25 LOAD_FAST                1 (L)
             28 LOAD_ATTR                1 (append)
             31 LOAD_FAST                2 (i)
             34 CALL_FUNCTION            1
             37 POP_TOP
             38 JUMP_ABSOLUTE           19
        >>   41 POP_BLOCK

  5     >>   42 LOAD_FAST                1 (L)
             45 RETURN_VALUE

In [7]: dis(sample_comprehension)
  2           0 BUILD_LIST               0
              3 DUP_TOP
              4 STORE_FAST               1 (_[1])
              7 LOAD_GLOBAL              0 (xrange)
             10 LOAD_FAST                0 (n)
             13 CALL_FUNCTION            1
             16 GET_ITER
        >>   17 FOR_ITER                13 (to 33)
             20 STORE_FAST               2 (i)
             23 LOAD_FAST                1 (_[1])
             26 LOAD_FAST                2 (i)
             29 LIST_APPEND
             30 JUMP_ABSOLUTE           17
        >>   33 DELETE_FAST              1 (_[1])
             36 RETURN_VALUE

実行速度は、リスト内方表記の方が倍以上速いですね。 バイトコードについては詳しくは解説しませんが、 sample_loopは "19 FOR_ITER" から "38 JUMP_ABSOLUTE" までの8命令、sample_comprehensionは "17 FOR_ITER" から "30 JUMP_ABSOLUTE" までの6命令がループになっています。 インタプリタはこれらの命令を読み込んで実行というループを行っているので、まずは命令数の削減が高速化の理由の1つになります。 さらに、ループの中の処理について見ていきましょう。

1. append属性の取り出し

sample_loop のバイトコードで、 "23 LOAD_ATTR" という部分が、 "L" という変数からロードしたオブジェクトから "append" という属性を取り出しています。 属性の参照がどれくらい重い処理なのか、 属性の参照はそれなりに「重い」処理です。試しに、 append 属性の参照をループの外に追い出してみましょう。

In [19]: def sample_loop2(n):
   ....:     L = []
   ....:     append = L.append
   ....:     for i in xrange(n):
   ....:         append(i)
   ....:     return L
   ....:

In [20]: %timeit sample_loop2(10000)
1000 loops, best of 3: 549 us per loop

リスト内方表記を利用した場合に比べて1割程度しか遅くありません。実は、元のコードのオーバーヘッドの大半は、append属性の参照にあったという事になります。

ちなみに、バイトコードは載せていませんが、LOAD_ATTRが無くなった分命令数も8命令から7命令に削減されています。

2. 関数の呼び出し

リスト内包表記が速い残り1割の理由は、 "29 LIST_APPEND" という命令にあります。

内包表記を使わない場合は、 "13 CALL_FUNCTION" によって、 append を「Pythonの関数として実行」する必要があります。この「Pythonの関数として実行」というのは、 スタックから引数リストを作成する必要があったり、呼び出される関数の方で引数のチェックをしていたり、関数の戻り値の処理("37 POP_BACK" など)が必要になります。

それに対して、 "LIST_APPEND" という命令はリストオブジェクトに要素を追加する専用の命令なので、内部ではリストへの要素の追加のみが行われるようになります。

まとめ

リスト内方表記を使うと速くなる仕組みを、バイトコードレベルで解説しました。もっと下まで知りたい方は、Pythonのソースコードの ceval.c というファイルから、 CALL_FUNCTION と LIST_APPEND の処理をしている部分を追ってみてください。

また、速くなる理由の大半は、属性の参照コストにありました。このコストは、内包表記を使わなくても、メソッドをループの手前でローカル変数に代入するというテクニックでも回避できます。このテクニックはリストのappendメソッド以外にも任意の属性に利用できるので、ボトルネックになるかもしれないループを書くときには積極的に利用しましょう。


@methane
klab_gijutsu2 at 17:25|この記事のURLComments(2)TrackBack(0)
2010年03月02日

Windowsでfopenを使ってはいけない!?

はてなブックマークに登録

Windows環境でsvn+ssh:// からの bzr branch に失敗するという報告があり、調べてみたところ面白いことが判ったので記事にしておきます。

まず、bzr-svnはsubvertpyというlibsvnのバインディングを利用していて、svn+ssh://プロトコルのハンドリングはlibsvnが行っています。 このため、bzr+ssh://ではPython製のSSHクライアントであるparamikoを利用している環境でも、svn+ssh://の場合はlibsvnがssh.exeやSVN_SSH環境変数に設定されたsshクライアントを起動しています。 私はputtyを利用しているので、SVN_SSH=plinkと設定してsvn+sshが利用できる環境を用意して問題が再現することを確認しました。

ログやトレースバックを追ってみたところ、あるファイルを構築する際に一時ファイルに書き込みしていって最後にクローズしてから目的のファイル名にリネームする、というロジックのリネームの部分でPermission Errorが発生していました。

しかし、エラーの原因になっているファイルは、リネーム直前に確実にclose()されています。 なぜPermission Errorが発生しているのかを調べるためにProcess Monitorで該当ファイルに関するシステムコールを追ってみたところ、

  • Bazaarがファイルをclose()しているタイミングで、CloseFile()が発行されていない
  • なぜかエラー発生後にplink.exeからCloseFile()されている

という事が判りました。

最初は「なんでplink.exeがBazaarの使っているファイルのハンドルを持っているんだ?」と混乱していたのですが、BazaarのIRCでこの事を話してみたところ、 "<jelmer>naoki: It's inheriting handles from the parent process perhaps?" と言われ、Windowsのプロセスとファイルハンドルについて調べてみました。

まず、WindowsのCreateProcess()システムコールは、bInheritHandlesという引数を持っていて、この引数がTRUEの場合ハンドルを子プロセスに引き継ぎます。 引き継がないとパイプが利用できないので、libsvnはbInheritHandles=TRUEでsshクライアントを立ち上げます。

CreateProcess()側でパイプ以外のハンドルを引き継がないような事ができないのですが、 CreateFile()のlpSecurityAttributes引数にSECURITY_ATTRIBUTES構造体を渡すことができ、この中にbInheritHandleというフラグがあります。 このフラグをFALSEにすると、CreateFile()で作ったファイルハンドルは子プロセスに引き継がれないようです。

Pythonの中からCreateFile() API を直接利用するのはちょっと面倒なので、MSVCRTのopen()やfopen()でもできないか調べてみたところ、

  • fcntl.h 内で定義されている O_NOINHERIT フラグを open() に渡す
  • fopen() の mode として、 "rbN" のように後ろに N をつける

という方法でファイルハンドルを引き継がないようにファイルを開くことが出来ることが判りました。

ここまで判ったところで、Bazaarの修正に取りかかりました。問題になっているファイルはビルトインのopen()関数を使っていて、 この関数のmode引数はそのままfopen()のmode引数になるので、 "wb" となっているところを "wbN" と書き換えるだけで良いかなと思ったのですが、 次のような問題がありました。

  • "N" は現在のglibcでは無視されているけれども、将来何かに利用されるかも知れないし、他のlibcで利用されているかも知れない。 なのでWindowsでのみ"N"を使うように修正しないといけない。
  • しかも、"N"が有効なのはVC++2005以降のMSVCRTで、それより前のMSVCRTでは無視される。Windows版のPython2.4やPython2.5では使えない。

なので、C言語のopen()関数に相当するPythonのos.open()関数と、os.O_NOINHERITフラグを利用してビルトインのopen()の代わりになる関数を作成し、ファイルをクローズした後にそのファイルを削除やリネームする場所でその関数を使うようにしました。

参考に、今回の修正のうち、open()の代替になっているopen_file()関数の定義部分だけ掲載しておきます。 実際の修正はこの修正のマージリクエスト で見ることが出来ます。また、Python標準ライブラリのtempfileモジュールもos.O_NOINHERITを利用して、一時ファイルを利用した後にファイルを削除できるようにしているので、そちらも参考にして下さい。

O_NOINHERIT = getattr(os, 'O_NOINHERIT', 0)

if sys.platform == 'win32':
    def open_file(filename, mode='r', bufsize=-1):
        """This function works like builtin ``open``. But use O_NOINHERIT
        flag so file handle is not inherited to child process.
        So deleting or renaming closed file that opened with this function
        is not blocked by child process.
        """
        writing = 'w' in mode
        appending = 'a' in mode
        updating = '+' in mode
        binary = 'b' in mode

        flags = O_NOINHERIT
        # see http://msdn.microsoft.com/en-us/library/yeby3zcb%28VS.71%29.aspx
        # for flags for each modes.
        if binary:
            flags |= O_BINARY
        else:
            flags |= O_TEXT

        if writing:
            if updating:
                flags |= os.O_RDWR
            else:
                flags |= os.O_WRONLY
            flags |= os.O_CREAT | os.O_TRUNC
        elif appending:
            if updating:
                flags |= os.O_RDWR
            else:
                flags |= os.O_WRONLY
            flags |= os.O_CREAT | os.O_APPEND
        else: #reading
            if updating:
                flags |= os.O_RDWR
            else:
                flags |= os.O_RDONLY

        return os.fdopen(os.open(filename, flags), mode, bufsize)
else:
    open_file = open

この修正が問題なく取り込まれれば、Bazaar 2.1.1からは svn+ssh:// からの bzr branch ができるようになります。


@methane
klab_gijutsu2 at 18:31|この記事のURLComments(0)TrackBack(0)
2010年02月17日

Bazaar 2.1 がリリースされました

はてなブックマークに登録

Bazaar 2.1 がリリースされました。まだソースコードのみですが、ここからダウンロードできます

Bazaarは 2.0 のリリース からUbuntuのような半年ごとのリリーススケジュールに移行していて、ほぼスケジュール通りのリリースとなりました。

今回も、私の基準でこの半年間のBazaarやその周辺の改良を紹介しておきます。

Bazaar本体の改良

まずはbzr本体の改善点です。

  • bzr+ssh でも bzr+sftp のように、 ~ でホームディレクトリを指定できるようになりました。 自分のホームディレクトリ以下にリポジトリ置き場を持っている人は、 bzr+ssh://host/~/bzr/proj/branch の用に短いURLを利用できるようになります。
  • メモリ使用量や速度が向上しました。特にタプルというPythonの軽量なデータ構造をさらに軽量にしたStaticTupleの存在が大きいです。Pythonを使う人はぜひ参考にしてみてください。
  • updateコマンドに-rオプションがつきました。これは svn の update -r オプションと同じで、現在の作業ディレクトリの内容を一時的に過去のリビジョンで 上書きするコマンドです。ビルド手順が複雑なプログラムの過去の動作を確認したいときなど、現在の作業ディレクトリを使い回して過去のリビジョンをビルド したい場面で便利です。最新リビジョンに戻る場合は -r オプション無しで bzr update します。

GUIの改良

TortoiseBZR

私がメンテナをしているTortoiseBZRも、この半年間で大きく改善しました。
  • ビルドするのにMicrosoft Visual Studio Standard以上が不要になりました。
    今まではシェル拡張の作成にATLを使っていたのですが、もうすぐでるVisual Studio 2010 でも無料のExpress EditionにATLが付属しないことが判明して、ATLの摘出手術をしました。 現在は Windows SDK と Python だけでビルドが可能です。ということで、一緒にメンテナしてくれる人を募集中です(笑)
  • ステータスアイコンのオーバーレイ表示で、まだステータスを取得していないファイルには ? の形をしたアイコンを表示しているのですが、 ステータス取得完了後にF5キーなどで手動更新しなくても自動でアイコンが更新されるようになりました。
  • i18n対応をしました。TortoiseBZR以外のGUIツールも日本語コミュニティの皆さんの強力で高い翻訳率を維持しているので、英語アレルギーな人にも安心してお使いいただけます。
  • コンテキストメニューの内容を整理しました。特に、Bazaarの推奨GUIである bzr-explorer や、任意のコマンドを実行できる qrun をコンテキストメニューから実行することが可能になりました。
  • ファイルの変更を監視しているディレクトリ内で大量のファイルの変更が発生した場合(プログラムのビルド中など)にCPU使用率が若干高めになっていたのを修正しました。

qbzrとbzr-explorer

全体的に完成度が高くなっています。特に、 bzr-explorer は先日 1.0rc1 をリリースしました。 0.x 系から 1.x 系に変更したのは、開発者の「そろそろ一般的な作業はこのGUIで完結できるだろう」という自信の表れだと思います。

  • qdiff などでテキストファイルを表示する際、テキストのエンコーディングをGUIから指定できるようになりました。
  • qbrowse や qcommit などのツリー表示で、ファイルのリネームや移動ができるようになりました。
  • bzr-explorer の作業ツリーブラウザでファイルを開くときに、拡張子毎にファイルを開くのに使うアプリケーションを指定できるようになりました。
GUIはどうしても一部の機能に偏って使ってしまうので、私が気づいていない大きな改良点もたくさんあると思います。

Bazaar 2.2 に向けて

2.1 は 2.0 のブラッシュアップのようなリリースですが、今後はまたユーザーが使いやすくなるような改良がどんどん入ってくると思われます。

私は bzr 本体のヘルプなどの i18n 対応を担当しようかと思っています。

他にも、gitやhgのように「作業ツリーとリポジトリは一つで、その中でたくさんのブランチを扱う」というスタイルを実現する colocated branch も、試験実装となる新しいプラグインが bzr-colo という名前でリリースされ、活発な議論がされています。

また、最近 bzr-git も試してみたのですが、シンボリックリンクの入ったgitリポジトリをうまく扱えないことに気づいて修正した以外は問題なくなっていました。 もうそろそろ 1.0 のリリースが近いのではないかと思います。 bzr-2.2 がリリースされたら 「github のクライアントとしてTortoiseBZR を使う」なんて記事を書くつもりです。

現在、日本でBazaarを使うのに一番足りていないのは日本語の情報だと思うので、日本語の情報を集めるHubサイトを作ろうと思います。 先週会社のデータセンターにある空きサーバーを一台借りて、空き時間を使ってセットアップ中なので、もうしばらくお待ち下さい。


@methane
klab_gijutsu2 at 16:17|この記事のURLComments(0)TrackBack(0)
2009年09月28日

Bazaar 2.0 がリリースされました

はてなブックマークに登録

過去に Bazaarの紹介 で紹介したときには 1.9 だったBazaarですが、その後も毎月のリリースをかさねて先週とうとう メジャーバージョンアップとなる 2.0 がリリースされました。(ダウンロードサイト)

正式なリリースノートは公式Webサイトを参照してもらうとして、この一年弱で改良された点を私の基準で紹介したいと思います。

TortoiseBZRが(たぶん)実用レベルになった

私はたいていコマンドラインから利用しているので、以前紹介したときはTortoiseBZRをあまり使い込んでいませんでした。

その後、TortoiseBZRを使い込んでみたところ、pushができない、addもできないなど最低限の機能がそろっていなかったり、ステータスの表示が重くてストレスになったりしたので結局アンインストールしてしまいました。

その後今年に入ってからTortoiseBZRの開発が停滞してしまい、このままじゃいけないと思って6月後半からTortoiseBZRの開発に参加しました。この2ヶ月で以下のような問題を解決しました。

  • シェル拡張の致命的なバグ(「新しいフォルダを作成」などのボタンが動作しなくなる、Windows Vistaでコンテキストメニューのレイアウトが崩れるなど)が修正されました。
  • ステータスの取得が速くなりました。また、まだステータスを取得していないファイルの場合 '?' マークのステータスアイコンを表示してバックグラウンドでステータスを取得することで、エクスプローラの応答速度の低下を抑えました。(この方法はTortoiseHGの開発者にTwitterで教えてもらって開発しました。感謝感謝)
  • add, push, send といった機能をコンテキストメニューに追加しました。

まだTortoiseSVNの完成度には到達していませんが、とりあえずユーザーを邪魔をする挙動が無くなって基本的な操作の大半がコンテキストメニューから呼び出せるようになったので、人に試してみてと言える状態になっています。

新しいリポジトリフォーマットが標準になった

Bazaar 2.0 からは 2a といわれる新らしいリポジトリフォーマットが標準になりました。 Bazaarではデータの格納方式が変わる以外にも、新機能のために新しいメタデータを追加した時にも古いバージョンのBazaarからそのメタデータがないリビジョンをコミットされてしまわないようにとリポジトリフォーマットを変更しています。その結果としてBazaarには大量のリポジトリフォーマットができてしまい、「○○のフォーマットでは△△の機能が使えない」といった問題が発生していました。

新しいフォーマットが標準になることで、リポジトリフォーマットをオプションから選ばなくても新しい機能が全部使える、リポジトリサイズが小さくなったりいくつかの操作が速くなっていたりするという利点があります。

WindowsでのUnicodeファイル名への対応が改良された

PythonではUnicode APIを利用することで、Unicodeファイル名でファイルを扱うことができるのですが、 sys.argv で取れるコマンドライン引数は非Unicodeです。(Python3ではUnicodeになります) Bazaarも sys.argv を利用していたため、せっかくUnicodeでファイル名を扱うのにコマンドライン引数からはコードページでしかファイル名を取得できないという問題がありました。

最近のバージョンではこの部分が GetCommandlineW() APIを利用するようになり、bzr本体だけでなくQBzr等のGUIを含めてUnicodeへの対応が進んでいるので、Unicodeファイル名に関する問題はほとんどなくなりました。

日本語ユーザーグループができた

日本語で議論ができる環境が欲しいと思い、Bazaar 2.0 のリリースをきっかけにユーザーグループを作ることにしました。 http://groups.google.co.jp/group/bazaar-ja

Launchpad上にドキュメント翻訳プロジェクトも作成し、ゆっくりですが翻訳作業も開始しています。TortoiseBZRのバグ報告や要望なんかも、英語でLaunchpadに登録するよりも手軽にできるはずです。Bazaarに関心のある方はぜひ参加してみてください。


@methane
klab_gijutsu2 at 21:14|この記事のURLComments(4)TrackBack(0)
2009年07月13日

MessagePackのPython Bindingをリリースしました

はてなブックマークに登録

MessagePack とは、古橋(id:viver)さんが開発された高速・高効率なバイナリシリアライズフォーマットです。詳しくは 古橋さんの日記プロジェクトサイト を見てください。

PythonからMessagePackフォーマットでSerialize/DeserializeするためのPythonパッケージを作ったので、取得方法と使い方について簡単に紹介します。

1. インストール

msgpackという名前でPython Package Index (PyPI)に登録してあります。 <http://pypi.python.org/pypi/msgpack/>

setuptoolsをインストールしている環境では、

$ easy_install msgpack

でインストールすることができます。

Windowsでインストールする場合は、PyPIのパッケージサイト からインストーラをダウンロードしてインストールするのがお手軽です。

2. シリアライズ

まず、msgpackパッケージをimportします。

>>> import msgpack
>>> help(msgpack)  # docstring を読む

一つのオブジェクトをシリアライズするには、 msgpack.packb() を利用するのが簡単です。

>>> msgpack.packb([1,2,3])
'\x93\x01\x02\x03'

連続してシリアライズする場合、 msgpack.Packer オブジェクトを利用するとオーバーヘッドが少なくなります。

>>> packer = msgpack.Packer()
>>> packer.pack([1,2,3])
'\x93\x01\x02\x03'
>>> packer.pack([4,5,6])
'\x93\x04\x05\x06'

3. デシリアライズ

msgpack.unpackb() を利用すると、1オブジェクト分のシリアライズされたデータをデシリアライズできます。

>>> msgpack.unpackb(b'\x93\x01\x02\x03')
[1, 2, 3]

ストリームからデシリアライズする場合、 msgpack.Unpacker オブジェクトを利用することで、連続したデシリアライズができたり、オブジェクトの境界が判らない場合に対応できます。

>>> unpacker = msgpack.Unpacker()
>>> buf = b'\x93\x01\x02\x03' * 5
>>> len(buf)
20
>>> unpacker.feed(buf[:9])
>>> for o in unpacker:
...     print o
...
[1, 2, 3]
[1, 2, 3]
>>> unpacker.feed(buf[9:])
>>> for o in unpacker:
...     print o
...
[1, 2, 3]
[1, 2, 3]
[1, 2, 3]

4. ベンチマーク

テストコード:

#!/usr/bin/env python
# coding: utf-8

from msgpack import packs, unpacks
from cPickle import dumps, loads
import simplejson as json
from time import time

BENCH_NUM = 10

def bench(func, num=BENCH_NUM):
    start = time()
    for i in xrange(BENCH_NUM):
            func()
    end = time()
    print "%-12s  %4.3f[ms]" % (func.__name__, (end-start)*1000/BENCH_NUM)

def setup_int():
    global a, a_pickle, a_mpack, a_json
    a = range(1024) * 2**10
    a_pickle = dumps(a)
    a_json = json.dumps(a)
    a_mpack = packs(a)

def setup_str():
    global a, a_pickle, a_mpack, a_json
    a = ['a'*(i % 4096) for i in xrange(2**14)]
    a_pickle = dumps(a)
    a_json = json.dumps(a)
    a_mpack = packs(a)


def pickle_dump(): dumps(a)
def pickle_load(): loads(a_pickle)

def json_dump(): json.dumps(a)
def json_load(): json.loads(a_json)

def mpack_pack(): packs(a)
def mpack_unpack(): unpacks(a_mpack)


targets = [
        pickle_dump, pickle_load,
        json_dump, json_load,
        mpack_pack, mpack_unpack]
import gc
gc.disable()

bytes_suffix = "[bytes]"

print "== Integer =="
setup_int()

print "= Size ="
print "pickle:", len(a_pickle), bytes_suffix
print "json:  ", len(a_json), bytes_suffix
print "mpack: ", len(a_mpack), bytes_suffix

for t in targets:
    bench(t)

print "== String =="
setup_str()

print "= Size ="
print "pickle:", len(a_pickle), bytes_suffix
print "json:  ", len(a_json), bytes_suffix
print "mpack: ", len(a_mpack), bytes_suffix

for t in targets:
    bench(t)

結果:

== Integer ==
= Size =
pickle: 6203398 [bytes]
json:   5154816 [bytes]
mpack:  2752517 [bytes]
pickle_dump   248.640[ms]
pickle_load   360.288[ms]
json_dump     255.601[ms]
json_load     136.922[ms]
mpack_pack    51.383[ms]
mpack_unpack  26.851[ms]
== String ==
= Size =
pickle: 33731696 [bytes]
json:   33611776 [bytes]
mpack:  33595139 [bytes]
pickle_dump   182.324[ms]
pickle_load   141.639[ms]
json_dump     126.675[ms]
json_load     93.945[ms]
mpack_pack    66.833[ms]
mpack_unpack  34.953[ms]

pickleやjsonを圧倒していますが、まだまだ改善の余地がありそうです。 とくにpackはunpackと違って大量のPythonオブジェクトを生成する必要があるわけではないのに、unpackよりも時間がかかってしまっています。

5. その他

ここでは紹介していない、simplejsonやpickleモジュールに似せたAPIもあります。pickleやjsonからmsgpackへ移行するときはパッケージの中を探してみて下さい。

ただし、現在のmsgpackはバイト列・整数・浮動小数点数・シーケンス型・辞書型にしか対応できていません。simplejsonは dumps()defalut() 関数を、 loads()object_hook() 関数を用意することでユーザーが定義したクラスのシリアライズ・デシリアライズができます。0.2 では同じような default()object_hook() へ対応しようと思います。


@methane
klab_gijutsu2 at 15:58|この記事のURLComments(1)TrackBack(1)
2009年03月13日

IPythonでunicodeリテラルを使う

はてなブックマークに登録

IPythonとは

IPythonとは、Pythonistaの中で絶大な人気を誇るインタラクティブシェルです。

Pythonはもともとインタラクティブシェルを内蔵しているのですが、 IPythonには内蔵のインタラクティブシェルと比べて多くの利点があります。いくつか挙げてみます。

  • 動的補完

    変数名などを途中まで入力した状態でTABキーを押すと、残りを補完してくれます。 候補が複数ある時は候補一覧を表示してくれます。

  • シンタックスハイライト

    プロンプトなどに色がつきます。ちょっとうれしいです。

  • 通常のシェルとしても利用可能

    IPythonの中で、lsでファイル一覧を見たり、cdでカレントディレクトリを移動したりできます。 !vim のようにエクスクラメーションマークに続いてコマンド名を入力することで、コマンドの実行もできます。 さらに、コマンドの出力がPythonの変数に格納され、Pythonから利用可能になります。Unixの大量のコマンドを使いこなせなかったり、Windowsでコマンドが少ないことが不満だったりするときに重宝します。

  • よく使う機能を省略形で記述可能

    help(o) の代わりに o? でそのオブジェクトのドキュメントが読めます。

    func(a, b) の代わりに func a b で関数呼び出しが可能です。

  • エディタと連携可能

    ed foo.py のようにすることで、 foo.py を外部エディタで開いて編集できます。 そしてエディタを閉じると、自動的にそのファイルがevalされます。 この機能を使うと、エディタで関数を書く→IPython上で動作を確認する→エディタで関数を修正する、というサイクルでサクサク開発できます。 これに馴れてしまうと、IDEが欲しいと思わなくなります。

IPythonでUnicodeリテラルを使う

IPython 0.9.1 でUnicodeリテラルを使おうとすると、次のような問題がありました

$ python
Python 2.5.2 (r252:60911, Oct  5 2008, 19:24:49)
[GCC 4.3.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> u"あ"
u'\u3042'
>>>
$ ipython
Python 2.5.2 (r252:60911, Oct  5 2008, 19:24:49)
Type "copyright", "credits" or "license" for more information.

IPython 0.9.1 -- An enhanced Interactive Python.
?         -> Introduction and overview of IPython's features.
%quickref -> Quick reference.
help      -> Python's own help system.
object?   -> Details about 'object'. ?object also works, ?? prints more.

In [1]: u"あ"
Out[1]: u'\xe3\x81\x82'

コンソールはUTF-8なので"あ" は3byteになっているのですが、それぞれのbyteを一文字と認識してUnicode文字に変換されています。

この問題を追ってみたところ、IPythonがPythonの組み込み関数 compile() を呼び出すときに、文字列をエンコードしてしまっているのに気づきました。 compile() の動作を調べて見たところ

>>> c = compile("""u'あ'""", "foo.py", 'single')
>>> c.co_consts
(u'\xe3\x81\x82', None)
>>> c = compile(u"""u'あ'""", "foo.py", "single")
>>> c.co_consts
(u'\u3042', None)

ビンゴです。

ということで、IPython/iplib.py に次の修正をすることで、IPythonでUnicodeリテラルが気持ちよく使えるようになります

--- iplib.py.old    2009-03-13 15:42:33.000000000 +0900
+++ iplib.py        2009-03-13 15:42:58.000000000 +0900
@@ -2019,9 +2019,9 @@
         # this allows execution of indented pasted code. It is tempting
         # to add '\n' at the end of source to run commands like ' a=1'
         # directly, but this fails for more complicated scenarios
-        source=source.encode(self.stdin_encoding)
-        if source[:1] in [' ', '\t']:
-            source = 'if 1:\n%s' % source
+        #source=source.encode(self.stdin_encoding)
+        if source[:1] in [u' ', u'\t']:
+            source = u'if 1:\n%s' % source

         try:
             code = self.compile(source,filename,symbol)

Windows (Python 2.6.1, IPython 0.9.1) でもうまく動くか確認して見ました

In [1]: u"あ"
Out[1]: u'\u3042'

In [2]: u"ソ"
Out[2]: u'\u30bd'

バッチリです


@methane
klab_gijutsu2 at 16:22|この記事のURLComments(0)TrackBack(0)
Blog内検索
Archives
このブログについて
DSASとは、KLab が構築し運用しているコンテンツサービス用のLinuxベースのインフラです。現在5ヶ所のデータセンタにて構築し、運用していますが、我々はDSASをより使いやすく、より安全に、そしてより省力で運用できることを目指して、日々改良に勤しんでいます。
このブログでは、そんな DSAS で使っている技術の紹介や、実験してみた結果の報告、トラブルに巻き込まれた時の経験談など、広く深く、色々な話題を織りまぜて紹介していきたいと思います。
最新コメント