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

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

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

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

github.com

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

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

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

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

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

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

計測結果は、

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

をつけて、

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

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

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

manylinux1 wheel を作ってみる

先日の記事 で紹介した、 manylinux1 wheel を作ってみます。

manylinux1 docker image

ビルド環境を Docker image として公開してくれています。

docker pull quay.io/pypa/manylinux1_x86_64; docker pull quay.io/pypa/manylinux1_i686 しておきましょう。

なお、このイメージは CentOS 5 をベースにビルドスクリプトを実行しているだけなので、 vagrant 等で同じ環境を作るのは簡単そうです。

作業ディレクトリを作って docker 内で bash を起動します。

$ mkdir docker
$ docker run --rm -ti -v `pwd`/docker:/docker  -w /docker quay.io/pypa/manylinux1_x86_64 bash

このように実行すると、bash を抜けるときにイメージが消えますが、ボリュームに指定した docker ディレクトリの内容は残るので便利です。

wheel をビルドしてみる

/opt/python 配下に各種 Python が用意されています。 Python 2.6 と 2.7 は、 utf-16 の narrow build (cp26m, cp27m) と utf-32 の wide build (cp26mu, cp27mu) があります。

[root@c3f621332eef docker]# ls /opt/python/
cp26-cp26m  cp26-cp26mu  cp27-cp27m  cp27-cp27mu  cp33-cp33m  cp34-cp34m  cp35-cp35m

試しに msgpack の最新リリース版をビルドしてみます。

[root@89ab8fbbfbc0 docker]# curl 'https://pypi.python.org/packages/a3/fb/bcf568236ade99903ef3e3e186e2d9252adbf000b378de596058fb9df847/msgpack-python-0.4.7.tar.gz' -o msgpack-python-0.4.7.tar.gz
[root@89ab8fbbfbc0 docker]# tar xf msgpack-python-0.4.7.tar.gz
[root@89ab8fbbfbc0 docker]# cd msgpack-python-0.4.7
[root@89ab8fbbfbc0 msgpack-python-0.4.7]# /opt/python/cp35-cp35m/bin/python setup.py bdist_wheel
...
[root@89ab8fbbfbc0 msgpack-python-0.4.7]# /opt/python/cp34-cp34m/bin/python setup.py bdist_wheel
...
[root@89ab8fbbfbc0 msgpack-python-0.4.7]# /opt/python/cp27-cp27m/bin/python setup.py bdist_wheel
...

[root@89ab8fbbfbc0 msgpack-python-0.4.7]# rm -rf build
[root@89ab8fbbfbc0 msgpack-python-0.4.7]# /opt/python/cp27-cp27mu/bin/python setup.py bdist_wheel

Python 2.7 や 2.6 のビルド時は、 rm -rf build をしておかないと、 narrow / wide ビルドが違っても build/ ディレクトリ配下にできるファイルにはタグがなくて再利用されてしまうので注意が必要です。

これで dist ディレクトリに、通常の linux 用 wheel ができます。しかしこのままでは PyPI にはアップロードできません。通常の linux wheel は、ローカルや同じ環境のマシンで使い回すためにしか使えません。

manylinux1 wheel に変換する

Docker イメージには auditwheel コマンドが用意されています。このコマンドで、 wheel 内のバイナリが、決められた以外の外部ライブラリに依存してないかなどのチェックが行われます。 (auditwheel も manylinux1 もまだ新しいツールなので、チェックが通ったから安全とは限りません。たとえば上で説明した narrow / wide ビルドミスは現状ではチェックが漏れています。)

auditwheel show コマンドでチェック結果が表示され、 auditwheel repair コマンドは依存してる外部ライブラリをバンドルして RPATH を設定するなどの黒魔術を施して linux wheel を manylinux1 wheel にリネームしてくれます。 repair コマンドは wheelhouse ディレクトリに wheel を生成します。

(repair コマンドの黒魔術が必要ない場合は、 上の手順で bdist_wheel するときに -p manylinux1_x86_64 オプションを使えば良さそうです。)

[root@0018d7cfd7df dist]# auditwheel show msgpack_python-0.4.7-cp27-cp27m-linux_x86_64.whl

msgpack_python-0.4.7-cp27-cp27m-linux_x86_64.whl is consistent with
the following platform tag: "manylinux1_x86_64".

The wheel references the following external versioned symbols in
system-provided shared libraries: GLIBC_2.2.5.

The following external shared libraries are required by the wheel:
{
    "libc.so.6": "/lib64/libc-2.5.so",
    "libgcc_s.so.1": "/lib64/libgcc_s-4.1.2-20080825.so.1",
    "libm.so.6": "/lib64/libm-2.5.so",
    "libpthread.so.0": "/lib64/libpthread-2.5.so",
    "libstdc++.so.6": "/usr/lib64/libstdc++.so.6.0.8"
}


