[インデックス 14071] ファイルの概要
このコミットは、Go言語のツールチェインの一部であるx86アーキテクチャ向けリンカ cmd/8l
に、SSE2 (Streaming SIMD Extensions 2) 命令のサポートを追加するものです。これにより、GoプログラムがSSE2命令を利用して、浮動小数点演算や整数演算をより効率的に実行できるようになります。
コミット
commit c1d06cef122b221340a1f4c5884dda4aff32c3da
Author: Russ Cox <rsc@golang.org>
Date: Sun Oct 7 16:36:14 2012 -0400
cmd/8l: add SSE2 instructions
R=ken
CC=golang-dev
https://golang.org/cl/6610065
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c1d06cef122b221340a1f4c5884dda4aff32c3da
元コミット内容
cmd/8l: add SSE2 instructions
このコミットの目的は、Go言語のx86リンカであるcmd/8l
にSSE2命令のサポートを追加することです。
変更の背景
Go言語は、その設計思想として、高性能な実行環境を提供することを目指しています。CPUが提供するSIMD (Single Instruction, Multiple Data) 命令セットは、特に科学技術計算、マルチメディア処理、グラフィックスなど、大量のデータを並列に処理するアプリケーションにおいて、劇的なパフォーマンス向上をもたらします。
SSE2は、Intel Pentium 4プロセッサで導入されたSIMD命令セットであり、浮動小数点演算だけでなく、整数演算にも対応しています。Go言語のランタイムや標準ライブラリ、あるいはユーザーが記述するGoプログラムが、これらの高性能な命令を直接利用できるようにするためには、コンパイラやリンカがそれらの命令を認識し、正しく機械語に変換できる必要があります。
このコミットが行われた2012年時点では、多くのx86プロセッサがSSE2をサポートしており、その利用はパフォーマンス最適化において不可欠でした。Go言語のツールチェインがSSE2命令をサポートすることで、Goで書かれたプログラムがこれらの命令を活用し、より高速に動作する基盤が整備されます。
前提知識の解説
Go言語のツールチェイン
Go言語のプログラムは、go build
コマンドによってコンパイルされ、実行可能なバイナリが生成されます。このプロセスには、以下の主要なコンポーネントが関与します。
- コンパイラ (
cmd/compile
): Goのソースコードをアセンブリコード(Goのアセンブリ言語)に変換します。 - アセンブラ (
cmd/asm
): Goのアセンブリコードをオブジェクトファイル(機械語)に変換します。 - リンカ (
cmd/link
): 複数のオブジェクトファイルやライブラリを結合し、最終的な実行可能バイナリを生成します。本コミットで変更されているcmd/8l
は、x86アーキテクチャ向けのリンカです(8
はx86を意味します)。
SIMD (Single Instruction, Multiple Data)
SIMDは、単一の命令で複数のデータ要素に対して同じ操作を同時に実行する並列処理の形態です。これにより、ループ処理などで同じ演算を多数のデータに適用する場合に、大幅な高速化が期待できます。
SSE (Streaming SIMD Extensions) と SSE2
Intelが開発したSIMD命令セットの拡張機能です。
- SSE (Streaming SIMD Extensions): Pentium IIIで導入され、主に単精度浮動小数点数(32ビット)のSIMD演算をサポートします。8つの128ビットXMMレジスタ(
XMM0
~XMM7
)を使用します。 - SSE2 (Streaming SIMD Extensions 2): Pentium 4で導入され、SSEをさらに拡張したものです。
- 倍精度浮動小数点数(64ビット)のSIMD演算をサポートします。
- 整数演算のSIMD演算をサポートします。
- 引き続き8つの128ビットXMMレジスタを使用します。
SSE/SSE2命令は、通常のx86命令とは異なるエンコーディングを持ち、特定のプレフィックスバイト(例: 0x66
、0xF2
、0xF3
)を必要とすることがあります。これらのプレフィックスは、命令がどのデータ型(単精度、倍精度、整数)を扱うか、あるいはどの命令セットに属するかを示すために使用されます。
リンカの役割と命令エンコーディング
リンカは、アセンブリコードが表現する命令を、CPUが直接実行できるバイナリ形式(機械語)に変換する重要な役割を担います。この変換には、各命令に対応するオペコード(操作コード)や、オペランド(操作対象のデータやレジスタ)のエンコーディング規則に関する詳細な知識が必要です。
リンカ内部では、通常、命令の種類、オペランドの型、アドレッシングモードなどに基づいて、適切な機械語バイト列を生成するためのテーブル(optab
のようなもの)が管理されています。新しい命令セット(SSE2)をサポートするということは、これらのテーブルに新しいエントリを追加し、それらの命令を正しくエンコードするためのロジックを実装することを意味します。
技術的詳細
このコミットは、Go言語のx86リンカ cmd/8l
にSSE2命令のサポートを統合するために、複数のファイルにわたる広範な変更を加えています。
-
命令コードとレジスタの定義追加 (
src/cmd/8l/8.out.h
):enum as
に、AADDPD
(Add Packed Double-precision Floating-point) やAMOVSD
(Move Scalar Double-precision Floating-point) など、多数のSSE2命令に対応する新しいアセンブリ命令コードが追加されています。これらの命令は、Goのアセンブリ言語でSSE2命令を記述する際に使用されます。D_X0
からD_X7
までの新しいレジスタタイプが追加されています。これらはSSE/SSE2命令が使用する128ビットのXMMレジスタ(XMM0
~XMM7
)に対応します。既存のD_NONE
以降の定数値がシフトされています。
-
内部定数とオペランドタイプの拡張 (
src/cmd/8l/l.h
):enum
に、SSE/SSE2レジスタの内部表現であるYmr
,Ymm
,Yxr
,Yxm
が追加されています。- オペランドの組み合わせやアドレッシングモードを定義する
Zxxx
タイプの定数に、Zm_r_xm
,Zm_r_i_xm
,Zr_m_xm
,Zr_m_i_xm
などが追加されています。これらは、メモリとXMMレジスタ間の操作や、即値を含む操作など、SSE/SSE2命令特有のオペランド形式を表現するために使用されます。 - 命令のプレフィックスバイトを定義する定数として、
Pf2
(0xF2) とPf3
(0xF3) が追加されています。これらは、特定のSSE/SSE2命令のエンコーディングに必要なバイトです(例:0xF2
はREPNE
プレフィックスとして、0xF3
はREPE
プレフィックスとして使用されることがありますが、SSE/SSE2では異なる意味を持ちます)。
-
レジスタ名の文字列化対応 (
src/cmd/8l/list.c
):- リンカがデバッグ出力などでレジスタ名を表示する際に使用する
regstr
配列に、X0
からX7
までのXMMレジスタの文字列表現が追加されています。
- リンカがデバッグ出力などでレジスタ名を表示する際に使用する
-
浮動小数点定数処理の拡張 (
src/cmd/8l/obj.c
):obj.c
はオブジェクトファイルの生成に関連するロジックを含んでいます。この変更では、AMOVSS
,AADDSS
,AMOVSD
,AADDSD
などのSSE2命令が、既存の浮動小数点命令と同様に、D_FCONST
(浮動小数点定数) をオペランドとして扱う場合の処理に追加されています。これは、リンカが浮動小数点定数を適切に処理し、機械語に埋め込むためのものです。
-
命令テーブルの更新とオペランドパターンの定義 (
src/cmd/8l/optab.c
):- このファイルは、アセンブリ命令とそれに対応する機械語のオペコード、オペランドの型、プレフィックスなどをマッピングする
optab
(operation table) を定義しています。 optab
配列に、AADDPD
からAXORPS
までの膨大な数のSSE2命令のエントリが追加されています。各エントリは、命令コード、オペランドの型を示すuchar
配列(例:yxm
,yxcvm1
)、プレフィックスバイト(Pq
,Pm
,Pe
,Pf2
,Pf3
)、およびオペコードバイトを含んでいます。yxm
,yxcvm1
,yxcvm2
,yxmq
,yxr
,yxr_ml
,yxcmp
,yxcmpi
,yxmov
,yxcvfl
,yxcvlf
,yxcvfq
,yxcvqf
,yxrrl
といった新しいuchar
配列が定義されています。これらは、各SSE2命令がどのようなオペランドの組み合わせ(例: XMMレジスタとメモリ、XMMレジスタとXMMレジスタ)を取るかを詳細に記述しています。
- このファイルは、アセンブリ命令とそれに対応する機械語のオペコード、オペランドの型、プレフィックスなどをマッピングする
-
命令初期化とエンコーディングロジックの調整 (
src/cmd/8l/span.c
):instinit
関数は、リンカの初期化時に命令テーブルの整合性をチェックし、レジスタのカバー範囲を定義します。このコミットでは、ycover
配列にXMMレジスタ(Ymm
,Yxm
)のカバー範囲が追加され、reg
配列にD_X0
からD_X7
までのXMMレジスタが初期化されています。oclass
関数は、アドレスのタイプに基づいてオペランドのクラスを返します。XMMレジスタ(D_X0
~D_X7
)に対してYxr
を返すように更新されています。asmand
関数は、オペランドを機械語にエンコードする際に使用されます。XMMレジスタ(D_X0
~D_X7
)を適切に処理するように変更されています。- 新しい静的関数
mediaop
が追加されています。この関数は、SSE/SSE2命令に特有のプレフィックスバイト(Pm
,Pe
,Pf2
,Pf3
)の処理を担当します。SSE/SSE2命令は、通常のx86命令とは異なるプレフィックスの規則を持つため、この専用の処理が必要となります。 doasm
関数は、アセンブリ命令を機械語に変換する主要な関数です。この関数は、新しいオペランドタイプ(Zm_r_xm
,Zm_r_i_xm
,Zr_m_xm
,Zr_m_i_xm
)を処理するためにmediaop
関数を呼び出すように変更されています。これにより、SSE2命令が正しくエンコードされるようになります。
これらの変更により、cmd/8l
はGoのアセンブリ言語で記述されたSSE2命令を認識し、適切な機械語に変換して実行可能バイナリに含めることができるようになります。
コアとなるコードの変更箇所
このコミットのコアとなる変更は、主に以下のファイルに集中しています。
src/cmd/8l/8.out.h
: SSE2命令の命令コードとXMMレジスタの定義。src/cmd/8l/l.h
: SSE/SSE2関連の内部定数、オペランドタイプ、命令プレフィックスの定義。src/cmd/8l/optab.c
: SSE2命令とそれに対応する機械語オペコード、オペランドパターン、プレフィックスのマッピング定義。src/cmd/8l/span.c
: リンカの初期化、レジスタ処理、そして特にSSE/SSE2命令のエンコーディングロジック(mediaop
関数の追加とdoasm
関数の変更)の調整。
特にoptab.c
におけるoptab
配列への大量のSSE2命令エントリの追加と、span.c
におけるmediaop
関数およびdoasm
関数の変更は、SSE2命令の機械語エンコーディングを可能にする上で最も重要な部分です。
コアとなるコードの解説
src/cmd/8l/optab.c
における optab
の拡張
optab
配列は、リンカがアセンブリ命令を機械語に変換するための「辞書」のようなものです。各エントリはOptab
構造体で、以下の情報を含みます。
as
: アセンブリ命令コード(AADDPS
など)。ytype
: オペランドの型を示すuchar
配列へのポインタ(例:yxm
)。この配列は、命令が取りうるオペランドの組み合わせ(例:Yxm
(XMMレジスタまたはメモリ) とYxr
(XMMレジスタ))を定義します。prefix
: 命令に付加されるプレフィックスバイト(Pq
,Pm
,Pe
,Pf2
,Pf3
)。op
: 命令の主要なオペコードバイト。
例えば、{ AADDPS, yxm, Pm, 0x58 }
というエントリは、AADDPS
命令がyxm
で定義されたオペランドパターンを持ち、Pm
(0x0f) プレフィックスと0x58
のオペコードを持つことを示します。
src/cmd/8l/span.c
における mediaop
関数
static int
mediaop(Optab *o, int op, int osize, int z)
{
switch(op){
case Pm:
case Pe:
case Pf2:
case Pf3:
if(osize != 1){
if(op != Pm)
*andptr++ = op;
*andptr++ = Pm;
op = o->op[++z];
break;
}
default:
if(andptr == and || andptr[-1] != Pm)
*andptr++ = Pm;
break;
}
*andptr++ = op;
return z;
}
このmediaop
関数は、SSE/SSE2命令のエンコーディングにおいて、命令プレフィックスを適切に処理するために導入されました。
op
引数は、optab
エントリから取得されたプレフィックスバイト(Pm
,Pe
,Pf2
,Pf3
)またはオペコードバイトです。osize
はオペコードのサイズを示します。z
はoptab
のオペコード配列内のインデックスです。
この関数は、Pm
, Pe
, Pf2
, Pf3
といったSSE/SSE2特有のプレフィックスを検出した場合、それらをandptr
(機械語バイトを書き込むバッファ)に書き込みます。特に、Pf2
やPf3
のようなプレフィックスは、その後にPm
(0x0f) プレフィックスが続くことが多いため、そのロジックも含まれています。これにより、複雑なSSE/SSE2命令のエンコーディング規則が正しく適用されます。
src/cmd/8l/span.c
における doasm
関数の変更
doasm
関数は、個々のアセンブリ命令を機械語に変換する中心的なロジックを含んでいます。このコミットでは、新しいオペランドタイプ(Zm_r_xm
, Zm_r_i_xm
, Zr_m_xm
, Zr_m_i_xm
)を処理するcase
文が追加され、これらのケースでmediaop
関数が呼び出されるようになっています。
例えば、Zm_r_xm
(メモリ/XMMレジスタからXMMレジスタへの操作) の場合、mediaop
を呼び出してプレフィックスを処理した後、asmand
関数を使ってオペランドをエンコードします。これにより、SSE2命令の複雑なオペランドとプレフィックスの組み合わせが正しく機械語に変換されます。
これらの変更は、Go言語のリンカがSSE2命令を完全に理解し、Goプログラムがこれらの高性能な命令を透過的に利用できるようにするための基盤を構築しています。
関連リンク
- Go言語の公式ドキュメント: Go言語のツールチェインやアセンブリ言語に関する公式情報。
- https://go.dev/doc/
- https://go.dev/doc/asm (Go Assembly Language)
- Intel 64 and IA-32 Architectures Software Developer's Manuals: SSE/SSE2命令セットの詳細な仕様が記載されています。
参考にした情報源リンク
- Streaming SIMD Extensions 2 (SSE2) - Wikipedia: https://en.wikipedia.org/wiki/SSE2
- Go Assembly Language - Go Wiki: https://go.dev/wiki/Assembly
- Go's linker - Russ Cox's blog: https://research.swtch.com/go-linker (Goリンカの内部構造に関するRuss Cox氏のブログ記事)
- Go's assembler - Russ Cox's blog: https://research.swtch.com/go-asm (Goアセンブラの内部構造に関するRuss Cox氏のブログ記事)