[インデックス 17128] ファイルの概要
このコミットは、Go言語のnet
パッケージにおけるファイルディスクリプタ(netFD
)の同期メカニズムを改善し、ネットワーク操作のパフォーマンスを向上させることを目的としています。具体的には、既存のsync.Mutex
ベースのロックを、fdMutex
という特殊なミューテックスに置き換えることで、ロックのオーバーヘッドを削減しています。
コミット
commit 23e15f72538381dab83d02b3bf543cf95230d3e8
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Fri Aug 9 21:43:00 2013 +0400
net: add special netFD mutex
The mutex, fdMutex, handles locking and lifetime of sysfd,
and serializes Read and Write methods.
This allows to strip 2 sync.Mutex.Lock calls,
2 sync.Mutex.Unlock calls, 1 defer and some amount
of misc overhead from every network operation.
On linux/amd64, Intel E5-2690:
benchmark old ns/op new ns/op delta
BenchmarkTCP4Persistent 9595 9454 -1.47%
BenchmarkTCP4Persistent-2 8978 8772 -2.29%
BenchmarkTCP4ConcurrentReadWrite 4900 4625 -5.61%
BenchmarkTCP4ConcurrentReadWrite-2 2603 2500 -3.96%
In general it strips 70-500 ns from every network operation depending
on processor model. On my relatively new E5-2690 it accounts to ~5%
of network op cost.
Fixes #6074.
R=golang-dev, bradfitz, alex.brainman, iant, mikioh.mikioh
CC=golang-dev
https://golang.org/cl/12418043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/23e15f72538381dab83d02b3bf543cf95230d3e8
元コミット内容
net: add special netFD mutex
fdMutex
というミューテックスは、sysfd
のロックとライフタイムを管理し、Read
およびWrite
メソッドを直列化します。これにより、すべてのネットワーク操作からsync.Mutex.Lock
呼び出し2回、sync.Mutex.Unlock
呼び出し2回、defer
1回、およびその他の雑多なオーバーヘッドを削減できます。
Linux/amd64、Intel E5-2690でのベンチマーク結果:
BenchmarkTCP4Persistent
: 9595 ns/op -> 9454 ns/op (-1.47%)BenchmarkTCP4Persistent-2
: 8978 ns/op -> 8772 ns/op (-2.29%)BenchmarkTCP4ConcurrentReadWrite
: 4900 ns/op -> 4625 ns/op (-5.61%)BenchmarkTCP4ConcurrentReadWrite-2
: 2603 ns/op -> 2500 ns/op (-3.96%)
一般的に、プロセッサモデルに応じて、すべてのネットワーク操作から70〜500ナノ秒を削減します。比較的新しいE5-2690では、ネットワーク操作コストの約5%に相当します。
Issue #6074を修正します。
変更の背景
Go言語のネットワーク操作において、ファイルディスクリプタ(netFD
)へのアクセスは、複数のゴルーチンからの同時アクセスを防ぐために同期メカニズムを必要とします。従来、この同期にはsync.Mutex
が使用されていました。しかし、sync.Mutex
は汎用的なロックメカニズムであり、そのオーバーヘッドがネットワーク操作のパフォーマンスに影響を与えていました。特に、Read
やWrite
のような頻繁に呼び出される操作では、ロックの取得と解放にかかるコストが無視できないレベルになっていました。
このコミットの背景には、以下の課題がありました。
sync.Mutex
のオーバーヘッド: ネットワーク操作ごとにsync.Mutex.Lock
とUnlock
が複数回呼び出され、defer
による遅延実行も加わることで、パフォーマンスのボトルネックとなっていました。- ファイルディスクリプタのライフタイム管理:
netFD
のsysfd
(システムファイルディスクリプタ)のライフタイム管理と、それに対する参照カウント、そしてクローズ処理の同期が複雑でした。 - 競合状態の回避: 複数のゴルーチンが同時に
netFD
のRead
やWrite
操作を行おうとした際に、データ競合や不正な状態に陥ることを防ぐ必要がありました。
これらの課題を解決し、特にネットワークI/Oのパフォーマンスを向上させるために、より特化した軽量な同期プリミティブの導入が検討されました。
前提知識の解説
このコミットを理解するためには、以下の概念について基本的な知識が必要です。
- Go言語の並行性: Go言語はゴルーチンとチャネルを用いた並行プログラミングをサポートしています。複数のゴルーチンが同時に実行されるため、共有リソースへのアクセスには同期メカニズムが必要です。
- ミューテックス (Mutex): ミューテックスは、共有リソースへのアクセスを排他的に制御するための同期プリミティブです。Go言語では
sync.Mutex
が提供されており、Lock()
とUnlock()
メソッドで排他制御を行います。 - アトミック操作 (Atomic Operations): アトミック操作は、複数のCPU命令からなる一連の操作が、中断されることなく単一の不可分な操作として実行されることを保証するものです。Go言語では
sync/atomic
パッケージが提供されており、atomic.LoadUint64
やatomic.CompareAndSwapUint64
などの関数があります。これらは、ロックを使用せずに共有変数を安全に操作するために使用されます。 - セマフォ (Semaphore): セマフォは、リソースへのアクセスを制御するための同期プリミティブです。Go言語のランタイム内部では、ゴルーチンのスケジューリングや同期のためにセマフォが使用されることがあります。
runtime_Semacquire
やruntime_Semrelease
は、Goランタイムが提供するセマフォ操作関数です。 - ファイルディスクリプタ (File Descriptor - FD): Unix系システムにおいて、ファイルやソケットなどのI/Oリソースを識別するために使用される整数値です。Go言語の
net
パッケージでは、ネットワーク接続を抽象化するためにnetFD
構造体が使用され、その内部でシステムファイルディスクリプタ(sysfd
)を管理しています。 defer
文: Go言語のdefer
文は、関数がリターンする直前に指定された関数を実行するものです。リソースの解放やロックの解除など、クリーンアップ処理によく使用されます。しかし、defer
にもわずかながらオーバーヘッドがあります。- ベンチマーク: ソフトウェアの性能を測定するためのテストです。Go言語では
testing
パッケージにベンチマーク機能が組み込まれており、go test -bench=.
などで実行できます。
技術的詳細
このコミットの核心は、net
パッケージにfdMutex
という新しい同期プリミティブを導入したことです。fdMutex
は、sync.Mutex
の代わりに、アトミック操作とセマフォを組み合わせて実装された、より軽量で特化したミューテックスです。
fdMutex
の構造と状態管理
fdMutex
構造体は、uint64
型のstate
フィールドを中心に構成されています。このstate
フィールドはビットフィールドとして扱われ、複数の状態情報を同時に保持します。
type fdMutex struct {
state uint64
rsema uint32 // セマフォ: 読み込み待機ゴルーチン用
wsema uint32 // セマフォ: 書き込み待機ゴルーチン用
}
state
フィールドの各ビットは以下の意味を持ちます。
mutexClosed
(1 bit):netFD
がクローズされているかどうかを示します。このビットがセットされている場合、それ以降のロック操作は失敗します。mutexRLock
(1 bit): 読み込み操作のためのロックが取得されているかどうかを示します。mutexWLock
(1 bit): 書き込み操作のためのロックが取得されているかどうかを示します。mutexRef
(20 bits): 参照カウント。読み込み、書き込み、その他の操作による参照の総数を示します。mutexRWait
(20 bits): 読み込みロックを待機しているゴルーチンの数。mutexWWait
(20 bits): 書き込みロックを待機しているゴルーチンの数。
これらのビットフィールドを操作するために、atomic
パッケージの関数(atomic.LoadUint64
, atomic.CompareAndSwapUint64
)が使用されます。これにより、ロックを使用せずにstate
フィールドを安全に更新できます。
fdMutex
の主要メソッド
Incref()
:netFD
への参照カウントをインクリメントします。netFD
がクローズされていない場合にのみ成功し、true
を返します。Decref()
:netFD
への参照カウントをデクリメントします。参照カウントが0になり、かつnetFD
がクローズされている場合(つまり、完全に解放可能になった場合)にtrue
を返します。IncrefAndClose()
:netFD
をクローズ済みとしてマークし、参照カウントをインクリメントします。同時に、待機中の読み込み/書き込みゴルーチンをすべてアンブロックします。RWLock(read bool)
: 読み込み(read=true
)または書き込み(read=false
)のためのロックを取得します。ロックが利用可能であればすぐに取得し、そうでなければセマフォを使って待機します。RWUnlock(read bool)
: 読み込み(read=true
)または書き込み(read=false
)のためのロックを解放し、参照カウントをデクリメントします。待機中のゴルーチンがいれば、そのうちの1つをアンブロックします。
これらのメソッドは、sync.Mutex
のような汎用的なロックではなく、netFD
の特定のライフサイクルとI/O操作の特性に合わせて最適化されています。特に、RWLock
とRWUnlock
は、読み込みと書き込みの排他制御を効率的に行い、同時に参照カウントも管理します。
netFD
の変更
netFD
構造体は、従来のsysmu
(sync.Mutex
)、sysref
(参照カウント)、closing
(クローズ状態フラグ)、rio
(sync.Mutex
for read I/O)、wio
(sync.Mutex
for write I/O) といったフィールドを削除し、代わりにfdmu fdMutex
フィールドを導入しました。
これにより、netFD
のライフタイム管理、参照カウント、および読み書き操作の同期がすべてfdMutex
によって一元的に行われるようになりました。
incref()
、decref()
、Close()
、shutdown()
などのライフサイクル関連のメソッドは、fdmu.Incref()
、fdmu.Decref()
、fdmu.IncrefAndClose()
などを呼び出すように変更されました。Read()
、Write()
、ReadFrom()
、WriteTo()
、accept()
などのI/O操作メソッドは、fd.rio.Lock()
/fd.rio.Unlock()
やfd.wio.Lock()
/fd.wio.Unlock()
の代わりに、fd.readLock()
/fd.readUnlock()
やfd.writeLock()
/fd.writeUnlock()
を呼び出すように変更されました。これらの新しいロックメソッドは内部でfdmu.RWLock()
とfdmu.RWUnlock()
を使用します。
パフォーマンスへの影響
コミットメッセージに記載されているベンチマーク結果が示すように、この変更によりネットワーク操作のパフォーマンスが向上しました。
sync.Mutex
のロック/アンロック呼び出しの削減:fdMutex
はアトミック操作とセマフォを直接使用するため、sync.Mutex
の内部処理(システムコールやコンテキストスイッチなど)を回避できる場合があります。defer
の削減:defer
は便利ですが、実行時にわずかなオーバーヘッドがあります。fdMutex
の導入により、一部のdefer
が不要になり、そのオーバーヘッドも削減されました。- 特化された同期:
fdMutex
はnetFD
の特定のユースケース(参照カウント、読み書きの排他制御、クローズ処理)に特化して設計されているため、汎用的なsync.Mutex
よりも効率的です。
これらの最適化により、ネットワーク操作あたりのレイテンシが70〜500ナノ秒削減され、特にIntel E5-2690プロセッサでは約5%の性能向上が見られました。
コアとなるコードの変更箇所
このコミットで追加・変更された主要なファイルは以下の通りです。
-
src/pkg/net/fd_mutex.go
(新規追加):fdMutex
構造体とその定数(mutexClosed
,mutexRLock
,mutexWLock
,mutexRef
など)が定義されています。Incref()
,IncrefAndClose()
,Decref()
,RWLock(read bool)
,RWUnlock(read bool)
といったfdMutex
の主要なメソッドが実装されています。これらのメソッドはsync/atomic
パッケージとGoランタイムのセマフォ関数(runtime_Semacquire
,runtime_Semrelease
)を使用しています。
-
src/pkg/net/fd_mutex_test.go
(新規追加):fdMutex
の機能と堅牢性を検証するための単体テストが記述されています。TestMutexLock
,TestMutexClose
,TestMutexCloseUnblock
,TestMutexPanic
,TestMutexStress
などのテストケースが含まれています。
-
src/pkg/net/fd_unix.go
:netFD
構造体からsysmu
,sysref
,closing
,rio
,wio
といったフィールドが削除され、代わりにfdmu fdMutex
が追加されました。incref()
,decref()
,Close()
などのメソッドが、fdmu
を使用するように変更されました。readLock()
,readUnlock()
,writeLock()
,writeUnlock()
という新しいヘルパーメソッドが追加され、これらが内部でfdmu.RWLock()
とfdmu.RWUnlock()
を呼び出すようになりました。Read()
,Write()
,ReadFrom()
,WriteTo()
,ReadMsg()
,WriteMsg()
,accept()
などのI/O操作メソッドが、fd.rio.Lock()
/fd.rio.Unlock()
やfd.wio.Lock()
/fd.wio.Unlock()
の代わりに、新しく追加されたreadLock()
/readUnlock()
やwriteLock()
/writeUnlock()
を使用するように変更されました。
-
src/pkg/net/fd_windows.go
:fd_unix.go
と同様に、netFD
構造体と関連メソッドがfdMutex
を使用するように変更されました。Windows固有のoperation
構造体からもmu sync.Mutex
が削除されています。
-
src/pkg/net/sendfile_freebsd.go
,src/pkg/net/sendfile_linux.go
,src/pkg/net/sendfile_windows.go
:sendFile
関数内でc.wio.Lock()
/c.wio.Unlock()
の代わりにc.writeLock()
/c.writeUnlock()
を使用するように変更されました。
-
src/pkg/net/sockopt_posix.go
,src/pkg/net/sockoptip_bsd.go
,src/pkg/net/sockoptip_linux.go
,src/pkg/net/sockoptip_posix.go
,src/pkg/net/sockoptip_windows.go
,src/pkg/net/tcpsockopt_darwin.go
,src/pkg/net/tcpsockopt_openbsd.go
,src/pkg/net/tcpsockopt_posix.go
,src/pkg/net/tcpsockopt_unix.go
,src/pkg/net/tcpsockopt_windows.go
:- 各種ソケットオプション設定関数内で
fd.incref(false)
の呼び出しがfd.incref()
に変更されました。これは、incref
関数からclosing
引数が削除されたためです。
- 各種ソケットオプション設定関数内で
-
src/pkg/runtime/mgc0.c
,src/pkg/runtime/mprof.goc
,src/pkg/runtime/proc.c
,src/pkg/runtime/race.c
,src/pkg/runtime/runtime.h
,src/pkg/runtime/sema.goc
:- Goランタイム内のセマフォ関連の関数(
runtime·semacquire
)のシグネチャが変更され、profile
引数(bool
型)が追加されました。これは、fdMutex
がセマフォを使用する際に、プロファイリング情報を収集するかどうかを制御するためです。 src/pkg/runtime/netpoll.goc
にruntime_Semacquire
とruntime_Semrelease
のGoラッパー関数が追加され、net
パッケージからランタイムのセマフォ関数を呼び出せるようになりました。
- Goランタイム内のセマフォ関連の関数(
コアとなるコードの解説
src/pkg/net/fd_mutex.go
このファイルは、fdMutex
の定義と実装を含んでいます。
// fdMutex is a specialized synchronization primitive
// that manages lifetime of an fd and serializes access
// to Read and Write methods on netFD.
type fdMutex struct {
state uint64
rsema uint32
wsema uint32
}
// fdMutex.state is organized as follows:
// 1 bit - whether netFD is closed, if set all subsequent lock operations will fail.
// 1 bit - lock for read operations.
// 1 bit - lock for write operations.
// 20 bits - total number of references (read+write+misc).
// 20 bits - number of outstanding read waiters.
// 20 bits - number of outstanding write waiters.
const (
mutexClosed = 1 << 0
mutexRLock = 1 << 1
mutexWLock = 1 << 2
mutexRef = 1 << 3
mutexRefMask = (1<<20 - 1) << 3
mutexRWait = 1 << 23
mutexRMask = (1<<20 - 1) << 23
mutexWWait = 1 << 43
mutexWMask = (1<<20 - 1) << 43
)
fdMutex
構造体は、state
というuint64
型のフィールドで複数の状態をビットフィールドとして管理します。mutexClosed
はFDが閉じられたか、mutexRLock
とmutexWLock
は読み書きロックの状態、mutexRef
は参照カウント、mutexRWait
とmutexWWait
は読み書き待機中のゴルーチン数を表します。これらの定数は、state
フィールド内の各情報のビット位置とマスクを定義しています。
func (mu *fdMutex) Incref() bool {
for {
old := atomic.LoadUint64(&mu.state)
if old&mutexClosed != 0 {
return false // 既にクローズされている場合は参照を増やせない
}
new := old + mutexRef // 参照カウントをインクリメント
if new&mutexRefMask == 0 {
panic("net: inconsistent fdMutex") // オーバーフローチェック
}
if atomic.CompareAndSwapUint64(&mu.state, old, new) {
return true // CAS成功
}
}
}
Incref()
は、fdMutex
の参照カウントをアトミックにインクリメントします。FDが既にクローズされている場合はfalse
を返します。ループ内でatomic.LoadUint64
で現在の状態を読み込み、atomic.CompareAndSwapUint64
で更新を試みます。これにより、他のゴルーチンとの競合を避けつつ安全に参照カウントを操作します。
func (mu *fdMutex) RWLock(read bool) bool {
var mutexBit, mutexWait, mutexMask uint64
var mutexSema *uint32
if read {
mutexBit = mutexRLock
mutexWait = mutexRWait
mutexMask = mutexRMask
mutexSema = &mu.rsema
} else {
mutexBit = mutexWLock
mutexWait = mutexWWait
mutexMask = mutexWMask
mutexSema = &mu.wsema
}
for {
old := atomic.LoadUint64(&mu.state)
if old&mutexClosed != 0 {
return false // 既にクローズされている場合はロックできない
}
var new uint64
if old&mutexBit == 0 {
// ロックがフリーの場合、ロックを取得し参照カウントをインクリメント
new = (old | mutexBit) + mutexRef
if new&mutexRefMask == 0 {
panic("net: inconsistent fdMutex")
}
} else {
// ロックが取得されている場合、待機ゴルーチン数をインクリメント
new = old + mutexWait
if new&mutexMask == 0 {
panic("net: inconsistent fdMutex")
}
}
if atomic.CompareAndSwapUint64(&mu.state, old, new) {
if old&mutexBit == 0 {
return true // ロックを直接取得できた場合
}
runtime_Semacquire(mutexSema) // ロックが取得済みの場合、セマフォで待機
// シグナルを送った側がmutexWaitを減算している
}
}
}
RWLock()
は、読み込みまたは書き込みのロックを取得します。read
引数によって、読み込みロック(mutexRLock
)か書き込みロック(mutexWLock
)かが決定されます。ロックがフリーであれば、ロックを取得し参照カウントをインクリメントします。ロックが既に取得されている場合は、待機ゴルーチン数をインクリメントし、runtime_Semacquire
を呼び出してセマフォで待機します。これにより、複数のゴルーチンが同時にロックを要求した場合でも、効率的に待機と解除が行われます。
src/pkg/net/fd_unix.go
このファイルは、netFD
構造体の変更と、fdMutex
を使用したI/O操作の同期方法を示しています。
type netFD struct {
// locking/lifetime of sysfd + serialize access to Read and Write methods
fdmu fdMutex
// immutable until Close
sysfd int
// ... (他のフィールド)
}
netFD
構造体から、従来のsysmu
, sysref
, closing
, rio
, wio
といったsync.Mutex
関連のフィールドが削除され、代わりにfdmu fdMutex
が追加されました。これにより、ファイルディスクリプタのライフタイム管理と読み書き操作の同期がfdmu
に集約されます。
func (fd *netFD) readLock() error {
if !fd.fdmu.RWLock(true) {
return errClosing
}
return nil
}
func (fd *netFD) readUnlock() {
if fd.fdmu.RWUnlock(true) {
fd.destroy() // 完全に解放可能になった場合、FDを破棄
}
}
func (fd *netFD) Read(p []byte) (n int, err error) {
if err := fd.readLock(); err != nil { // 新しいreadLockを使用
return 0, err
}
defer fd.readUnlock() // deferでreadUnlockを呼び出し
// ... (実際の読み込み処理)
}
readLock()
とreadUnlock()
は、fdmu.RWLock(true)
とfdmu.RWUnlock(true)
をそれぞれ呼び出すヘルパー関数です。Read()
メソッドでは、従来のfd.rio.Lock()
とdefer fd.rio.Unlock()
の代わりに、これらの新しいヘルパー関数が使用されています。これにより、コードが簡潔になり、fdMutex
による最適化された同期メカニズムが適用されます。writeLock()
とwriteUnlock()
も同様に実装され、書き込み操作で使用されます。
関連リンク
- Go言語の
sync/atomic
パッケージ: https://pkg.go.dev/sync/atomic - Go言語の
sync
パッケージ: https://pkg.go.dev/sync - Go言語の
net
パッケージ: https://pkg.go.dev/net
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード
- コミットメッセージに記載されているベンチマーク結果と説明