[root@c89bce940544 dist]# for i in *.whl; do auditwheel repair $i; done
Repairing msgpack_python-0.4.7-cp27-cp27m-linux_x86_64.whl
Previous filename tags: linux_x86_64
New filename tags: manylinux1_x86_64
Previous WHEEL info tags: cp27-cp27m-linux_x86_64
New WHEEL info tags: cp27-cp27m-manylinux1_x86_64

Fixed-up wheel written to /docker/msgpack-python-0.4.7/dist/wheelhouse/msgpack_python-0.4.7-cp27-cp27m-manylinux1_x86_64.whl
Repairing msgpack_python-0.4.7-cp27-cp27mu-linux_x86_64.whl
Previous filename tags: linux_x86_64
New filename tags: manylinux1_x86_64
Previous WHEEL info tags: cp27-cp27mu-linux_x86_64
New WHEEL info tags: cp27-cp27mu-manylinux1_x86_64

Fixed-up wheel written to /docker/msgpack-python-0.4.7/dist/wheelhouse/msgpack_python-0.4.7-cp27-cp27mu-manylinux1_x86_64.whl
Repairing msgpack_python-0.4.7-cp34-cp34m-linux_x86_64.whl
Previous filename tags: linux_x86_64
New filename tags: manylinux1_x86_64
Previous WHEEL info tags: cp34-cp34m-linux_x86_64
New WHEEL info tags: cp34-cp34m-manylinux1_x86_64

Fixed-up wheel written to /docker/msgpack-python-0.4.7/dist/wheelhouse/msgpack_python-0.4.7-cp34-cp34m-manylinux1_x86_64.whl
Repairing msgpack_python-0.4.7-cp35-cp35m-linux_x86_64.whl
Previous filename tags: linux_x86_64
New filename tags: manylinux1_x86_64
Previous WHEEL info tags: cp35-cp35m-linux_x86_64
New WHEEL info tags: cp35-cp35m-manylinux1_x86_64

Fixed-up wheel written to /docker/msgpack-python-0.4.7/dist/wheelhouse/msgpack_python-0.4.7-cp35-cp35m-manylinux1_x86_64.whl

PyPI にアップロード

docker で実行していた bash を抜けると、 docker イメージは消えますが、ボリュームの中に wheel が残っています。これを twine を使ってアップロードします。

$ pip install twine
$ cd docker/msgpack-python-0.4.7/dist/wheelhouse/
$ ls
msgpack_python-0.4.7-cp27-cp27m-manylinux1_x86_64.whl  msgpack_python-0.4.7-cp34-cp34m-manylinux1_x86_64.whl
msgpack_python-0.4.7-cp27-cp27mu-manylinux1_x86_64.whl msgpack_python-0.4.7-cp35-cp35m-manylinux1_x86_64.whl
$ twine upload *.whl
Uploading distributions to https://pypi.python.org/pypi
Uploading msgpack_python-0.4.7-cp27-cp27m-manylinux1_x86_64.whl
Uploading msgpack_python-0.4.7-cp27-cp27mu-manylinux1_x86_64.whl
Uploading msgpack_python-0.4.7-cp34-cp34m-manylinux1_x86_64.whl
Uploading msgpack_python-0.4.7-cp35-cp35m-manylinux1_x86_64.whl

あとは同じ手順を i686 の方の docker でもすれば x86 対応も簡単そうです。

終わりに

msgpack は libstdc++ に依存してしまっていますが、これは PEP 513 で定義されている Core shared library に含まれているので、多分問題ないと思います。

一方 mysqlclient-python の場合、 libmysqlclient 経由で ssl などに依存していて、これは core shared library に含まれていないので、 static link や bundle が必要になります。特に ssl のようにセキュリティアップデートが頻繁にあるライブラリの場合は wheel 提供者が責任をもって追随アップデートする必要がありそうで面倒です。

Core shared library に含まれていないライブラリを利用している場合は、 PEP 513 を良く読んで、ちゃんと理解できないのであれば、 manylinux1 wheel の提供は危険です。 wheel builders という ML ができたので、そこで相談してみるのが良いと思います。

Wheel-builders Info Page

追記: sdist から wheel を作るより簡単な手順

上の手順では sdist の tar.gz を展開してから setup.py を実行していましたが、 pip wheel コマンドを使うとこの流れを自動化できます。 作業ディレクトリも毎回作り直されるので、 build ディレクトリを消し忘れて narrow / wide 互換性問題を起こすことも無いはずです。

bash-3.2# for i in cp35-cp35m cp34-cp34m cp27-cp27m cp27-cp27mu; do /opt/python/$i/bin/pip wheel --build-option='-pmanylinux1_i686' msgpack-python-0.4.7.tar.gz ; done;

