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│Comments(0)TrackBack(0)Python 

トラックバックURL

この記事にコメントする

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