Windows では2020年を待たずに Python 2.7 が使い物にならなくなっていく

昨日 mysqlclient 1.3.10 をリリースしました。 今までは Windows 版の wheel は Python 2.7 だけに提供していたのですが、 1.3.10 からは 3.5 と 3.6 だけに提供して 2.7 はドロップしました。

そもそも今まで Python 3 に wheel を提供できてなかったのは、 MySQL Connector/C の VC14 (VS2015) に対応したライブラリが提供されておらず、 Python 3.5, 3.6 は VC14 でビルドされていて VC12 用のライブラリにリンクすると大量のエラーでるわ自分で手順読みながら頑張って MySQL をソースからビルドしてもなんか動かないわで諦めてたからです。

それが、2年待て、よーーーやく MySQL Connector/C 6.1.9 から VC14 のライブラリが同梱される用になりました。 GPLだから困ったら自分でなんとかしろと言われればそれまでなんですが、もうちょっと早くできなかったんでしょうか、Oracleさん。。。

一方で、 Python 2.7 は VC9 (VS2008) でビルドされています。しばらく前の(具体的なバージョンは忘れた) MySQL Connector/C から、 vc10, vc11, vc12 用のライブラリしか提供されてなかったのですが、 vc10 と無理やりリンクするとリンクエラーも起こらずちゃんと動いてるっぽかったし、特にライブラリ側でmallocしたメモリをPython側でfreeするみたいな明らかな問題も無かったので、vc9とvc10のランタイムライブラリを両方リンクすることでなんとか wheel を提供できていました。

それが、 Connector/C 6.1.9 からは vc11, vc12, vc14 のライブラリだけが提供されるようになり、 vc10 のライブラリが消えました。 一応、 utf8mb4 に対応した後、 6.1.9 以前のライブラリバージョンを使ってビルドし続けることもできるんですが、もともと気持ちよくない構成でビルドしてたので思い切って Python 2.7 wheel のビルドをやめることにしました。

Windows でバイナリ互換性を維持するには VCRT のバージョンを固定する必要があり、 Python 2.7 は 2008 年の VCRT にずっと依存しています。 Python コア開発者は 2020 年まで Python をメンテしていますが、サードパーティーのライブラリは 2020 年まで 2008 年の VCRT をサポートしてくれるわけではありません。 VC++ 2013 や 2015 に移行したライブラリは、 VC++ 2008 では利用できない機能 (<stdint.h> とか) を喜んで使うでしょう。古いバージョンのライブラリを使い続けるのにも、セキュリティ fix をバックポートする努力をしなければ、安全でないものを配布することになります。 (Python 2.7 サポートを望む声に負けて、安全でないライブラリを配布してるライブラリがすでに存在するかも知れません。というか存在すると思います。)

Windows 向けに binary wheel を提供している開発者は、そろそろ Python 2.7 のサポートのために変に頑張るのを止めて、代わりにユーザーに Python 3 への移行を頑張ってもらうべきだと思います。

WindowsPython 2.7 を使っている開発者は、 pip install が成功するからという理由で Python 2.7 を使い続けるのをやめましょう。 「Python 2.7 サポートのためにバグやセキュリティホールのある古いライブラリが使われてないか」をチェックし続ける努力より、 Python 3 に移行する努力のほうが建設的ですよ。

pip 9.1 から msgpack が使われるようです

Adopt cachecontrol 0.12.0 with msgpack support というコミットがありました。

どうやら CacheControl というのが pip が使っている requests 用のキャッシュライブラリで、その最新版が msgpack を使っているようです。

前のバージョンはバイナリデータを base64 した上で json に入れて gzip していたのですが、もともと圧縮されてるバイナリを扱うときに gzipbase64 によって増えた分を減らす以上の効果は期待できない上、 PyPI からダウンロードするファイルってほぼ100%圧縮済みなので、キャッシュファイルの読み書きで無駄なオーバーヘッドがあったみたいですね。

バンドルされてる msgpack は pure Python で実行できる fallback モジュールのみなのでどこでも動くし、 Cython 版じゃないのも今回みたいにファイルが大きい以外はデータは多くなくて単に base64 + gzip の負荷を減らしたいときは完全に安全だし、まさに msgpack の典型的な成功例 (jsonバイナリ詰めたかったら msgpack!) のように見えます。

ということで、 pip 9.1 がリリースされたら msgpack のユーザーが爆発的に増えそうです。

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

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

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

Hyper Threading

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

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

性能の安定性

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

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

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

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

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

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

その他

Azure VM

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

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

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

EC2

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

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

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

GCE

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

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

結論

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

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

CPython の Core Developer になりました

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

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

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

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

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

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

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

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

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

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

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

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

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

では行きましょう

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

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

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

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

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

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

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

f-string

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

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

pathlib が使いやすくなった

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

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

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

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

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

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

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

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

C99 解禁

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

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

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

高速化

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

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

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

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

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

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