/opt/_internal/cpython-3.5.1/lib/python3.5/site-packages/pip/commands/wheel.py:126: UserWarning: Disabling all use of wheels due to the use of --build-options / --global-options / --install-options.
  cmdoptions.check_install_build_global(options)
Processing ./msgpack-python-0.4.7.tar.gz
Building wheels for collected packages: msgpack-python
  Running setup.py bdist_wheel for msgpack-python ... done
  Stored in directory: /docker
Successfully built msgpack-python
...
...
Successfully built msgpack-python
bash-3.2# ls -la
total 1108
drwxr-xr-x  1 1000 ftp     238 Apr 27 08:52 .
drwxr-xr-x 39 root root   4096 Apr 27 08:46 ..
-rw-r--r--  1 1000 ftp  241090 Apr 27 08:51 msgpack_python-0.4.7-cp27-cp27m-manylinux1_i686.whl
-rw-r--r--  1 1000 ftp  241106 Apr 27 08:52 msgpack_python-0.4.7-cp27-cp27mu-manylinux1_i686.whl
-rw-r--r--  1 1000 ftp  261276 Apr 27 08:51 msgpack_python-0.4.7-cp34-cp34m-manylinux1_i686.whl
-rw-r--r--  1 1000 ftp  256676 Apr 27 08:51 msgpack_python-0.4.7-cp35-cp35m-manylinux1_i686.whl
-rw-r--r--  1 1000 ftp  126251 Apr 27 06:52 msgpack-python-0.4.7.tar.gz

ここまで行けば、 docker で bash を実行する代わりに、 wheel をビルドするスクリプトを直接実行できそうですね。

文字列の先頭への追加に対する最適化を期待していいか?

paulownia.hatenablog.com

文字列型が immutable な言語では、文字列にループで文字を追加していくようなコードは、 Java でいえば StringBuilder などの方法を使えというのが昔から言われていた。しかし、 Java でも Python でも、文字列の右側に追加していくコードは最適化されるようになっており、現代では処理系の最適化を前提に出来るなら、読みやすくなる方が良いという風に常識が変わってきていると思う。

しかし、今回クソコード呼ばわりした left-pad のループは、文字列の左側への追加だ。静的型を元にコンパイル時に最適化出来る言語ならともかく、実行時に振る舞いを見ながら最適化するような言語で、処理系が文字列の先頭への追加を最適化してくれることを前提にして良いものだろうか?少なくとも Python はそこまではやってくれない。node.jsではどうだろう?

追記:記事を公開した時に書いていたコードにバグがあり、結論が180度変わりました!

'use strict';

function leftpad1(str, len, ch) {
  //str = String(str);

  var i = -1;

  if (!ch && ch !== 0) ch = ' ';

  len = len - str.length;

  while (++i < len) {
    str = ch + str;
  }

  return str;
}

function leftpad2(str, len, ch) {
  str = String(str);
  if (!ch && ch !== 0) ch = ' ';

  len -= str.length;
  if (len < 1) {
    return str;
  }
  var pad = "";
  for (var i=0; i<len; i++) {
    pad += ch;
  }
  return pad + str
}

function leftpad_repeat(str, len, ch) {
  str = String(str);
  ch = String(ch);
  if (!ch && ch !== 0) ch = ' ';

  len -= str.length;
  if (len < 1) {
    return str;
  }
  return ch.repeat(len) + str;
}

const count = 1000000;

for (var len=4; len<=128; len*=2) {
  console.log("pad length", len);

  console.log("leftpad1", leftpad1('aaaa', len, 0));
  console.log("leftpad2", leftpad2('aaaa', len, 0));
  console.log("repeat  ", leftpad_repeat('aaaa', len, 0));

  console.time('leftpad1');
  for (let i = 0; i < count; i++) {
    leftpad1('aaaa', len, 0);
  }
  console.timeEnd('leftpad1');

  console.time('leftpad2');
  for (let i = 0; i < count; i++) {
    leftpad2('aaaa', len, 0);
  }
  console.timeEnd('leftpad2');

  console.time('leftpad_repeat');
  for (let i = 0; i < count; i++) {
    leftpad_repeat('aaaa', len, 0);
  }
  console.timeEnd('leftpad_repeat');
}

結果 [ms]

padding len 4 8 16 32 64 128
left-pad 21 118 265 491 959 1755
左追加除去版 19 81 273 483 875 1623
repeat 38 98 145 171 197 215

結論: やはり左追加を最適化してくれることを前提にするのは間違っている。

結論: 元の実装でも十分速い。JS処理系の文字列処理最適化は相当賢い。クソコードとか言ってごめんなさい。

