Edited at

Goとrdtscの謎を追う

最近重い腰をあげてようやくGoの勉強を本格的にはじめました。それでたぶんGoにかんしてはじめて記事を書くのですが、なにぶん初心者なのでお手柔らかにお願いします。

そういうわけで src/runtime/asm_amd64.s をなんとなく読んでいたのですが、以下のような気になるコードを見つけました。

    // Figure out how to serialize RDTSC.

// On Intel processors LFENCE is enough. AMD requires MFENCE.
// Don't know about the rest, so let's do MFENCE.
CMPL BX, $0x756E6547 // "Genu"
JNE notintel
CMPL DX, $0x49656E69 // "ineI"
JNE notintel
CMPL CX, $0x6C65746E // "ntel"
JNE notintel
MOVB $1, runtime·isIntel(SB)
MOVB $1, runtime·lfenceBeforeRdtsc(SB)

このコードはcpuid命令がある場合にベンダー情報を取得して、それがIntelのプロセッサだった場合に isIntel フラグと lfenceBeforeRdtsc フラグを建てているということをやっているのですが、気になるのがコメントの部分です。

x86プロセッサにはrdtsc(Read Time Stamp Counter)という命令があり、これを用いてCPUクロックごとに加算されるタイムスタンプカウンタの値を取得することができます。

Goのコード内では cputick という関数を経由して内部のタイマやらなんやらいろんなところで使われます。

// func cputicks() int64

TEXT runtime·cputicks(SB),NOSPLIT,$0-0
CMPB runtime·lfenceBeforeRdtsc(SB), $1
JNE mfence
LFENCE
JMP done
mfence:
MFENCE
done:
RDTSC
SHLQ $32, DX
ADDQ DX, AX
MOVQ AX, ret+0(FP)
RET

しかしrdtsc命令を使う場合には注意が必要です。最近のスーパースカラプロセッサはOoO(Out-of-Order実行)が主流になっていて、rdtsc命令が先行する命令を追い越して実行されてしまうと結果を格納するレジスタの値が意図しないものになってしまいます。

そのためfence命令やcpuid命令を先行して使うことによって命令リオーダリングを発生させないようにする必要があります。

(追記:ここたぶん勘違いしてて、lfence/mfence命令を使う理由はSMP環境で別のCPUコアのtsc値をみてしまった場合に単調増加した値にならないからという理由のようです。Linuxカーネルのrdtsc関数のコメントにその旨が載っていました)

(再追記: コメントでyohhoyさんに指摘いただきました、やはりrdtsc命令は命令リオーダリングを防ぐためにfence命令を使う必要があります)

ここまでは知っている人も多いと思いますが、気になるのはIntelのプロセッサではlfence命令で十分だけどAMDの場合はmfence命令が必要というコメントです。いったいこれはどこがソースになっているのでしょうか。

git blameしてみると以下のコミットで入った変更のようです。コミットの作者は現在のGoのスケジューラのコア実装を行ったdvyukov大先生です。

commit 6e70fddec0e1d4a43ffb450f555dde82ff313397

Author: Dmitry Vyukov <dvyukov@google.com>
Date: Tue Feb 17 14:25:49 2015 +0300

runtime: fix cputicks on x86

See the following issue for context:
https://github.com/golang/go/issues/9729#issuecomment-74648287
In short, RDTSC can produce skewed results without preceding LFENCE/MFENCE.
Information on this matter is very scrappy in the internet.
But this is what linux kernel does (see rdtsc_barrier).
It also fixes the test program on my machine.

Update #9729

Change-Id: I3c1ffbf129fdfdd388bd5b7911b392b319248e68
Reviewed-on: https://go-review.googlesource.com/5033
Reviewed-by: Ian Lance Taylor <iant@golang.org>


Information on this matter is very scrappy in the internet.


草。

どうやらこのロジックはLinuxカーネルの rdtsc_barrier 関数(現在はリネームされて rdtsc_ordered 関数になっています)からもってきたもののようです。

ではLinuxカーネルではどのように書かれているのでしょうか。

rdtsc_orderedにリネームされた時点のコミットではコード はこのようになっていました。

どうやら各アーキテクチャマニュアルのほうに詳しいことは書いてあるみたいです。

じっさいにIntel SDMのほうはVol3 Chapter 8.3に「LFENCE does provide some gurantees ~」という記述があり、ADM APMもmfenceでcpuid相当のことが可能であるという文面がありました。

/**

* rdtsc_ordered() - read the current TSC in program order
*
* rdtsc_ordered() returns the result of RDTSC as a 64-bit integer.
* It is ordered like a load to a global in-memory counter. It should
* be impossible to observe non-monotonic rdtsc_unordered() behavior
* across multiple CPUs as long as the TSC is synced.
*/

static __always_inline unsigned long long rdtsc_ordered(void)
{
/*
* The RDTSC instruction is not ordered relative to memory
* access. The Intel SDM and the AMD APM are both vague on this
* point, but empirically an RDTSC instruction can be
* speculatively executed before prior loads. An RDTSC
* immediately after an appropriate barrier appears to be
* ordered as a normal load, that is, it provides the same
* ordering guarantees as reading from a global memory location
* that some other imaginary CPU is updating continuously with a
* time stamp.
*/

alternative_2("", "mfence", X86_FEATURE_MFENCE_RDTSC,
"lfence", X86_FEATURE_LFENCE_RDTSC);
return rdtsc();
}

また古いLinuxカーネルでは lfence; rdtsc; lfence; のようにしていたのが このコミットlfence; rdtsc に変わっています。コミットメッセージをみるに後ろのlfenceが不要なことにのちのち気がついた、というふうに読み取れます。


参考資料

参考というかほとんど書き終わったタイミングで gotsc の存在に気がつきました。READMEにはほとんど同じことを書いていますが、こちらはGoからrdtscをたたくライブラリ?になっているみたいです。まあGo本体の歴史的経緯には触れられていないので多少は記事を書いた意味はあるのではないでしょうか…


おまけ

ところでそもそもこんなのx86側で命令を用意しろ、というごもっともな指摘があると思います。じっさいにこの問題を解決するrdtscp命令というのがあるんですが、Vyukov先生いわく、「古いIntelのプロセッサ(Nehalem以前)にはrdtscpないし、lfenceは数サイクルくらいしかかからないから大してうまみがない。」という理由でGoはrdtscを使い続けているみたいです。