メインコンテンツへスキップ

Go Proposal Weekly Digest

Go言語のproposal更新を毎週お届け

#63999likely_accept

sync/atomic: add Max/Min operators

ステータス変更: active likely_accept

要約

AIによる要約であり、誤りを含む場合があります。

概要

sync/atomicパッケージに、アドレス上の値を指定値との最大値・最小値でアトミックに更新するMax/Min系の演算子を追加する提案です。ランタイム内部のTODOコメントで示唆されていた実装ニーズを一般公開APIとして解放するものです。

ステータス変更

activelikely_accept
2026年7月10日の週次proposal reviewミーティングにて、@aclements により「likely accept」と判定されました。議論では設計上の大きな懸念は見られず、「戻り値は更新前の値(old)とすべきか、更新後の値(new)とすべきか」という論点のみが検討されました。既存のANDやORなどのビット演算アトミック操作と同様、更新前の値さえ分かれば更新後の値は呼び出し側で計算できる(逆は不可能)ため、oldを返す設計が妥当と判断されました。また、ARM64・PPC・RISC-Vのハードウェア命令もいずれも更新前の値を返す仕様であり、CASループによるソフトウェア実装でも自然にoldが得られることから、この方針が支持されました。

技術的背景

現状の問題点

現在のsync/atomicにはビットごとのAND/OR操作(Go 1.23で追加)はありますが、最大値・最小値を求めるアトミック操作は存在しません。そのため、複数のゴルーチンが共有カウンタの最大値・最小値を安全に更新したい場合、以下のようなCAS(Compare-And-Swap)ループを自前で書く必要があります。

// Before: CASループによる手動実装
func atomicMax(addr *int64, val int64) int64 {
    for {
        old := atomic.LoadInt64(addr)
        if val <= old {
            return old
        }
        if atomic.CompareAndSwapInt64(addr, old, val) {
            return old
        }
    }
}

このパターンはruntime/mgcsweep.go内のTODOコメントでも指摘されており、GC内部でも同様の最大値追跡が必要とされています。

提案された解決策

sync/atomicに以下のような関数・メソッドを追加します(内部的にはruntime/internal/atomicにもMax/Max64/Maxuintptr/Min/Min64/Minuintptrが実装されます)。

func MaxInt32(addr *int32, val int32) (old int32)
func MaxInt64(addr *int64, val int64) (old int64)
func MaxUint32(addr *uint32, val uint32) (old uint32)
func MaxUint64(addr *uint64, val uint64) (old uint64)
func MaxUintptr(addr *uintptr, val uintptr) (old uintptr)
func (*Int32) Max(val int32) (old int32)
// ...Int64/Uint32/Uint64/Uintptr も同様
// Minも同様の構成
func MinInt64(addr *int64, val int64) (old int64)
func (*Int64) Min(val int64) (old int64)

対応するアーキテクチャごとのアトミック命令も活用されます。RISC-VにはAMOMAX/AMOMIN、ARM64にはLDSMAX系命令、PPCにはUMAX系命令があり、これらを利用したアセンブリ実装が可能です。一方AMD64にはネイティブな最大値・最小値アトミック命令がないため、CASループによるread-modify-write実装になる見込みです。またレースディテクタ(TSan)には現状fetch_max/fetch_min相当の関数がなく、別途実装が必要になる点も課題として挙げられています。

これによって何ができるようになるか

この機能により、複数ゴルーチンが並行して値を更新する場面で、ロックを使わずに安全かつ簡潔に最大値・最小値を追跡できるようになります。
主なユースケース:

  • 統計収集: 複数のワーカーゴルーチンが処理した入力のうち最大サイズを記録する
  • 最適化処理の早期終了判定: 並列探索において、これまでに達成した最良スコア(最大値)を共有し、見込みのないワーカーを打ち切る
  • ロックフリーなデータ構造の実装: 排他ロックなしで最大・最小トラッキングを行う共有データ構造の構築
  • ランタイム内部での活用: GC(mgcsweep.go)における統計値の更新など

コード例

// Before: CASループによるワークアラウンド
func recordMax(addr *int64, val int64) {
    for {
        old := atomic.LoadInt64(addr)
        if val <= old {
            return
        }
        if atomic.CompareAndSwapInt64(addr, old, val) {
            return
        }
    }
}
// After: 新APIを使った書き方
var maxSeen atomic.Int64
maxSeen.Max(val) // valがmaxSeenより大きければ更新、常にスレッドセーフ

議論のハイライト

  • 提案者は、C++標準ライブラリ(libstdc++)における同種のproposal(fetch_max/fetch_min)を引用し、既に他言語でも同様の需要が認識されていることを示した
  • 既に受理済みのAND/OR演算子の提案と設計思想が類似しており、前例に沿った拡張として自然に受け入れられた
  • @adonovan からARM64(LDSMAX)、PPC(UMAX)など各アーキテクチャの対応命令に関する情報提供があり、AMD64のみネイティブ命令がなくCASループでの実装が必要になることが確認された
  • レビュー会議での唯一の論点は戻り値の仕様(old vs new)で、ハードウェア命令の挙動とAPIの一貫性(新しい値は呼び出し側で計算可能)から「old値を返す」設計が支持された
  • レースディテクタ(TSan)にfetch_max/fetch_min相当の機能がない点は今後の実装課題として認識されている

関連リンク