暇人じゃない
——  Technology

「Go 1.27 リリースパーティ」に参加した

8/18 に開催された「Go 1.27 リリースパーティ」に参加した。

Go 1.27 リリースパーティ - connpass

最近は Go を触っていて、Go の情報やコミュニティに触れたくて参加した。

Go 1.27 のリリースノートはこちら: Go 1.27 Release Notes - The Go Programming Language

発表を聞いて、気になった内容について調べた。

メモリ割り当ての最適化

従来はヒープ割り当て時に汎用的なメモリアロケーション関数が呼ばれ、実行時にサイズを判定したり、割り当てサイズに応じたサイズクラスを特定していた。

Go 1.27 からは、コンパイラがヒープメモリ確保を行う際、サイズごとに用意されたアロケーション関数を直接呼び出すコードを生成するようになった。

この変更により、コンパイル時にサイズが確定している 80 バイト未満の割り当てについて、実行時のサイズ計算やサイズクラスの特定が削減されるようになった。

メリット・デメリット

  • 80 バイト未満のメモリ確保のオーバーヘッドが最大で 30% 削減される
  • ヒープ割り当てが頻繁に発生するアプリケーションでは約 1% の改善が見込まれる
  • トレードオフとして、バイナリサイズが一律で約 60 KB 増加する

なぜ 80 バイト未満なのか?

  • Go ランタイムのメモリアロケータは、割り当てサイズごとに定義されたサイズクラスでメモリを管理している
  • 一般的なアプリケーションの構造体、文字列・スライスのヘッダ、クロージャなどは 80 バイト未満のサイズクラスに集中している
  • 当初は 128 バイト以上のサイズクラスも検証されたが、サイズクラスを増やすとバイナリサイズが肥大化し、それに対して得られる性能向上が見合わなかった

GOEXPERIMENT=nosizespecializedmalloc でオプトアウトできるが、Go 1.28 で廃止予定となっている。

詳しく調査している記事があった: Go 1.27 の size-specialized malloc はなぜ速いのか - knwoop 備忘録

uuid パッケージ / UUID v7

標準ライブラリに uuid パッケージが追加された。UUID v4 と v7 をサポートしている。 uuid.New() は UUID v4 を生成する。UUID v7 を生成するには uuid.NewV7() を呼ぶ。

convto さんの発表では UUID v7 の仕様や実装に重点が置かれていた。

UUID v7 の実装

以下は UUID v7 の基本構造。

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           unix_ts_ms                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          unix_ts_ms           |  ver  |       rand_a          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|var|                        rand_b                             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                            rand_b                             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

v7 は時系列でソート可能だが、ミリ秒の精度なので、同じミリ秒内に生成された UUID 同士の順序は保証されない。

そこで、RFC 9562 では同一ミリ秒内で複数の UUID が生成された場合の順序保証について複数のアプローチを規定している。

  • Method 1: 固定長カウンター (Fixed Bit-Length Dedicated Counter)
    • rand_a (12 bit) をカウンター領域とする
  • Method 2: 単調増加ランダム (Monotonic Random)
    • rand_b をランダムな初期値を持つカウンターとして扱い、生成ごとにランダムな整数を加算する
  • Method 3: rand の空間を削って精度を高める (Replace Leftmost Random Bits with Increased Clock Precision)
    • rand_a にサブミリ秒の情報を埋め込む

Go の実装はどうなっているかというと、

  • ベースは Method 3 (rand_a の 12 bit をサブミリ秒の格納に使用)
  • rand_b は毎回ランダム
  • 前回採番した値をプロセス内に保持し、今回の値が前回以下になる場合は前回値 + 1 を採用する
    • sync.Mutex でロックして取得・参照する

シングルノード・シングルプロセス前提なので、マルチノードやマルチプロセス間の順序は保証されない。 また、クロックが大きく巻き戻った場合も同様。

convto さんのブログ記事が詳しくまとめられており、読み応えがある: Go 1.27 から uuid 実装がサポートされる!ので個人的に気になった議論とその着地をまとめてみた

GODEBUG オプション

Go 1 はソースコードレベルの後方互換性を保証している。仕様に則って書かれたコードであれば、将来のバージョンでも無変更でコンパイルが通る。(たまにセキュリティ上の問題や不具合などで破壊的変更が入ることがある)

Go 1.21 で後方互換性に拡張が入り、破壊的な挙動変更を導入する場合は GODEBUG オプションで挙動を戻せるようになった。最低 2 年は維持される。

Go 1.27 でも htmlmetacontenturlescapefips140ems などの設定が追加されたり、gotypesaliasasynctimerchan などいくつかの設定が削除された。

Go のバージョンをアップデートする場合、GODEBUG の設定を使用している箇所がないことを確認する。使うとしても短期かつ最小限に留める。 使わないのがベストという話だった。

終わりに

他にも、目玉のジェネリックメソッドや encoding/json/v2 が標準ライブラリに入ったり、net/http/httptest にインメモリのネットワークで HTTP サーバーを立てられる NewTestServer が追加されたりと、恩恵を受けそうな機能が入った。

Go の勉強会に参加するのは初めてで、勉強会自体も数年ぶりなので緊張した。

久しぶりにオフラインの勉強会に参加したが、オンラインで発表資料を見るだけでは得られないものがあったり、オフラインの雰囲気に良い刺激を受けたりした。 また参加したい。