Wheel が Linux でもバイナリパッケージに対応しました

PEP 0513 -- A Platform Tag for Portable Linux Built Distributions | Python.org

今まで WindowsMac では、ビルド済みのバイナリ形式の拡張モジュールを wheel にして配布することができました。

WindowsMacに比べてLinuxは環境の差が激しいのでバイナリ wheel に対応していなかったのですが、有名なディストリであればだいたい動くようにするためにどうすればいいか (glibc の古いAPIしか使わないなど) を勧告としてまとめて、十分実用的だと判断されたようです。(ただしこれは勧告であって、実際に pip や PyPI でこのルールに則っているかチェックされるわけではないようです)

manylinux1 policy

性質上、何らかのライブラリのバインディングを提供する拡張モジュール (例えば libxml を使う lxml など) で利用するのは難しそうですが、スピードアップのために重い処理を拡張モジュール化しているケースでは利用しやすいはずです。

特に今 Python を盛り上げているデータ・計算系は、ユーザーが専門のプログラマとは限らず、CやC++が難しいからPythonを使ってる人も多いので、インストールするのに必要な(トラブルシュートにCの知識を要求する)ステップが不要になるのはとても良いことです。

個人的に期待しているのは、今までインストールのしやすさのためにできるかぎりC言語オンリーで書いていた拡張が、RustやGoなどで書いて配布しやすくなることです。ヘッダーオンリーの pybind11 を使って C++ も使えるかも。

とりあえず PEP をよく読んで、自分の作ってるライブラリがこの勧告に準拠できるかよく確認するところから始めましょう。

依存するパッケージは厳選しよう

japan.zdnet.com

JS界隈が大騒ぎになった事件だけど、こういった事件自体は完全に防ぐことは不可能だと思う。

今回は依存ライブラリが削除されるだけで済んだけど、 npm install するだけで ~/.ssh ディレクトリを zip にしてどこかに送信するような悪質な攻撃であれば、単にCIが止まるどころでなく、世界中のエンジニアの秘密鍵がばらまかれてあちこちのサーバーにssh可能な事態になったわけで、そんな悪質な攻撃を bugfix なマイクロバージョンアップとして公開される事もありえたわけだ。

第三者のパッケージに依存するということは、それだけのリスクを背負い込むということだ。だが、逆に外部のライブラリに依存しないようにすると、生産性が落ちてしまう。 なので、コードを読む、信頼できるメンテナの公開しているパッケージを選ぶなどといった方法で、リスクとメリットのバランスを取って行かないといけない。

信頼できるパッケージを選ぶ手段の一つとして、既に信頼を集めている人やプロジェクトが信頼しているものを信頼するという方法がある。例えば Babel を使うことを選択した場合、 Babel が依存してる全部のパッケージも信頼して自分のマシンにインストールするということを選んだ訳で、なら自分のプロジェクトでも同じパッケージに依存するというのは合理的な選択のはずだ。

さて、今回の問題で驚いたのは、大混乱を招いた left-pad が、ある程度の技術があるプログラマが見たら一目でクソコードだと判断するような代物だったことだ。

  len = len - str.length;

  while (++i < len) {
    str = ch + str;
  }

渡された文字列 str に、 ch で指定されたパディング文字を左に追加することで、指定された長さの文字列にする(右寄せする)コードなのだが、「文字列の左側に1文字ずつ追加する」コードがクソなのは多くの言語で共通の認識だろう。実際、事件の後、パフォーマンスを改善するプルリクエストが4件も立て続けに来ている。 https://github.com/azer/left-pad/pulls

問題は、たった17行のコードに、こんなに明らかな問題があるのに、事件が起こるまで誰にも気づかれずに、 Babel を始めとした大量のプロジェクトで利用されていたことだ。 僕は JSer じゃないので憶測だが、この開発者がJS界で超有名人で信頼されていたというよりも、「Babelも使ってるし」といった感じでどんどん依存が広まっていったのでは無いだろうか? Babel の中の人はそこまで考えて依存するライブラリを選んでいただろうか。(Babelよりも先に left-pad に依存していた有名プロジェクトがあったらごめんなさい)

外部のパッケージに依存することには大きなリスクが有ることを認識した上で、それでも利用することで得られるメリットが上回っているか、良く考えよう。

特に多くの人に利用されるOSSプロジェクトでは、そのメリット・デメリットがそのプロジェクトだけでなくコミュニティ全体に「◯◯が使ってるからウチも使おう」という基準で広まることまで考えて、より厳選しよう。

できれば依存するライブラリのコードを読んで、他人におすすめできるコードかどうか確かめ、そうでないならより良い別のライブラリを探すなり、プルリクエストを出すなりしよう。

あわせて読みたい: postd.cc

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