Edited at

Python 3.7.4 からGC対象オブジェクトのメモリ使用量が8バイト増えるかも

x86-64 の ABI では、 int128 や long double 型は 16byte 境界に配置することになっています。

しかし Python がもっている pymalloc 実装は 8 バイト境界のメモリブロックを返すようになっています。。。大丈夫か?

16byte alignment を要求する型が入った構造体を malloc しようとすると 16byte の倍数になるはずなので、malloc(sizoef(構造体)) ならセーフで、 malloc(sizeof(構造体)+16の倍数じゃないバイト数) みたいなケースでは問題になりそう。

それよりも問題なのがGCヘッダ。64bit環境でのGCヘッダは 24byte です。「ヘッダ」という名前が示すとおり、GC対象オブジェクト(循環参照を構成するかもしれないオブジェクト。tuple,list,dictなど。)の先頭に配置されます。

GC対象オブジェクトを生成するときは、 malloc(sizeof(GCヘッダ) + オブジェクトサイズ) のようにメモリ確保してからGCヘッダ分ずらしたアドレスを PyObject* 型のポインタに利用します。なので malloc が 16byte 境界アドレスを返しても、 +24byte したアドレスに Python オブジェクトが格納されることになり、そのオブジェクトの構造体に int128 や long double 型があると 16byte 境界をまたぐように配置されてしまいます。

最近のコンパイラは実際に 16byte 境界を前提にした命令を利用するようになっているようで、「理論上は違反しているが実際は問題なく動いている」ではなく「本当にセグフォで落ちる」問題になってしまいました。

最近まで気づかなかったのですが、 Python 2.7 で先にこの現象が報告されていて、 2.7.15 でGCヘッダが 24byte から 32byte に増やされていました。 (changelog, commit, issue)

最近 Python 3.7 でも同様の問題が報告された (クラッシュのissue, 昔からあったubsanエラーのissue) ので、ABI互換を破壊しない他の良い方法が見つからない限りは同じ修正を適用するしかなさそうです。 long double とか int128 とかいったレアな型のために、大量にある&それらの型を絶対に入れることがないタプルのメモリ消費を+8バイトしないといけないのは、ケチな私からすると惜しいです。

ところで、Python 3.8 ではGCヘッダの中身を3ワード(64bit環境で24byte)から2ワード(同16byte)に削って、GC対象オブジェクトstr,bytes,int,floatなどは対象外)のメモリ使用量を8バイト削ることに成功していました。そのためこの問題の影響を受けることなく、余計なパディングは必要ありません。結果的に Python 2.7.15 以降や多分 3.7.4 以降に比べると16byteの削減になります。この改善に成功していて本当に良かった。