Matebook 14 (2020)

amzn.to

税込み11万円弱で、AMD Ryzen 7 4800H、RAM 16GB、SSD 512GB、sRGB 100%の3:2液晶はすごいコスパ

Windowsはサブだけどある程度の開発生産性は欲しいという場面にいいと思って購入した。バタフライキーボードになる前のMBP 13 (2015)と比べて、

  • 日本語キー配列はいたって普通。キーボードのストローク、タイプ感も十分で、キートップを触ると若干の凹みも感じられて、十分にタイプしやすい。
  • 液晶解像度はMBPには劣るが、横解像度もFullHDよりも若干高めで、個人的には十分。
  • 低反射コーティングの質はMBPより悪い気がする。輝度を少し高めて使ってるけど、人によってはノングレアのフィルムを貼ったほうが良いかもしれない。
  • 重さは同じはず (1.49kg)
  • バッテリー持ちは(比較対象が古すぎるけど)圧倒的にいい。電源モードを「高パフォーマンス」(最高の1つ下)にして、一日リビングで充電なしで作業できる。
  • 液晶を開けただけではスタンバイから復帰せず、指紋センサー兼電源ボタンを押すことがある。でも復帰後にロック解除のために改めて指紋センサーを押す必要はないので使い勝手は十分にいい。
  • 液晶を開けるときに最初の1センチはいいんだけど、そこから開く力が重くなって下側が浮いてしまうので反対の手で下側を支えないといけないところに、細かいけれどもツメの甘さを感じる。
  • USB Type-C ポートが1つしかなくて、しかも充電がここからしかできないのがネック。

amzn.to

関数アノテーションを軽量化しました

この記事は KLab 2020 Advent Calendar の 12/2 分になります。 qiita.com

最近の Python に対する改善を紹介します。私が設計、コードレビューまでしましたが、実装は他のコントリビューターにしていただきました。 (プルリクエストはこちら)

背景として、Python 3.10 からは from __future__ import annotations がデフォルト化され、アノテーション部分は実行時に評価されずにただの文字列になります。( PEP 563 を参照してください。)

>>> def add(a: int, b: int) -> int:
...     return a+b
...
>>> add.__annotations__
{'a': 'int', 'b': 'int', 'return': 'int'}

アノテーションが実行時に評価されないということは、コンパイル時にアノテーションがすべて計算可能ということです。そこで Python 3.10 からさらなる最適化を導入しました。

まずは Python 3.9 で関数アノテーションがどう実装されていたかをおさらいします。

# a.py
from __future__ import annotations

def add(a: int, b: int) -> int:
    return a + b
$ python3 -m dis a.py
(snip)
  3          12 LOAD_CONST               2 ('int')
             14 LOAD_CONST               2 ('int')
             16 LOAD_CONST               2 ('int')
             18 LOAD_CONST               3 (('a', 'b', 'return'))
             20 BUILD_CONST_KEY_MAP      3
             22 LOAD_CONST               4 (<code object add at 0x7f7c9efac870, file "a.py", line 3>)
             24 LOAD_CONST               5 ('add')
             26 MAKE_FUNCTION            4 (annotations)
             28 STORE_NAME               2 (add)

4つのLOAD_CONST命令と1つのBUILD_CONST_KEY_MAP命令でdictを作って、それをMAKE_FUNCTIONに渡しています。この dict は function オブジェクトに格納されます。

これは関数の定義時に実行される命令なのでプログラムの実行速度への影響は軽微ですが、ループの中で inner function を定義している場合には毎回実行されますし、そうでなくても pyc ファイルを import する時間に多少影響を与える可能性があります。

これが Python 3.10 ではこうなります。

  3          12 LOAD_CONST               9 (('a', 'int', 'b', 'int', 'return', 'int'))
             14 LOAD_CONST               6 (<code object add at 0x7f9e932a7be0, file "a.py", line 3>)
             16 LOAD_CONST               7 ('add')
             18 MAKE_FUNCTION            4 (annotations)
             20 STORE_NAME               2 (add)

アノテーションが1つのタプルにまとめられ、このタプルが dict に変換されずそのまま MAKE_FUNCTION に渡され、関数オブジェクトに保存されるようになりました。これでアノテーション付きの関数を作る速度が数倍速くなります。 https://bugs.python.org/issue42202#msg381320 からマイクロベンチマーク結果を1つだけピックアップします。

def f(a: int, /, b: int, *, c: int, **d: int) -> None: pass

Python 3.9.0
5000000 loops, best of 5: 326 nsec per loop

Python 3.10.0a2+ (from __future__ import annotations がデフォルト化された状態)
5000000 loops, best of 5: 264 nsec per loop

Python 3.10.0a2+ with compact representation
5000000 loops, best of 5: 87.1 nsec per loop

さらにメモリ使用量にも影響があります。このタプルは func.__annotations__ が最初にアクセスされた時に dict に変換されるのですが、静的型チェックやコード補完機能、ドキュメントの目的で付けられた関数アノテーションは実行時には利用されないことが多いので、アクセスされないままならずっとタプルのままです。これでメモリ使用量は4割以下に減らせます。

