Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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回、defer1回、およびその他の雑多なオーバーヘッドを削減できます。

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は汎用的なロックメカニズムであり、そのオーバーヘッドがネットワーク操作のパフォーマンスに影響を与えていました。特に、ReadWriteのような頻繁に呼び出される操作では、ロックの取得と解放にかかるコストが無視できないレベルになっていました。

このコミットの背景には、以下の課題がありました。

  1. sync.Mutexのオーバーヘッド: ネットワーク操作ごとにsync.Mutex.LockUnlockが複数回呼び出され、deferによる遅延実行も加わることで、パフォーマンスのボトルネックとなっていました。
  2. ファイルディスクリプタのライフタイム管理: netFDsysfd(システムファイルディスクリプタ)のライフタイム管理と、それに対する参照カウント、そしてクローズ処理の同期が複雑でした。
  3. 競合状態の回避: 複数のゴルーチンが同時にnetFDReadWrite操作を行おうとした際に、データ競合や不正な状態に陥ることを防ぐ必要がありました。

これらの課題を解決し、特にネットワークI/Oのパフォーマンスを向上させるために、より特化した軽量な同期プリミティブの導入が検討されました。

前提知識の解説

このコミットを理解するためには、以下の概念について基本的な知識が必要です。

  1. Go言語の並行性: Go言語はゴルーチンとチャネルを用いた並行プログラミングをサポートしています。複数のゴルーチンが同時に実行されるため、共有リソースへのアクセスには同期メカニズムが必要です。
  2. ミューテックス (Mutex): ミューテックスは、共有リソースへのアクセスを排他的に制御するための同期プリミティブです。Go言語ではsync.Mutexが提供されており、Lock()Unlock()メソッドで排他制御を行います。
  3. アトミック操作 (Atomic Operations): アトミック操作は、複数のCPU命令からなる一連の操作が、中断されることなく単一の不可分な操作として実行されることを保証するものです。Go言語ではsync/atomicパッケージが提供されており、atomic.LoadUint64atomic.CompareAndSwapUint64などの関数があります。これらは、ロックを使用せずに共有変数を安全に操作するために使用されます。
  4. セマフォ (Semaphore): セマフォは、リソースへのアクセスを制御するための同期プリミティブです。Go言語のランタイム内部では、ゴルーチンのスケジューリングや同期のためにセマフォが使用されることがあります。runtime_Semacquireruntime_Semreleaseは、Goランタイムが提供するセマフォ操作関数です。
  5. ファイルディスクリプタ (File Descriptor - FD): Unix系システムにおいて、ファイルやソケットなどのI/Oリソースを識別するために使用される整数値です。Go言語のnetパッケージでは、ネットワーク接続を抽象化するためにnetFD構造体が使用され、その内部でシステムファイルディスクリプタ(sysfd)を管理しています。
  6. defer: Go言語のdefer文は、関数がリターンする直前に指定された関数を実行するものです。リソースの解放やロックの解除など、クリーンアップ処理によく使用されます。しかし、deferにもわずかながらオーバーヘッドがあります。
  7. ベンチマーク: ソフトウェアの性能を測定するためのテストです。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操作の特性に合わせて最適化されています。特に、RWLockRWUnlockは、読み込みと書き込みの排他制御を効率的に行い、同時に参照カウントも管理します。

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が不要になり、そのオーバーヘッドも削減されました。
  • 特化された同期: fdMutexnetFDの特定のユースケース(参照カウント、読み書きの排他制御、クローズ処理)に特化して設計されているため、汎用的なsync.Mutexよりも効率的です。

これらの最適化により、ネットワーク操作あたりのレイテンシが70〜500ナノ秒削減され、特にIntel E5-2690プロセッサでは約5%の性能向上が見られました。

コアとなるコードの変更箇所

このコミットで追加・変更された主要なファイルは以下の通りです。

  1. 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)を使用しています。
  2. src/pkg/net/fd_mutex_test.go (新規追加):

    • fdMutexの機能と堅牢性を検証するための単体テストが記述されています。TestMutexLock, TestMutexClose, TestMutexCloseUnblock, TestMutexPanic, TestMutexStressなどのテストケースが含まれています。
  3. 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()を使用するように変更されました。
  4. src/pkg/net/fd_windows.go:

    • fd_unix.goと同様に、netFD構造体と関連メソッドがfdMutexを使用するように変更されました。Windows固有のoperation構造体からもmu sync.Mutexが削除されています。
  5. 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()を使用するように変更されました。
  6. 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引数が削除されたためです。
  7. 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.gocruntime_Semacquireruntime_SemreleaseのGoラッパー関数が追加され、netパッケージからランタイムのセマフォ関数を呼び出せるようになりました。

コアとなるコードの解説

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が閉じられたか、mutexRLockmutexWLockは読み書きロックの状態、mutexRefは参照カウント、mutexRWaitmutexWWaitは読み書き待機中のゴルーチン数を表します。これらの定数は、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言語の公式ドキュメント
  • Go言語のソースコード
  • コミットメッセージに記載されているベンチマーク結果と説明