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

[インデックス 12864] ファイルの概要

このコミットは、Go言語のツールチェインにおいて、x86 (386) アーキテクチャ向けのアセンブラ (8a) とリンカ (8l) にPREFETCH命令のサポートを追加し、さらにランタイムのasm_386.sファイルでPREFETCHNTA命令の使用方法を更新するものです。これにより、Goプログラムがデータプリフェッチを活用し、メモリレイテンシを削減してパフォーマンスを向上させる可能性が生まれます。

コミット

commit 3d462449f670e2c0a810a6ef3bfc732a117f3cf7
Author: Russ Cox <rsc@golang.org>
Date:   Tue Apr 10 10:09:27 2012 -0400

    8a, 8l: add PREFETCH instructions
    
    R=ken2
    CC=golang-dev
    https://golang.org/cl/5992082

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/3d462449f670e2c0a810a6ef3bfc732a117f3cf7

元コミット内容

    8a, 8l: add PREFETCH instructions
    
    R=ken2
    CC=golang-dev
    https://golang.org/cl/5992082

変更の背景

この変更の背景には、プログラムのパフォーマンス最適化、特にメモリレイテンシの削減という目的があります。現代のCPUは非常に高速ですが、メインメモリからのデータ取得はCPUの処理速度に比べてはるかに遅いという「メモリウォール」問題に直面しています。このギャップを埋めるために、CPUはキャッシュメモリを利用しますが、キャッシュミスが発生すると、CPUはメインメモリからのデータロードを待つ必要があり、これがパフォーマンスのボトルネックとなります。

PREFETCH命令は、プログラマが将来必要となるであろうデータを事前にキャッシュにロードするようCPUに「ヒント」を与えるための命令です。これにより、実際にデータが必要になったときにすでにキャッシュに存在している可能性が高まり、メモリレイテンシによるストール(処理の停止)を減らすことができます。

Go言語のツールチェインにこれらの命令のサポートを追加することで、Goプログラムが低レベルな最適化、特にデータアクセスパターンが予測可能な場合に、より高いパフォーマンスを達成できるようになります。これは、特に数値計算、データ処理、ゲーム開発など、メモリへのアクセスが頻繁に行われるアプリケーションにおいて重要です。

前提知識の解説

1. CPUキャッシュとメモリ階層

CPUは、処理速度の速い順にレジスタ、L1キャッシュ、L2キャッシュ、L3キャッシュ、そしてメインメモリ(RAM)という階層的なメモリシステムを持っています。CPUがデータにアクセスする際、まず最も高速なL1キャッシュから探し、見つからなければL2、L3と順に探し、最終的にメインメモリにアクセスします。メインメモリへのアクセスは、L1キャッシュへのアクセスに比べて数百倍もの時間がかかることがあります。

2. キャッシュミスとパフォーマンス

CPUが要求するデータがキャッシュに存在しない場合(キャッシュミス)、CPUは下位のメモリ階層からデータをロードする必要があります。このロードには時間がかかり、その間CPUはアイドル状態になるか、他のタスクに切り替えることになります。これがプログラムの実行速度を低下させる主要な原因の一つです。

3. プリフェッチ(Prefetching)

プリフェッチとは、CPUが実際にデータが必要になる前に、そのデータをキャッシュにロードしておく技術です。これはハードウェアによって自動的に行われる場合もありますが(ハードウェアプリフェッチ)、プログラマが明示的に指示することも可能です(ソフトウェアプリフェッチ)。PREFETCH命令はソフトウェアプリフェッチの一種です。

4. PREFETCH命令の種類 (x86/x64アーキテクチャ)

Intel/AMDのx86/x64アーキテクチャには、いくつかのPREFETCH命令が存在し、それぞれ異なるキャッシュレベルやプリフェッチの挙動を指示します。

  • PREFETCHT0: データをすべてのキャッシュレベル(L1, L2, L3)にロードするようヒントを与えます。最も積極的なプリフェッチです。
  • PREFETCHT1: データをL2キャッシュとL3キャッシュにロードするようヒントを与えます。L1キャッシュにはロードしない可能性があります。
  • PREFETCHT2: データをL2キャッシュにロードするようヒントを与えます。L3キャッシュにはロードしない可能性があります。
  • PREFETCHNTA: データを非テンポラル(Non-Temporal)な方法でキャッシュにロードするようヒントを与えます。これは、データが一度しか使用されない可能性が高い場合に有用です。キャッシュラインを汚染せず、既存のキャッシュデータを追い出す可能性を低減します。ストリーミングデータなど、一度読み込んだらすぐに不要になるデータに適しています。

これらの命令はあくまで「ヒント」であり、CPUが必ずしもその通りに実行するとは限りません。CPUの内部状態や実装によって、プリフェッチが無視されたり、異なる挙動をしたりすることがあります。しかし、適切に使用すれば、パフォーマンス向上に大きく貢献します。

