Python

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)
2009年02月27日

WSGIServerを3行でマルチスレッド化する

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

WSGIとは

PythonでWebアプリを作るときに必ず出てくる単語にWSGIがあります。 WSGIとは、Web Server Gateway Interface の略で、WebサーバーとPython製Webアプリを つなげる標準インタフェースです。

WSGIの上で動くようにアプリケーションを作ると、そのアプリケーションは修正無しに Apache+mod_wsgi, Apache+mod_python, fastcgi, scgi, cgi, 等の環境で動かせるように なります。

他にもミドルウェアという考え方があります。例えばOpenID認証機能をWebフレームワークの プラグインとして開発した場合では他のWebフレームワークでは利用できないのですが、 WSGIミドルウェアとして開発すればWebフレームワークを問わずに利用できるようになります。

標準ライブラリのwsgirefモジュール

Python2.5から、標準ライブラリにwsgirefモジュールが追加されました。その中にはWSGIサーバーも 入っているので、自分で書いたWSGIアプリを手軽に立ち上げたいときに重宝します。

次のコードは、簡単なWSGIサーバー+アプリの例です。

from wsgiref.simple_server import *

def myapp(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/plain')])
    return ['hello']

server = make_server('0.0.0.0', 8000, myapp)
server.serve_forever()

WSGIServerのマルチスレッド化

上記のコードで立ち上がるWebサーバーは、シングルスレッド&シングルプロセスで動作しているので、 複数のクライアントからアクセスされた場合に問題があります。接続が遅かったり不安定だったりする クライアントが一つでも合った場合、その接続が完了するか切断するまで他の接続ができないので、 接続に失敗したり数十秒かかったりすることになります。

普通はWebアプリを公開する場合には標準ライブラリのWSGIServerではなくて別のサーバーを使う ものですが、お手軽な方法として3行追加と1行修正のみで、他のライブラリ無しに マルチスレッド化したりマルチプロセス化することが出来ます。 次のコードが上記のコードをマルチスレッド化したものになります。

from wsgiref.simple_server import *
from SocketServer import *
class ThreadingWsgiServer(ThreadingMixIn, WSGIServer):
    pass

def myapp(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/plain')])
    return ['hello']

server = make_server('0.0.0.0', 8000, myapp, ThreadingWsgiServer)
server.serve_forever()

このコードについて簡単に説明します。 wsgiref.simple_server.make_server は第三引数でサーバークラスを指定することができて、 デフォルト値は wsgiref.simple_server.WSGIServer になっています。 WSGIServerの継承関係は次のようになっています。

SocketServer.BaseServer
  ↑
SocketServer.TCPServer
  ↑
BaseHTTPServer.HTTPServer
  ↑
wsgiref.simple_server.WSGIServer

SocketServerモジュールには、ThreadingMixIn, ForkingMixIn というクラスがあり、 SocketServer.BaseServer を継承したサーバークラスであれば簡単にマルチスレッド化 したりfork化したりできるようになっています。 WSGIServer も BaseServer を継承しているので、ThreadingMixIn/ForkingMixIn クラスを Mix-in してやるだけでマルチスレッド化やマルチプロセス化が可能です。

小さなテクニックですが、Webアプリが完成するまではサーバーの設定の問題とアプリの 問題が混ざらないようにしたいときとか、Webサーバーの設定ファイルを書くのに慣れてなくて 後回しにしたいときに重宝します。

ちなみに、 flup というライブラリには、 thread pool や prefork を使ったWSGIサーバーがあって十分実用になる速度が出るので、 標準ライブラリのサーバーだと遅いという時に利用できます。


@methane
klab_gijutsu2 at 17:40|この記事のURLComments(0)TrackBack(0)
2008年11月11日

64bit版 Windows で 64bit版 Python の常用に挑戦してみた

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

Python は早くから64bit版 Windows に対応した64bitの Python (amd64だけでなくia64も)を 配布してきましたが、一般的に使われる多くの拡張モジュールが64bit版のバイナリパッケージを 配布していないこともあり、ほとんど普及してきませんでした。

私はamd64版 Windowsを自宅で使っているのですが、 Python 2.5 まではやはり32bit版を 利用してきました。しかし、amd64版があるのに32bit版を使うのは、こう、なんというか、 負けた気分になるので、いつかはamd64版を使いたいと思っていました。

最近Windowsをクリーンインストールすることになり、 Python 2.6 を amd64でいれるか、それとも 今まで通りx86で入れるのか考えたのですが、win32 と wxPython という有名な拡張モジュールが amd64版を配布しはじめている事を知り、とうとうamd64版の Pythonを使い始めることにしました。

環境をそろえていく上でいくつか問題もあったので、環境構築手順を記録しておきます。

Pythonのインストール

Pythonの公式サイト から python-2.6.amd64.msi を ダウンロードしてインストールします。 デフォルトでは C:\Python にインストールされるのですが、私の趣味で C:\usr\Python にインストールしました。

インストールが完了したら、 C:\usr\PythonC:\usr\Python\Scripts を環境変数 PATH に設定しておきます。

setuptoolsのインストール

setuptools は ez_setup.py を使ってインストールするのが常套手段なのですが、同梱されているランチャの 実行ファイルが32bitでコンパイルされており、64bitコンソール上で利用するとエラーになって しまいます。 そこで、パッケージをダウンロードして、ランチャを64bitでコンパイルしなおした上でインストール することにします。

まず、amd64のバイナリを作成できる環境を用意します。私は Microsoft Windows SDK v6.1 を利用しました。コマンドプロンプトから、

"C:\Program Files\Microsoft SDKs\Windows\v6.1\Bin\SetEnv.Cmd" /Release

を実行して、clやlinkが使えるようにしておきます。

次に、Python Package Index から、 setuptools の tarballをダウンロードします。

先ほどのコマンドプロンプトでtarballを展開してできたディレクトリに入り、 launcher.c をコンパイルします。Windows SDK v6.1 でコンパイルするときには、 launcher.c に一行includeを追加してやる必要がありました。

 #include "windows.h"
+#include "process.h"

用意ができたら、ランチャをコンパイルします。gui版(実行してもプロンプトが表示されない) とcli版(プロンプトの中で実行される)の二種類を作成します。コンパイルオプションに注意してください。

C:\setuptools-0.6c9>cl /Ox /DGUI=0 /Fesetuptools\\cli.exe /MT launcher.c /link /SUBSYSTEM:CONSOLE /ENTRY:WinMainCRTStartup
Microsoft (R) C/C++ Optimizing Compiler Version 15.00.21022.08 for x64
Copyright (C) Microsoft Corporation.  All rights reserved.

launcher.c
Microsoft (R) Incremental Linker Version 9.00.21022.08
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:setuptools\\cli.exe
/SUBSYSTEM:CONSOLE
/ENTRY:WinMainCRTStartup
launcher.obj

C:\setuptools-0.6c9>cl /Ox /DGUI=1 /Fesetuptools\\gui.exe /MT launcher.c /link /SUBSYSTEM:WINDOWS /ENTRY:WinMainCRTStartup
Microsoft (R) C/C++ Optimizing Compiler Version 15.00.21022.08 for x64
Copyright (C) Microsoft Corporation.  All rights reserved.

launcher.c
Microsoft (R) Incremental Linker Version 9.00.21022.08
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:setuptools\\gui.exe
/SUBSYSTEM:WINDOWS
/ENTRY:WinMainCRTStartup
launcher.obj

これで、64bit版のランチャがビルドできたので、setuptools をインストールできます。

C:\setuptools-0.6c9>python setup.py install

IPythonのインストール

IPythonは、通常通り easy_install ipython でインストールできます。 ただし、これだけだと pyreadline がインストールされず、ipythonの力が 半減してしまいます。 pyreadline は普通にeasy_installするだけでは インストールに失敗したので、 http://ipython.scipy.org/dist/ から pyreadline-1.5.tar.gz をダウンロードしてきて、

>easy_install pyreadline-1.5.tar.gz

としてインストールしました。これで、ipythonのセットアップは完了です。

Linuxでは、bash等のシェルの代わりにIPythonを使う気にはならないのですが、 WindowsのコマンドプロンプトのシェルはLinux系のシェルよりも機能が少ないので、 単純なファイル操作だけでもIPythonが便利だったりします。

pywin32のインストール

pywin32はPythonからWindows APIを利用するための拡張ライブラリです。 これを使うと、たとえば win32file.CreateHardLink をつかって ハードリンクを作れるようになります。

sourceforge のダウロードページ から、 pywin32-{バージョン}.win-amd64-py2.6.exe をダウンロードして、インストールします。

拡張モジュールのコンパイル環境の設定

拡張モジュールが入ったソースパッケージを easy_install でインストールするには、 コンパイラの設定をしておく必要があります。32bit版では、Linux派に易しいMinGWを 使っていたのですが、amd64のためにWindows SDKを使う方法を紹介しておきます。

本来ならば、

>set DISTUTILS_USE_SDK=1
>"c:\Program Files\Microsoft SDKs\Windows\v6.1\Bin\SetEnv.Cmd" /Release /x64

とすることで、distutilsはコンパイラを自力で見つけようとせずに、環境変数をそのまま 使うようになります。

しかし、Python-2.6のdistutilsにはバグ (issue3741) があって、エラーになってしまいます。 Pythonをインストールしたディレクトリの下の、Lib/distutils/msvc9compiler.py を開いて、 316行目のtypoを修正します。

-        self.__path = []
+        self.__paths = []

これで、上記の方法でWindows SDKのコンパイラを利用できるようになりました。 32bit Windowsで使えるたいていの拡張ライブラリは、この設定をしてからソースパッケージを インストールすることで利用可能になります。


@methane
klab_gijutsu2 at 17:43|この記事のURLComments(0)TrackBack(0)
2008年11月10日

Bazaarの紹介

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

私は以前までバージョン管理システムには Mercurial を利用していたのですが、 最近 Bazaar に移行しました。 先日 1.9 がリリースされましたので、これを機に Bazaar を紹介します。

現在、日本で一番使われているバージョン管理システムはSubversionだと思うのですが、 最近は分散型のバージョン管理システムがオープンソース界で人気になっていて、Bazaarはその一つです。

分散型バージョン管理システムの利点はたくさん有るのですが、個人的に便利だと 感じる点は、(1)ネットワークにつながっていなくてもバージョン管理できる、(2)Subversion よりもマージが楽、(3)リポジトリのバックアップが楽、というところです。

分散型バージョン管理システムでよく使われているものに、git, Mercurial (hg), Bazaar (bzr) があります。git は Linux Kernel や Ruby on Rails で、 Mercurial は OpenJDK や Open Solaris で、 Bazaar は Ubuntu Linux の開発や MySQL で使われています。

Bazaarの利点

いくつかのバージョン管理システムの中で Bazaar を選ぶ理由について、Bazaarのサイトに載っています。

個人的に「Bazaarエライ!」と思う点を挙げてみます。

1. 日本語ファイル名の扱い

gitやMercurialはファイル名を「バイト列」として扱います。 なので、Windowsユーザーがコマンドプロンプトでファイルをコミットしようとすると、 ファイル名はcp932エンコードでコミットされます。これをLinuxでcheckoutすると、 Linuxのlocaleを無視して、cp932ファイル名でファイルが作られてしまいます。

それに対して、Bazaarはファイル名を「Unicode文字列」として扱います。 コマンドライン引数のファイル名は、Windowsであればコードページ、Linuxであればlocaleに 基づいてデコードされます。 なので、Mercurialやgitのような問題は起こりませんし、「表」「ソ」といった 2バイト目に '\' (0x5c) が入っているパスでも問題が起きません。

2. TortoiseBzr の存在

Bazaarのダウンロードページ から、Windows Standalone Installer (まだ 1.9 は RC までしか installer がReleaseされていませんが、そのうち出ると思います) をインストールすると、TortoiseBzr や bzr-svn (BazaarをSubversionクライアントとして使えるプラグイン) をまとめたバイナリがダウンロードできます。

この TortoiseBzr は、TortoiseSVN のように使うことができます。1.9rc1のTortoiseBzrを少し使ってみたのですが、 特に日本語の化けもなく、普通に使えている様です。

3. Bazaarをインストールしていないサーバーにリポジトリを置ける

Bazaarは、ssh+bzrというプロトコルの他に、sftpやftpでサーバー上のリポジトリに対して push/pull が できます。 また、リポジトリを読み込み専用で公開したい場合、単にリポジトリをApacheによって公開されている ディレクトリに 置くだけで済みます。

なので、例えばFTPしか利用できないレンタルサーバーでも、Bazaarリポジトリ置き場にして、そのリポジトリを 公開することができます。sshでログインできるサーバーがあったとしても、セットアップ作業が要らないのは 便利です。

Launchpadについて

バージョン管理に Bazaar を使ったオープンソースプロジェクトのホスティングサービスに、 launchpad があります。 Bazaar リポジトリのホスティングの 他にも、バグ管理、アイデア管理、翻訳、メーリングリストが利用できます。Ubuntu Linux や MySQL もここで開発されています。

Bazaar ユーザー以外にも、例えば Ubuntu Linux ユーザーであれば、「このアプリの日本語訳間違ってる」と 思ったときに launchpad 上で翻訳の修正ができるので、アカウントを取ってみては如何でしょうか?

@methane
klab_gijutsu2 at 21:03|この記事のURLComments(1)TrackBack(0)
2008年09月22日

Python2.x/3.0のunicode内部表現について

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

イントロ

Python2.6/3.0共にRC版がリリースされ、正式リリースが近づいて来ました。Python3.0の大きな変更の一つが、 Python2.xのstrとunicodeがunicode文字列のstrに統合され、従来のstrの代わりにbytesを導入することで、バイト列と文字列が明確に分けられたことです。

現在、Python2.5では、unicode文字列の内部表現がucs2のものとucs4のものがあり、それぞれの間では拡張 モジュールの互換性がなくなっています。Python2.6/3.0でこの状況がどう変化するのか調べてみました。

Python2.xのunicode内部表現について

Python2.5/2.6では、configureオプションに、--enable-unicode=ucs[24] というものがあり、デフォルトでは2になっています。 また、FedoraやUbuntuの最新版に入っているPythonのパッケージは、--enable-unicode=ucs4でビルドされています。

ucs2の利点として、Java/Windows/.NET上での相互運用性があります。 Javaや.NETの文字列オブジェクトを直接にPythonのUnicode文字列にマッピングできます。 Windows用のCPythonは、wfopenに直接ucs2のunicode文字列内部表現を渡すことで、unicodeパスに 対応しています。

もちろん、UCS-2を使う以上、Windows/Java/.NETも直面してきた、BMP外の文字への対応という問題もあります。 Windows/Java/.NETは、UCS-2にサロゲートペアを加えたUTF-16に対応することで、一文字2byteの内部表現のまま BMP外の文字へ対応してきました。Python2.5も同じくサロゲートペアに対応しています。

>>> u = u"\U00020b9f"
>>> print u
𠮟
>>> u
u'\U00020b9f'
>>> len(u)
2
>>> u[0]
u'\ud842'
>>> u[1]
u'\udf9f'

この用に、"\U"というエスケープを用いてBMP外の文字が入ったユニコードリテラルを生成すると、内部的には2つのcode unitと して扱われ、コード変換も正常に行われます。

ただし、code pointではなくてcode unitを単位とすると、for c in u: でサロゲートペアが別々にcに格納されたりして、 文字単位の操作が面倒になります。以前、小飼弾さんが perl, python & ruby - chr() vs. Unicode perl, python & ruby - ord() vs. Unicode でご指摘されたように、unichr()/ord()も、このcode unit単位の操作となり、BMP外の文字の扱いが面倒です。

Windows APIやJava/.NETの文字列との相互運用性を気にしなくてもよいLinux上のCPythonでは、ucs2を選ぶかucs4を選ぶかは メモリ消費量と使い勝手のトレードオフになります。Fedora/Ubuntuでは、使い勝手の良いucs4が選ばれたようです。先ほどの例を ucs4のPython2.5で実行するとこうなります。

>>> u = u"\U00020b9f"
>>> print u
𠮟
>>> len(u)
1
>>> u[0]
u'\U00020b9f'

Python2.6でも、Python2.5の動作を踏襲しているので、状況はまったく変わりません。(Pythonは2.x=>3.xのようなメジャー バージョンアップ以外で、下位互換性がなくなるような変更は基本的に入りません) ucs2/4間で拡張モジュールのバイナリ互換性はありませんし、ucs4のPythonで 1 code unit = 1 code point を前提に 作られたコードはucs2のPythonでBMP外の文字を正常に扱えません。

Python 3.0の変更点

Python 3.0では、configureオプションが変更され、 --with-wide-unicode になっています。 2.xに比べると、(1)unicode無しが選択できなくなった、(2)utf-16も扱えるのにconfigureオプションでucs2と書いていたのが修正された、 というだけの修正で、デフォルトでcode unitが2byteな事に変わりがありません。

>>> u = "\U00020b9f"
>>> print(u)
𠮟
>>> len(u)
2
>>> u[0]
'\ud842'
>>> u[1]
'\udf9f'
>>> u
𠮟

せっかく、文字列とバイト列が分離されたのに、文字列のAPIで「文字」ではなくて「code unit」などというものが単位になって しまったのは、個人的にすごく残念に思います。 python-devメーリングリストでも、私と同じように思った人が、utf-16のPython3.0でもcode unitではなくてcode pointを単位に してほしいという要望があったのですが、却下されてしまいました。 (該当する議論が行われたスレッド)

ただし、chr/ordについては、ucs2のPythonでもBMP外文字を扱えるようになりました。

>>> chr(0x00020b9f)
'𠮟'
>>> ord('\U00020b9f')
134047
>>> print(hex(_))
0x20b9f

ucs2で単位の操作をする方法ですが、今のところ標準の方法は用意されていないようです。先ほど紹介したMLのスレッドでも、 Guidoさんはこう書いています。(該当のポスト)

The one thing that may be missing from Python is things like interpretation of surrogates by functions like isalpha() and I'm okay with adding that (since those have to loop over the entire string anyway).

結論として、Python 2.6/3.0になっても、サロゲートペアへの対応は中途半端で、将来的に Javaと同じようなAPIの追加によってucs2のPythonでもBMP外の文字を含む文字列に対応する 可能性があるようです。

個人的には、これからもLinux上でucs4のPythonを利用することで、サロゲートペアへの対応は必要な時以外 考えないようにしようと思います。(だって面倒ですし…)


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

PasteDeployを使って複数のWSGIアプリを一つのプロセスに共存させる

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

社内でWebアプリをデモしたいとか、開発環境としてLinuxサーバーが欲しくなったとき、「サーバー欲しい」と言えば すぐにLinuxの入った仮想環境をもらうことができます。

せっかくもらったrootを持ってるサーバーなので、MercurialやTracなど入れて遊んでいるのですが、 PCサーバー上の仮想マシンなので、メモリが何GBも載っている訳ではありません。 すこしメモリ使用量を削減してみることにしました。

いまどきのPython製Webアプリは、大抵WSGIという標準インタフェースを持っているので、 PasteDeploy(Pylonsが利用しているWSGIアプリ用のメタフレームワーク)を利用して、 一つのPythonプロセスに複数のWSGIアプリを同居させてみます。

PasteDeploy設定ファイル

Pylonsアプリの設定ファイルを流用して、複数のアプリをcompositeするように設定します。
# comp_exam.ini

# ...省略...

[composite:main]
use = egg:Paste#urlmap
/trac = tracapp
/hg = hgapp
/nanika = nanika

## factoryについては後述
[app:tracapp]
paste.app_factory = compapp:trac_factory
trac.env_parent_dir = /path/to/trac

[app:hgapp]
paste.app_factory = compapp:hg_factory
hgapp.rep_path = /path/to/hg/repo


## もとのPylonsアプリを、[app:main]から[app:適当な名前]に変更します。
[app:nanika]
use = egg:Nanika
lang = ja

# ...省略...

WSGIアプリケーションファクトリ

PasteDeployにWSGIアプリを渡すために、WSGIアプリのファクトリを作ります。このファクトリは、 引数にconfig(設定ファイルの内容)を受け取って、WSGIアプリのオブジェクトを返します。 上の設定ファイルに対応するcompapp.pyは以下の用になります。

#compapp.py
import mercurial.hgweb.hgweb_mod as hgweb
import trac.web.main as tracweb

def hg_factory(config, **localconfig):
    return hgweb.hgweb(localconfig['hgapp.rep_path'])

def trac_factory(config, **localconfig):
    def trac_app(environ, start_response):
        environ['trac.env_parent_dir'] = localconfig['trac.env_parent_dir']
        return tracweb.dispatch_request(environ, start_response)
    return trac_app
このファイルをPYTHONPATHが通ったところにおいて、通常のPylonsアプリを起動するのと 同じ感覚で起動します。
$ PYTHONPATH=. paster serve comp_exam.ini

これで、http://localhost/trac/ で Trac に、 http://localhost/hg/ で Mercurial にアクセスできるようになります。

追記: TracとPythonの最適化オプションについて

Trac-0.11が出たので入れてみたのですが、PYTHONOPTIMIZE=xという環境変数をセットしてPythonの最適化を有効にすると、Wikiのヘッダとフッタが出なかったりしてハマりました。Pythonの最適化オプションってあまり使われていないかもしれませんが、僕のように貧乏性な方はお気をつけください。


@methane
klab_gijutsu2 at 21:58|この記事のURLComments(0)TrackBack(0)
2008年07月10日

setuptoolsでモジュールのバージョンを指定する方法

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

ほとんどのPythonistaがお世話になっているeasy_install (setuptools) ですが、よく、

$ easy_install "SQLAlchemy == 0.4.6"

のようにバージョン指定してモジュールをインストールすることがあります。

バージョン指定には不等号も使うことができるのですが、例えば0.4系の新バージョンが出てupgradeする場合に、

$ easy_install -U "SQLAlchemy < 0.5"

として最新版を自動的に探してupgradeしようとすると、0.5beta1がインストールされてしまいました。確かに 0.5betaは0.5よりも前のバージョンだろうけど…あぅあぅ…ということで、setuptoolsがバージョンをどう扱うか 調べてみました。

バージョン番号について

リリース番号
1.2.3 みたいな部分
タグ
1.2.3foobar のうち foobar の部分
プレリリースタグ
タグが辞書順で"final"よりも前だと、プレリリースタグになる。 ただし、rc、pre、preview は文字cと同じ扱い。 プレリリースタグがつくと、タグなしの同じリリース番号が示すバージョンよりも 過去のバージョンになる。 1.2.3pre3 < 1.2.3
ポストリリースタグ
タグが辞書順で"final"よりも後ろだと、ポストリリースタグになる。'-'も使える。 ポストリリースタグがつくと、より新しいバージョンを示す。 1.2.3 < 1.2.3r2 , 1.2.3rc1 < 1.2.3rc1-20080707

バージョン条件の指定の仕方

easy_install や、 setupスクリプトの依存関係記述において、バージョンに関する 複数の条件を、カンマで区切って指定できる。

$ easy_install "SQLAlchemy >= 0.4, < 0.5a"


いままで自分でsetupスクリプト(setup.py)を書くときに、○○以上というバージョン指定しか してなかったのですが、そのパッケージをeasy_installでインストールする人が依存パッケージの 自動インストールで想定したより新しい、しかもα版のパッケージをインストールしてしまったり していたので、これからは○○以上△△未満という形で書いていこうと思います。


@methane
klab_gijutsu2 at 18:38|この記事のURLComments(0)TrackBack(0)
2008年07月01日

Pythonで@deprecatedというデコレータを用意する

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

@deprecatedというデコレータがあれば、

## そろそろ削除したい関数があるとき
@deprecated
def unsupportedfunc():
  nanika

## foo から bar に関数名を変えるとき
def bar():
  nanika
foo = deprecated(bar)
とできて便利です。

このdecoratorの実装例が、Cookbookに載っていました <http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/391367> Cookbookのコメント欄にもありますが、stacklevelを設定することで、deprecatedな関数を呼んだのがどこかを調べることができます。
        warnings.warn("Call to deprecated function %s." % func.__name__,
-                      category=DeprecationWarning)
+                      category=DeprecationWarning,
+                      stacklevel=2)

Python 2.5にdeprecated decoratorを入れるかどうかがPython-Devで話題になっていたのですが、今のところ__builtin__にも標準ライブラリにも入ってないようです。(あまり深く追っていません)


@methane
klab_gijutsu2 at 14:15|この記事のURLComments(0)TrackBack(0)
2008年06月19日

Linux上のPythonのzipfileを使って、permissionを気にしないでzipファイルを作成する

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

Pythonの標準ライブラリには、zipファイルを扱うためのzipfileモジュールがあります。以下のようにして、簡単にzipファイルを作成することが出来ます。
In [1]: from zipfile import ZipFile

In [2]: z = ZipFile('foo.zip', 'w')

In [3]: z.writestr('foo.txt', 'foo\nbar\nbaz')

In [4]: z.close()
しかし、これをunzipすると、permissionが全部0になってしまいます。
In [5]: !unzip foo.zip
Archive:  foo.zip
 extracting: foo.txt                

In [6]: !ls -l
total 8
---------- 1 naoki naoki  11 Jun 19 18:21 foo.txt
-rw-r--r-- 1 naoki naoki 123 Jun 19 18:21 foo.zip
いくつかのフラグを操作すれば任意のパーミッションを設定できるはずなのですが、特に設定したいパーミッションが無い場合は、zipファイルが Windows上で作成されたと偽装することで、unzip時にデフォルトのパーミッションが設定されるようにすることが出来ます。
In [7]: z = ZipFile('bar.zip', 'w')

In [8]: z.writestr('bar.txt', 'foo\nbar\nbaz')

In [9]: for zi in z.filelist:                    
   ...:     zi.create_system = 0   # Linuxだとデフォルトで3が設定されている。
   ...:    

In [10]: z.close()

In [11]: !unzip bar.zip
Archive:  bar.zip
 extracting: bar.txt                

In [12]: !ls -l bar.txt
-rw-r--r-- 1 naoki naoki 11 Jun 19 18:23 bar.txt

@methane
klab_gijutsu2 at 18:35|この記事のURLComments(0)TrackBack(0)
Blog内検索
Archives
このブログについて
DSASとは、KLab が構築し運用しているコンテンツサービス用のLinuxベースのインフラです。現在5ヶ所のデータセンタにて構築し、運用していますが、我々はDSASをより使いやすく、より安全に、そしてより省力で運用できることを目指して、日々改良に勤しんでいます。
このブログでは、そんな DSAS で使っている技術の紹介や、実験してみた結果の報告、トラブルに巻き込まれた時の経験談など、広く深く、色々な話題を織りまぜて紹介していきたいと思います。
最新コメント