Python 3.12 から Unicode のサイズが小さくなります

Python 3.11 までは、空文字でも64バイトのメモリを使用していました。(64bitプラットフォームの場合)

Unicodeの内部表現のうち一番小さい PyASCIIObject 構造体が48バイトで、その構造体の後ろにASCII文字列が続きます。その文字列はNUL終端されているので、空文字列でも1バイト追加されて49バイトになります。

>>> sys.getsizeof("")
49

さらに小さいメモリブロックのアロケートをしているpymallocがメモリを(アライメントの関係で)16バイト単位で割り当てるので、49バイトのmallocでも64バイトが確保されてしまいます。

Python 3.12 からは、PyASCIIObject構造体から wchar_t* 表現をキャッシュするポインタが消え、40バイトになりました。それでASCIIで7文字までの文字列であれば48バイトに収まるようになりました。

>>> import sys
>>> for i in range(10):
...   print(i, sys.getsizeof("a"*i))
...
0 41
1 42
2 43
3 44
4 45
5 46
6 47
7 48
8 49
9 50

オブジェクトの名前として頻出する7文字以下のASCII文字列のメモリ消費量が、64バイトから48バイトへと、16バイト削減できました。

副作用として、同一の文字列を頻繁にWindowsAPIに渡す場合に、 wchar_t* 表現を毎回生成しないといけないのでオーバーヘッドが発生します。しかし Windows API に渡す文字列はごく一部しかないですし、Unixだとほぼ wchar_t* は出番がないので、ほとんどの場面ではメリットの方が圧倒的に大きいです。

この変更を実現するために、2年前に4000弱のPyPIのパッケージをダウンロードして deprecated な API の利用状況を調査した上でPEPを書いていました。3.11に入れられなかったのは、C APIレベルの非互換性のためにPEPで3.12をターゲットにしていたからです。

peps.python.org

methane.hatenablog.jp

まだ3.11がベータになったばかりですが、来年末の3.12にも期待してください。

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