5. Go言語のツールチェイン (8a, 8l)

  • 8a (Go Assembler for 386): Go言語のソースコード(特に.sファイルで書かれたアセンブリコード)を、386 (x86) アーキテクチャ向けのオブジェクトコードに変換するアセンブラです。Goのランタイムや一部の標準ライブラリは、パフォーマンスが重要な部分でアセンブリコードを使用しています。
  • 8l (Go Linker for 386): 8aによって生成されたオブジェクトファイルや、Goコンパイラによって生成されたオブジェクトファイルをリンクし、実行可能なバイナリを生成するリンカです。

これらのツールは、Go言語のクロスコンパイル能力を支える重要なコンポーネントであり、特定のアーキテクチャ向けの低レベルな最適化を可能にします。

技術的詳細

このコミットは、Go言語の386アーキテクチャ向けのアセンブラとリンカにPREFETCH命令のサポートを追加することで、GoプログラムがこれらのCPU命令を直接利用できるようにします。

具体的には、以下のファイルが変更されています。

  1. src/cmd/8a/lex.c:

    • このファイルは、8aアセンブラの字句解析器(lexer)の一部です。
    • PREFETCHT0, PREFETCHT1, PREFETCHT2, PREFETCHNTAという新しいニーモニック(命令のシンボル名)が追加されています。
    • これにより、Goのアセンブリコード内でこれらのPREFETCH命令を記述できるようになります。例えば、PREFETCHT0 (AX)のように記述できるようになります。
  2. src/cmd/8l/8.out.h:

    • このヘッダファイルは、8lリンカが使用する命令コード(opcode)の定義を含んでいます。
    • APREFETCHT0, APREFETCHT1, APREFETCHT2, APREFETCHNTAという新しい定数(enum値)が追加されています。これらは、リンカが内部的にPREFETCH命令を識別するために使用する数値コードです。
  3. src/cmd/8l/optab.c:

    • このファイルは、8lリンカのオペレーションテーブル(optab)を定義しています。オペレーションテーブルは、各命令のニーモニックと、それに対応する機械語コードの生成ルールをマッピングするものです。
    • yprefetchという新しいオペランドタイプ(Ym, Ynone, Zm_o, 2)が定義されています。これは、PREFETCH命令がメモリオペランド(Ym)またはオペランドなし(Ynone)を取り、特定のバイト数(2)の機械語を生成することを示唆しています。
    • optab配列に、APREFETCHT0, APREFETCHT1, APREFETCHT2, APREFETCHNTAに対応するエントリが追加されています。
      • これらのエントリは、各PREFETCH命令がyprefetchオペランドタイプを使用し、機械語コードとして0x18というオペコード(これはPREFETCH命令の共通オペコードの一部)と、それに続く特定のバイト(01, 02, 03, 00)を生成することを示しています。これらのバイトは、PREFETCH命令の具体的な種類(T0, T1, T2, NTA)を指定するものです。
  4. src/pkg/runtime/asm_386.s:

    • このファイルは、Goランタイムの386アーキテクチャ向けアセンブリコードを含んでいます。
    • runtime·prefetch関数内で、コメントアウトされていた古いバイトコードによるPREFETCHNTA命令の記述(BYTE $0x0f; BYTE $0x18; BYTE $0x00)が削除され、新しく追加されたニーモニックPREFETCHNTA (AX)に置き換えられています。
    • これは、アセンブラとリンカがPREFETCH命令をネイティブにサポートするようになったため、手動でバイトコードを記述する必要がなくなり、より可読性の高いアセンブリニーモニックを使用できるようになったことを示しています。

これらの変更により、Go言語のコンパイラやランタイム、あるいはユーザーが記述するアセンブリコードにおいて、PREFETCH命令を直接利用できるようになり、特定のパフォーマンスクリティカルなセクションでメモリレイテンシを積極的に管理することが可能になります。

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

src/cmd/8a/lex.c (アセンブラの字句解析器)

--- a/src/cmd/8a/lex.c
+++ b/src/cmd/8a/lex.c
@@ -667,6 +667,10 @@ struct
 	"MFENCE",	LTYPE0, AMFENCE,
 	"SFENCE",	LTYPE0, ASFENCE,
 	"EMMS",		LTYPE0, AEMMS,
+	"PREFETCHT0",		LTYPE2,	APREFETCHT0,
+	"PREFETCHT1",		LTYPE2,	APREFETCHT1,
+	"PREFETCHT2",		LTYPE2,	APREFETCHT2,
+	"PREFETCHNTA",		LTYPE2,	APREFETCHNTA,
 
 	0
 };

src/cmd/8l/8.out.h (リンカの命令定義)