>>> sys.getsizeof({"a":"int","b":"int","return":"int"})
232
>>> sys.getsizeof(("a","int","b","int","return","int"))
88

おまけに、同じソースファイルの中にある LOAD_CONST で読まれるタプルは、中身が同じであれば1つのインスタンスを使いまわします。 (Python 3.7までは1つのcodeオブジェクト(1つの関数など)内でしか使い回さなかったのですが、これも私が改良していました。 https://github.com/python/cpython/commit/c2e1607a51d7a17f143b5a34e8cff7c6fc58a091)

コードジェネレータに生成されたファイルなどで数千の同じシグネチャの関数がある場合は、今までは関数の数だけ dict オブジェクトを作っていたのが、 Python 3.10 ではたった1つのタプルで済むようになります。これでより気軽に関数アノテーションを使えるようになります。

将来の野望としては、関数アノテーションだけでなく docstring も、実際に利用されてからロードする遅延ロードが実装できたら良いなと思っています。しかしこれには pyc ファイルのフォーマットを完全に刷新する必要があり、しかもモジュールをロードしたあとに pyc ファイルが上書きされたらどうするのかという問題に対する解決策をまだ思いついていないので、 近い将来に実現するのは難しいです。いいアイデアを思いついたら教えて下さい。

ISUCON10予選敗退してきました

例年は同僚と参加していたのですが、今年は予選申し込みが始まってから去年のメンバーに打診したら申し込み締め切りに間に合わなかったために、申し込みに成功してメンバーを募集されていた @catatsuy さんにお願いしてメンバーに入れてもらいました。 @catatusy さんの記事はこちら

結果は1503点で、惜しくもなんともない完敗でした。

序盤

事前に打ち合わせしていた通りに、お互いで共通の ssh_config を作る、除外すべき大きいファイルやディレクトリをチェックしつつ初期コードをコミットする(今回は適切に .gitignore がされていて git add . だけでいけました)、ローカルの開発環境を構築しその手順を共有する、netdata をインストールし netdata cloud に登録する、アクセスログ集計のための alp コマンドのオプションを共有するといったことをしていました。

小さいトラブルはあったものの、ここまではスムーズで良かったと思います。

中盤〜終盤

alp の結果とアクセスログを見比べて、estateとchairの検索が遅いこと、検索クエリのほとんどがシンプル(検索条件が1つ)であることなどを把握しました。

検索は LIMIT X OFFSET Y 型式のページングと、 COUNT(*) 型式の総数表示があります。前者はインデックスでソートすれば早めに打ち切れるので降順インデックスが使える MySQL8 にすることで十分な高速化が見込めますが、 COUNT(*) の方は工夫しないとインデックス全体のスキャンになります。MySQL側で工夫するよりもアプリ側で処理する方が早いと判断してオンメモリ化を始めます。。。。が、ここからドツボにはまります。

最初に簡単な estate の検索から始めたのですが、 CSV入稿に失敗したというエラーや /api/estate/search の結果が違うというエラーに悩まされます。安定してエラーになるのならまだ良いのですが、パスしたりエラーになったりするし、エラーの内容を教えてくれないのもあって迷走しました。

午後7時ごろ、迷子になった出前の配達員を迎えに走っていたときに、元は ORDER BY popularity DESC, id ASC だったのにアプリでは popularity の降順ソートしかしていないことに思い当たります。しかしそれを修正してからもエラーになったりならなかったりは続きます。

最終的に、オンメモリのデータの件数だけなら利用してもエラーにならないと判断し、 SELECT COUNT(*) を削除することにしましたが、スコアはあまり上がりませんでした。

反省点と感想

最近のISUCONだとベンチマーカーが間違っている点を詳しく教えてくれる事が多かったためにベンチマーカーをテストに使うという戦略に依存してしまい、そのためエラーの原因が最後までわからずに迷走してしまいました。せっかくローカルで動作する環境があったので自分でしっかりテストをするべきでした。

ただ、その反省を消化しようとローカルでオンメモリ版とDBを使う版でいくつかの条件でAPIを叩いてみてもAPIが出力するJSONが完全一致したので、いまだにエラーの原因がわかりません。後日ベンチマーカーが公開されたら改めてデバッグしようと思います。

またオンメモリ化も定型的な作業で自分なら楽勝だと信じきってしまっていたのもドツボにハマった原因です。次の機会ではよりシンプルでバグの少ないその他の選択肢(MySQLあるいはアプリでのクエリキャッシュを使うとか)を先に検討しようと思います。

最後になりますが、今年の問題はボリュームがとても小さいのにやらなければならないことは十分に多い良問でした。自分の実力と勉強不足を痛感させられました。ありがとうございました。

PyPIのTOP 4000 パッケージのsdistをダウンロードする

Deprecate済みの機能をそろそろ削除していいかどうか悩んだときに、Githubソースコード検索だとソースコードのコピーが置いてある個人リポジトリがたくさんあったりして役に立ちません。

PyPIのミラーを作って全部のパッケージを確認できればいいのですが面倒なので、とりあえずTOP4000パッケージだけを対象にしてソースパッケージのダウンロードして調査しています。

まずTOP4000パッケージのリストですが、 Top PyPI Packages というサイトから入手できます。BigQueryで自分でリストを作るよりもお手軽です。

hugovk.github.io

つぎにパッケージをダウンロードする方法ですが、 pip download だと --no-deps つけてもビルド依存関係を解決しようとしたりしてうまくいかなかったので、PyPIJSON APIからsdistを見つけてダウンロードするようにしています。

notes/download_sdist.py at master · methane/notes · GitHub

Python/C API を調べたかったのでこのスクリプトは sdist しかダウンロードしていませんが、 Universal wheel しか配布していないパッケージもあるので、Python標準ライブラリの利用状況を調べたい場合は sdist が存在しない場合に wheel をダウンロードするように改修する必要があります。

sdist の拡張子は .tar.gz, .tar.bz2, .zip なので、あとは単にシェルの機能を使って展開するだけです。

他にもいい方法ないかなと思ったら、 Sourcegraph が結構使えそうでした。これは PyEval_ReleaseLock というDeprecateされている関数の利用例をSourcegraphで探す例です。

PyEval_ReleaseLock file:… - Sourcegraph

PEP 8騒動について

今週PEP 8の小さい変更についてMLで騒動が起こってしまいました。

該当のコミットはこれです。

PEP 8: Change requirement to adhere to Standard English (#1470) · python/peps@0c6427d · GitHub

変更点はごくごくシンプルなものです。

- When writing English, follow Strunk and White.
+ Ensure that your comments are clear and easily understandable to other
+ speakers of the language you are writing in.

今まで知らなかったのですが、変更前の "Strunk and White" とは The Elements of Style というすごく有名なライティングに関する本らしいです。

www.amazon.co.jp

この変更は "Strunk and White" を知らない人にとって分かりやすくなるようにする変更で、これだけなら論争が起こることはなかったのですが、コミットログでこの変更がBLMの動きに関連づけられてしまったために論争が起こってしまいました。

Instead of requiring that comments be written in Strunk & White Standard English, require instead that English-language comments be clear and easily understandable by other English speakers.

This accomplishes the same goal without upholding relics of white supremacy. Many native English speakers do not use Standard English as their native dialect, so requiring conformation to Standard English centers whiteness in an inappropriate and unnecessary way, and can alienate and put up barriers for people of color and those whose native dialect of English is not Standard English.

This change is a simple way to correct that while maintaining the original intent of the requirement.

最初の段落はいいのですが、次の段落の冒頭の "This accomplishes the same goal without upholding relics of white supremacy." でまずギョっとします。

White=善/Black=悪というバイアスを強化する用語は、たとえ肌の色を意味していなくても排除しようという運動があり、最近は blacklist/whitelist に対する word policingが大きな話題になりました。そんな背景があったので、 "Struck & White Standard English" の White はもちろん著者の人名なんですが、まさか人名のWhiteすらword policing対象になるの!?と反射的に思ってしまう人がいました。

もちろん人名が問題になっているわけではありません。 Strunk & White Standard English は白人優位な社会で生まれた白人の標準語で、その利用を推奨することは黒人英語(AAVE)などの方言が標準英語ではないんだ、白人英語の方が標準(=優位)なんだというバイアスを助長するのが問題だとされています。とはいえ、コミットコメントでは Strunk & White がなぜ白人優位なのかの説明が抜けているので、まさか名前が問題なの?という誤解の原因になっています。

もう一つ論争になってしまっているのは標準語の利用を推奨するかどうかで、結局 "clear and easily understandable to other speakers of the language you are writing in." を達成するには標準的な一つの方言を使うしかないんですよね。あちこちの英語の方言が混ざると読みにくく、わかりにくくなります。それに英語ネイティブじゃない人に英語の読み書きを強制させることは英語=Standard、英語社会優位のバイアスを強化してしまってることには変わりありません。


PEP 8は本来Pythonの標準ライブラリのためのコーディング規約なので、結局Python開発に参加するときにアメリカの白人英語を使うことに変わりはありません。とはいえPEP 8はPython標準ライブラリに限らず広く使われているので、白人英語に限定した表現をせずに "clear and easily understandable to other speakers of the language you are writing in." という書き方にする変更自体は良いと思います。

とはいえ、このコミッターとGuidoだけで、他のコミッターに説明なくPEP 8が更新されたために、「Whiteっていう人名すらダメなの?」とか「方言使うことを推奨するの?黒人英語だけじゃなくて○○方言は?そもそも英語じゃない言語は?」っていう不要な誤解に基づく論争が生まれてしまいました。

今は git のヒストリを書き換えて、この誤解を招くコミットログをもっと誤解を生まない表現に修正するかどうかが話し合われています。

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