[インデックス 18325] ファイルの概要
このコミットは、Goランタイムのネットワークポーリング(netpoll)のホットパスからロックを削除し、パフォーマンスを向上させることを目的としています。具体的には、ゴルーチンをパーク(待機)させるための新しい二段階メカニズムを導入し、これにより待機述語を保護するためのミューテックスが不要になります。この変更は、リーダー、ライター、およびI/O通知間の競合を軽減し、ホットパスから多数のミューテックス操作を排除することで、全体的な処理速度を向上させます。ベンチマーク結果は、TCP4の同時読み書きおよび永続的な接続において、顕著なパフォーマンス改善を示しています。
コミット
commit 9cbd2fb1aa7ac3d4cd33442a93187d8549dbf1c4
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Wed Jan 22 11:27:16 2014 +0400
runtime: remove locks from netpoll hotpaths
Introduces two-phase goroutine parking mechanism -- prepare to park, commit park.
This mechanism does not require backing mutex to protect wait predicate.
Use it in netpoll. See comment in netpoll.goc for details.
This slightly reduces contention between reader, writer and read/write io notifications;
and just eliminates a bunch of mutex operations from hotpaths, thus making then faster.
benchmark old ns/op new ns/op delta
BenchmarkTCP4ConcurrentReadWrite 2109 1945 -7.78%
BenchmarkTCP4ConcurrentReadWrite-2 1162 1113 -4.22%
BenchmarkTCP4ConcurrentReadWrite-4 798 755 -5.39%
BenchmarkTCP4ConcurrentReadWrite-8 803 748 -6.85%
BenchmarkTCP4Persistent 9411 9240 -1.82%
BenchmarkTCP4Persistent-2 5888 5813 -1.27%
BenchmarkTCP4Persistent-4 4016 3968 -1.20%
BenchmarkTCP4Persistent-8 3943 3857 -2.18%
R=golang-codereviews, mikioh.mikioh, gobot, iant, rsc
CC=golang-codereviews, khr
https://golang.org/cl/45700043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9cbd2fb1aa7ac3d4cd33442a93187d8549dbf1c4
元コミット内容
runtime: remove locks from netpoll hotpaths
Introduces two-phase goroutine parking mechanism -- prepare to park, commit park.
This mechanism does not require backing mutex to protect wait predicate.
Use it in netpoll. See comment in netpoll.goc for details.
This slightly reduces contention between reader, writer and read/write io notifications;
and just eliminates a bunch of mutex operations from hotpaths, thus making then faster.
benchmark old ns/op new ns/op delta
BenchmarkTCP4ConcurrentReadWrite 2109 1945 -7.78%
BenchmarkTCP4ConcurrentReadWrite-2 1162 1113 -4.22%
BenchmarkTCP4ConcurrentReadWrite-4 798 755 -5.39%
BenchmarkTCP4ConcurrentReadWrite-8 803 748 -6.85%
BenchmarkTCP4Persistent 9411 9240 -1.82%
BenchmarkTCP4Persistent-2 5888 5813 -1.27%
BenchmarkTCP4Persistent-4 4016 3968 -1.20%
BenchmarkTCP4Persistent-8 3943 3857 -2.18%
R=golang-codereviews, mikioh.mikioh, gobot, iant, rsc
CC=golang-codereviews, khr
https://golang.org/cl/45700043
変更の背景
GoランタイムのネットワークI/O処理(netpoll)は、多くのゴルーチンが同時にネットワークイベントを待機するホットパスであり、パフォーマンスのボトルネックになりやすい部分です。従来のnetpollの実装では、ゴルーチンがI/Oイベントを待機する際にミューテックスを使用して共有状態を保護していました。しかし、このミューテックスは、特に高負荷な環境下で、リーダー、ライター、およびI/O通知間の競合を引き起こし、パフォーマンスを低下させる原因となっていました。
このコミットの主な目的は、この競合を緩和し、netpollの効率を向上させることです。ミューテックス操作はCPUサイクルを消費し、キャッシュの無効化を引き起こすため、ホットパスからこれらを排除することは、全体的なスループットとレイテンシの改善に直結します。特に、ゴルーチンが待機状態に入る(パークする)際にミューテックスを必要としない新しいメカニズムを導入することで、この問題に対処しています。
前提知識の解説
Goのゴルーチンとスケジューラ
Goは軽量な並行処理の単位として「ゴルーチン(goroutine)」を提供します。ゴルーチンはOSのスレッドよりもはるかに軽量であり、数百万個のゴルーチンを同時に実行することも可能です。Goランタイムには、これらのゴルーチンをOSスレッドにマッピングし、実行をスケジュールする独自のスケジューラが組み込まれています。
ネットワークポーリング (netpoll)
GoのネットワークI/Oは、OSの提供する非同期I/Oメカニズム(Linuxのepoll、macOSのkqueue、WindowsのIOCPなど)を利用して実装されています。このメカニズムは「netpoll」と呼ばれ、複数のネットワーク接続からのI/Oイベントを効率的に監視し、準備ができた接続に対応するゴルーチンをスケジューラに通知します。これにより、ゴルーチンはI/O操作が完了するまでブロックされることなく、他の処理を実行できます。
ミューテックスと競合
ミューテックス(Mutex)は、共有リソースへのアクセスを排他的に制御するための同期プリミティブです。複数のゴルーチンが同時に共有データにアクセスしようとすると、データ競合が発生し、プログラムの動作が予測不能になる可能性があります。ミューテックスは、一度に一つのゴルーチンだけが共有データにアクセスできるようにすることで、この問題を解決します。しかし、ミューテックスの取得と解放にはオーバーヘッドがあり、特に頻繁にロックされるホットパスでは、パフォーマンスのボトルネックとなることがあります。
アトミック操作
アトミック操作は、不可分な操作であり、その実行中に他の操作によって中断されることがありません。これにより、複数のゴルーチンが同時に同じメモリ位置にアクセスしても、データ競合が発生しないことが保証されます。アトミック操作はミューテックスよりも軽量であり、競合が少ない場合にパフォーマンス上の利点があります。CompareAndSwap (CAS)
やExchange (XCHG)
などが代表的なアトミック操作です。
ゴルーチンのパーク/アンパーク
Goランタイムでは、ゴルーチンがI/O操作の完了やチャネルからの受信などを待機する必要がある場合、そのゴルーチンは「パーク(park)」されます。パークされたゴルーチンは実行を停止し、スケジューラによって実行キューから外されます。待機していたイベントが発生すると、そのゴルーチンは「アンパーク(unpark)」され、再び実行可能な状態になり、スケジューラによって実行が再開されます。
技術的詳細
このコミットの核心は、netpoll
のホットパスからミューテックスを排除するために導入された「二段階ゴルーチンパーキングメカニズム」です。従来のpark
関数は、ロックを保持したままゴルーチンを待機状態にする必要があり、これが競合の原因となっていました。新しいメカニズムでは、待機述語を保護するためのミューテックスを必要とせず、アトミック操作と特定の状態遷移を利用してゴルーチンのパークとアンパークを管理します。
PollDesc
構造体内のrg
(読み込みゴルーチン)とwg
(書き込みゴルーチン)のセマフォは、以下の4つの状態を取るように拡張されました。
- READY (
(G*)1
): I/O準備完了通知が保留中であることを示します。ゴルーチンはこの状態をnil
に変更することで通知を消費します。 - WAIT (
(G*)2
): ゴルーチンがセマフォ上でパークする準備をしているが、まだパークされていない状態を示します。ゴルーチンは状態を自身のG
ポインタに変更することでパークをコミットするか、あるいは同時I/O通知が状態をREADY
に変更するか、タイムアウト/クローズが状態をnil
に変更します。 - Gポインタ: ゴルーチンがセマフォ上でブロックされている状態を示します。I/O通知またはタイムアウト/クローズが状態を
READY
またはnil
にそれぞれ変更し、ゴルーチンをアンパークします。 - nil: 上記のいずれでもない状態。
この二段階パーキングメカニズムは、netpollblock
関数とnetpollunblock
関数で具体的に実装されています。
netpollblock
(パークの準備とコミット)
netpollblock
関数は、ゴルーチンをパークする際に以下のロジックで動作します。
- WAIT状態への設定: まず、
rg
またはwg
セマフォをアトミック操作(runtime·casp
)でnil
からWAIT
に設定しようとします。- もし既に
READY
状態であれば、I/Oが既に準備できているため、すぐにtrue
を返してブロックせずに処理を続行します。 - もし
nil
からWAIT
への設定に成功すれば、パークの準備が完了です。 - もし他の状態であれば、二重待機などの不正な状態として
runtime·throw
でパニックします。
- もし既に
- エラー状態の再チェック:
gpp
をWAIT
に設定した後、エラー状態(タイムアウトやクローズ)を再チェックします。これは、runtime_pollUnblock
やruntime_pollSetDeadline
などの関数が、closing
/rd
/wd
へのストアとrg
/wg
のロードの間にフルメモリバリアを挿入しているため、競合なく状態を読み取れることを保証します。 - パークのコミット:
waitio
フラグがtrue
であるか、またはエラーがない場合、runtime·park
を呼び出してゴルーチンを実際にパークします。このruntime·park
は、新しいblockcommit
関数をコールバックとして受け取ります。blockcommit
は、gpp
がまだWAIT
状態であれば、それを現在のゴルーチンgp
のポインタにアトミックに設定することで、パークをコミットします。 - READY通知の処理: パークから復帰した後、
runtime·xchgp(gpp, nil)
を使用してセマフォの状態をnil
に戻します。このxchgp
は、元の状態を返します。もし元の状態がREADY
であれば、I/Oが準備できたことを意味するためtrue
を返します。
netpollunblock
(アンパーク)
netpollunblock
関数は、I/Oイベントが発生した際にゴルーチンをアンパークするために使用されます。
- 状態の遷移:
rg
またはwg
セマフォの状態をアトミックにnil
またはREADY
に設定しようとします。- もし既に
READY
状態であれば、既に通知済みなのでnil
を返します。 - もし
nil
状態であり、かつioready
(I/O準備完了)でない場合は、アンパークする必要がないためnil
を返します。 runtime·casp
を使用して、現在の状態(old
)から新しい状態(new
、ioready
であればREADY
、そうでなければnil
)へアトミックに遷移させます。
- もし既に
- ゴルーチンの取得: 状態遷移が成功した場合、
old
の値がWAIT
よりも大きい(つまり、ゴルーチンへのポインタである)場合、そのゴルーチンを返します。これは、そのゴルーチンがパークされていたことを意味し、スケジューラによって実行可能状態にされるべきです。
runtime·park
の変更
runtime·park
関数は、ロックを引数として受け取る代わりに、bool(*unlockf)(G*, void*)
という新しいシグネチャを持つコールバック関数を受け取るように変更されました。このコールバックは、ゴルーチンがパークされる直前に呼び出され、ロックの解放や待機述語の更新など、パーク前の最終処理を行います。unlockf
がfalse
を返した場合、ゴルーチンはすぐに再開されます。
また、runtime·parkunlock
という新しいヘルパー関数が導入されました。これは、従来のruntime·park(runtime·unlock, lock, reason)
の呼び出しを置き換えるもので、内部で新しいparkunlock
コールバックを使用します。parkunlock
は単にロックを解放し、true
を返してゴルーチンがパークされることを許可します。
xchgp
アセンブリ関数の追加
ポインタのアトミックな交換(Exchange Pointer)を行うruntime·xchgp
関数が、386、amd64、およびARMアーキテクチャ向けにアセンブリコードで追加されました。これは、netpollblock
やnetpollunblock
内でPollDesc
のrg
/wg
フィールドの状態をアトミックに更新するために使用されます。
これらの変更により、netpoll
のクリティカルパスからミューテックスが排除され、アトミック操作と二段階パーキングメカニズムによって、より効率的で競合の少ないI/O待機処理が実現されています。
コアとなるコードの変更箇所
このコミットによる主要なコード変更は以下のファイルに集中しています。
src/pkg/runtime/asm_386.s
,src/pkg/runtime/asm_amd64.s
,src/pkg/runtime/atomic_arm.c
:- ポインタのアトミックな交換を行う
runtime·xchgp
関数が追加されました。これは、XCHGL
(386) /XCHGQ
(amd64) 命令、またはARMのCASループを使用して実装されています。
- ポインタのアトミックな交換を行う
src/pkg/runtime/chan.c
:- チャネルの送受信における
runtime·park(runtime·unlock, c, ...)
の呼び出しが、新しいruntime·parkunlock(c, ...)
に置き換えられました。 select
文におけるruntime·park
の呼び出しも、新しいselparkcommit
コールバックを使用するように変更されました。
- チャネルの送受信における
src/pkg/runtime/mgc0.c
:- ファイナライザの待機処理における
runtime·park
の呼び出しがruntime·parkunlock
に置き換えられました。
- ファイナライザの待機処理における
src/pkg/runtime/netpoll.goc
:PollDesc
構造体のrg
とwg
フィールドのコメントが更新され、READY
,WAIT
,G pointer
,nil
の4つの状態が説明されました。#define WAIT ((G*)2)
が追加されました。runtime_pollReset
,runtime_pollWait
,runtime_pollWaitCanceled
,runtime_netpollready
関数からruntime·lock(pd)
とruntime·unlock(pd)
の呼び出しが削除されました。これにより、これらのホットパスからミューテックスが排除されました。netpollblock
関数のシグネチャが変更され、waitio
引数が追加されました。netpollblock
とnetpollunblock
関数の内部ロジックが大幅に変更され、二段階パーキングメカニズムとアトミック操作(runtime·casp
,runtime·xchgp
)が導入されました。runtime·atomicstorep
がruntime_pollSetDeadline
とruntime_pollUnblock
、deadlineimpl
で使用され、メモリバリアを保証しています。
src/pkg/runtime/proc.c
:runtime·park
関数のシグネチャが変更され、void(*unlockf)(Lock*)
からbool(*unlockf)(G*, void*)
になりました。- 新しいヘルパー関数
parkunlock
とruntime·parkunlock
が追加されました。 park0
関数(runtime·park
の内部実装)が、unlockf
の戻り値に基づいてゴルーチンをすぐに再開するロジックを追加しました。
src/pkg/runtime/runtime.h
:M
構造体のwaitunlockf
フィールドの型が変更されました。- アトミック操作関連の関数宣言が整理され、
runtime·xchgp
の宣言が追加されました。 runtime·park
とruntime·parkunlock
の関数宣言が更新されました。
src/pkg/runtime/sema.goc
:- セマフォ操作における
runtime·park
の呼び出しがruntime·parkunlock
に置き換えられました。
- セマフォ操作における
src/pkg/runtime/time.goc
:- タイマー処理における
runtime·park
の呼び出しがruntime·parkunlock
に置き換えられました。
- タイマー処理における
コアとなるコードの解説
runtime·park
とruntime·parkunlock
の変更
従来のruntime·park
は、unlockf
という関数ポインタを受け取り、ゴルーチンがパークされる前にその関数を呼び出してロックを解放していました。しかし、このメカニズムでは、unlockf
がロックを解放する際にミューテックスを必要とし、これが競合の原因となっていました。
新しいruntime·park
は、bool(*unlockf)(G*, void*)
というシグネチャの関数ポインタを受け取ります。このunlockf
は、ゴルーチンG*
とロックオブジェクトvoid*
を引数に取り、ゴルーチンをパークするかどうかを示すbool
を返します。false
を返した場合、ゴルーチンはパークされずにすぐに実行を再開します。
// src/pkg/runtime/proc.c
void
runtime·park(bool(*unlockf)(G*, void*), void *lock, int8 *reason)
{
m->waitlock = lock;
m->waitunlockf = unlockf;
m->waitreason = reason;
runtime·mcall(park0);
}
static bool
parkunlock(G *gp, void *lock)
{
USED(gp);
runtime·unlock(lock);
return true;
}
void
runtime·parkunlock(Lock *lock, int8 *reason)
{
runtime·park(parkunlock, lock, reason);
}
runtime·parkunlock
は、runtime·park
の新しいラッパー関数として導入されました。これは、従来のruntime·park(runtime·unlock, lock, reason)
の呼び出しを置き換えるもので、内部でparkunlock
というシンプルなコールバックを使用します。parkunlock
は、与えられたロックを解放し、true
を返すことでゴルーチンがパークされることを許可します。これにより、チャネル、セマフォ、タイマーなどの待機処理が、ミューテックスを直接操作することなくゴルーチンをパークできるようになりました。
netpoll.goc
における二段階パーキング
netpoll.goc
では、PollDesc
構造体のrg
とwg
フィールドが、I/Oイベントを待機するゴルーチンの状態を管理するためのセマフォとして機能します。
// src/pkg/runtime/netpoll.goc
struct PollDesc
{
// ...
G* rg; // READY, WAIT, G waiting for read or nil
Timer rt; // read deadline timer
int64 rd; // read deadline
G* wg; // READY, WAIT, G waiting for write or nil
Timer wt; // write deadline timer
int64 wd; // write deadline
};
#define READY ((G*)1)
#define WAIT ((G*)2)
netpollblock
関数は、ゴルーチンがI/Oイベントを待機するためにブロックされる際に呼び出されます。
// src/pkg/runtime/netpoll.goc
static bool
netpollblock(PollDesc *pd, int32 mode, bool waitio)
{
G **gpp, *old;
gpp = &pd->rg;
if(mode == 'w')
gpp = &pd->wg;
// set the gpp semaphore to WAIT
for(;;) {
old = *gpp;
if(old == READY) {
*gpp = nil;
return true;
}
if(old != nil)
runtime·throw("netpollblock: double wait");
if(runtime·casp(gpp, nil, WAIT))
break;
}
// need to recheck error states after setting gpp to WAIT
// this is necessary because runtime_pollUnblock/runtime_pollSetDeadline/deadlineimpl
// do the opposite: store to closing/rd/wd, membarrier, load of rg/wg
if(waitio || checkerr(pd, mode) == 0)
runtime·park((bool(*)(G*, void*))blockcommit, gpp, "IO wait");
// be careful to not lose concurrent READY notification
old = runtime·xchgp(gpp, nil);
if(old > WAIT)
runtime·throw("netpollblock: corrupted state");
return old == READY;
}
static bool
blockcommit(G *gp, G **gpp)
{
return runtime·casp(gpp, WAIT, gp);
}
netpollblock
は、まずruntime·casp
(CompareAndSwap Pointer)を使用して、セマフォの状態をnil
からWAIT
にアトミックに遷移させます。これにより、他のゴルーチンが同時にセマフォを操作しようとしても、競合なく状態を更新できます。もしREADY
状態であれば、すぐにtrue
を返してブロックを回避します。
その後、runtime·park
を呼び出し、blockcommit
関数をコールバックとして渡します。blockcommit
は、セマフォがまだWAIT
状態であれば、それを現在のゴルーチンgp
のポインタにアトミックに設定します。これにより、ゴルーチンが実際にパークされたことがコミットされます。
パークから復帰した後、runtime·xchgp
(Exchange Pointer)を使用してセマフォの状態をnil
に戻し、元の状態がREADY
であったかどうかをチェックして返します。これにより、パーク中にI/Oイベントが発生した場合でも、その通知を失うことなく処理できます。
netpollunblock
関数
netpollunblock
関数は、I/Oイベントが発生した際に、待機しているゴルーチンをアンパークするために使用されます。
// src/pkg/runtime/netpoll.goc
static G*
netpollunblock(PollDesc *pd, int32 mode, bool ioready)
{
G **gpp, *old, *new;
gpp = &pd->rg;
if(mode == 'w')
gpp = &pd->wg;
for(;;) {
old = *gpp;
if(old == READY)
return nil;
if(old == nil && !ioready) {
// Only set READY for ioready. runtime_pollWait
// will check for timeout/cancel before waiting.
return nil;
}
new = nil;
if(ioready)
new = READY;
if(runtime·casp(gpp, old, new))
break;
}
if(old > WAIT)
return old; // must be G*
return nil;
}
netpollunblock
は、ループ内でruntime·casp
を使用してセマフォの状態をアトミックに更新します。
- もしセマフォが
READY
状態であれば、既に通知済みなのでnil
を返します。 - もし
nil
状態であり、かつioready
でない場合は、アンパークする必要がないためnil
を返します。 - それ以外の場合、
old
の状態からnew
の状態(ioready
であればREADY
、そうでなければnil
)へアトミックに遷移させます。 - 状態遷移が成功した場合、
old
の値がWAIT
よりも大きい(つまり、ゴルーチンへのポインタである)場合、そのゴルーチンを返します。これは、そのゴルーチンがパークされていたことを意味し、スケジューラによって実行可能状態にされるべきです。
これらの変更により、netpoll
のI/O待機処理は、ミューテックスの代わりにアトミック操作と二段階パーキングメカニズムを使用するようになり、高負荷環境下でのパフォーマンスが大幅に向上しました。
関連リンク
- Go CL 45700043: https://golang.org/cl/45700043
参考にした情報源リンク
- Go言語のソースコード (特に
src/pkg/runtime
ディレクトリ) - コミットメッセージと関連するベンチマーク結果
- Goの並行処理とスケジューラに関する一般的な知識
- アトミック操作とミューテックスに関する一般的な知識
- GoのネットワークI/Oに関するドキュメントと解説記事 (必要に応じてWeb検索を使用)
- Goの
netpoll
実装に関する技術ブログや論文 (必要に応じてWeb検索を使用)