--- a/src/cmd/8l/8.out.h
+++ b/src/cmd/8l/8.out.h
@@ -451,6 +451,11 @@ enum	as
 	ASFENCE,
 
 	AEMMS,
+	
+	APREFETCHT0,
+	APREFETCHT1,
+	APREFETCHT2,
+	APREFETCHNTA,
 
 	ALAST
 };

src/cmd/8l/optab.c (リンカのオペレーションテーブル)

--- a/src/cmd/8l/optab.c
+++ b/src/cmd/8l/optab.c
@@ -349,6 +349,11 @@ uchar	ysvrs[] =
 	Ym,	Ynone,	Zm_o,	2,
 	0
 };
+uchar	yprefetch[] =
+{
+	Ym,	Ynone,	Zm_o,	2,
+	0,
+};
 
 Optab optab[] =
 /*	as, ytab, andproto, opcode */
@@ -761,5 +766,10 @@ Optab optab[] =
 
 	{ AEMMS, ynone, Pm, 0x77 },
 
+	{ APREFETCHT0,	yprefetch,	Pm,	0x18,(01) },
+	{ APREFETCHT1,	yprefetch,	Pm,	0x18,(02) },
+	{ APREFETCHT2,	yprefetch,	Pm,	0x18,(03) },
+	{ APREFETCHNTA,	yprefetch,	Pm,	0x18,(00) },
+
 	0
 };

src/pkg/runtime/asm_386.s (Goランタイムのアセンブリコード)

--- a/src/pkg/runtime/asm_386.s
+++ b/src/pkg/runtime/asm_386.s
@@ -417,8 +417,7 @@ TEXT runtime·atomicstore64(SB), 7, $0
 
 TEXT runtime·prefetch(SB), 7, $0
 	MOVL    4(SP), AX
-	// PREFETCHNTA (AX)
-	BYTE $0x0f; BYTE $0x18; BYTE $0x00
+	PREFETCHNTA (AX)
 	RET
 
 // void jmpdefer(fn, sp);

コアとなるコードの解説

src/cmd/8a/lex.c

この変更は、GoのアセンブラがPREFETCHT0, PREFETCHT1, PREFETCHT2, PREFETCHNTAという新しい命令ニーモニックを認識できるようにするためのものです。LTYPE2は、これらの命令が特定のオペランドタイプを持つことを示しています。これにより、Goのアセンブリソースファイル(.sファイル)内でこれらの命令を直接記述できるようになります。

src/cmd/8l/8.out.h

このヘッダファイルは、リンカが内部的に使用する命令の列挙型(enum)を定義しています。APREFETCHT0などの新しいエントリが追加されたことで、リンカはこれらのPREFETCH命令を個別の命令として識別し、処理できるようになります。

src/cmd/8l/optab.c

このファイルは、Goのリンカがアセンブリ命令を実際の機械語コードに変換するための「レシピ」を提供します。

  • yprefetch配列は、PREFETCH命令が取りうるオペランドのパターンを定義しています。Ymはメモリオペランド、Ynoneはオペランドなし、Zm_oはメモリオペランドのオフセット、2は命令のバイト長に関連する情報を示唆しています。
  • optab配列に追加されたエントリは、各PREFETCH命令(APREFETCHT0など)がどのように機械語に変換されるかを具体的に示しています。
    • 0x18は、PREFETCH命令の共通のオペコードの一部です。
    • 括弧内の01, 02, 03, 00は、PREFETCH命令の具体的な種類(T0, T1, T2, NTA)を指定するための追加のバイト(ModR/Mバイトの一部)です。例えば、PREFETCHNTA00を使用し、PREFETCHT001を使用します。

これらの定義により、リンカはアセンブリコード中のPREFETCH命令を正しく解析し、対応するx86機械語命令を生成できるようになります。

src/pkg/runtime/asm_386.s

この変更は、Goランタイム内のruntime·prefetch関数におけるPREFETCHNTA命令の記述方法を更新しています。

  • 変更前は、PREFETCHNTA命令を直接バイトコード(BYTE $0x0f; BYTE $0x18; BYTE $0x00)で記述していました。これは、アセンブラがこの命令をネイティブにサポートしていなかったため、手動で機械語を埋め込む必要があったことを意味します。
  • 変更後は、新しく追加されたニーモニックPREFETCHNTA (AX)を使用しています。これは、8aアセンブラと8lリンカがPREFETCH命令を完全にサポートするようになったため、より高レベルで可読性の高いアセンブリ構文を使用できるようになったことを示しています。AXレジスタは、プリフェッチするデータのメモリアドレスを保持していると推測されます。

この変更は、GoランタイムがPREFETCH命令をよりクリーンかつ標準的な方法で利用できるようになったことを示しており、将来的なメンテナンス性や可読性の向上に貢献します。

関連リンク

参考にした情報源リンク