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

[インデックス 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レジスタ(XMM0XMM7)を使用します。
  • SSE2 (Streaming SIMD Extensions 2): Pentium 4で導入され、SSEをさらに拡張したものです。
    • 倍精度浮動小数点数(64ビット)のSIMD演算をサポートします。
    • 整数演算のSIMD演算をサポートします。
    • 引き続き8つの128ビットXMMレジスタを使用します。

SSE/SSE2命令は、通常のx86命令とは異なるエンコーディングを持ち、特定のプレフィックスバイト(例: 0x660xF20xF3)を必要とすることがあります。これらのプレフィックスは、命令がどのデータ型(単精度、倍精度、整数)を扱うか、あるいはどの命令セットに属するかを示すために使用されます。

リンカの役割と命令エンコーディング

リンカは、アセンブリコードが表現する命令を、CPUが直接実行できるバイナリ形式(機械語)に変換する重要な役割を担います。この変換には、各命令に対応するオペコード(操作コード)や、オペランド(操作対象のデータやレジスタ)のエンコーディング規則に関する詳細な知識が必要です。

リンカ内部では、通常、命令の種類、オペランドの型、アドレッシングモードなどに基づいて、適切な機械語バイト列を生成するためのテーブル(optabのようなもの)が管理されています。新しい命令セット(SSE2)をサポートするということは、これらのテーブルに新しいエントリを追加し、それらの命令を正しくエンコードするためのロジックを実装することを意味します。

技術的詳細

このコミットは、Go言語のx86リンカ cmd/8l にSSE2命令のサポートを統合するために、複数のファイルにわたる広範な変更を加えています。

  1. 命令コードとレジスタの定義追加 (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レジスタ(XMM0XMM7)に対応します。既存のD_NONE以降の定数値がシフトされています。
  2. 内部定数とオペランドタイプの拡張 (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命令のエンコーディングに必要なバイトです(例: 0xF2REPNEプレフィックスとして、0xF3REPEプレフィックスとして使用されることがありますが、SSE/SSE2では異なる意味を持ちます)。
  3. レジスタ名の文字列化対応 (src/cmd/8l/list.c):

    • リンカがデバッグ出力などでレジスタ名を表示する際に使用する regstr 配列に、X0 から X7 までのXMMレジスタの文字列表現が追加されています。
  4. 浮動小数点定数処理の拡張 (src/cmd/8l/obj.c):

    • obj.c はオブジェクトファイルの生成に関連するロジックを含んでいます。この変更では、AMOVSS, AADDSS, AMOVSD, AADDSD などのSSE2命令が、既存の浮動小数点命令と同様に、D_FCONST (浮動小数点定数) をオペランドとして扱う場合の処理に追加されています。これは、リンカが浮動小数点定数を適切に処理し、機械語に埋め込むためのものです。
  5. 命令テーブルの更新とオペランドパターンの定義 (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レジスタ)を取るかを詳細に記述しています。
  6. 命令初期化とエンコーディングロジックの調整 (src/cmd/8l/span.c):

    • instinit 関数は、リンカの初期化時に命令テーブルの整合性をチェックし、レジスタのカバー範囲を定義します。このコミットでは、ycover配列にXMMレジスタ(Ymm, Yxm)のカバー範囲が追加され、reg配列にD_X0からD_X7までのXMMレジスタが初期化されています。
    • oclass 関数は、アドレスのタイプに基づいてオペランドのクラスを返します。XMMレジスタ(D_X0D_X7)に対してYxrを返すように更新されています。
    • asmand 関数は、オペランドを機械語にエンコードする際に使用されます。XMMレジスタ(D_X0D_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はオペコードのサイズを示します。
  • zoptabのオペコード配列内のインデックスです。

この関数は、Pm, Pe, Pf2, Pf3といったSSE/SSE2特有のプレフィックスを検出した場合、それらをandptr(機械語バイトを書き込むバッファ)に書き込みます。特に、Pf2Pf3のようなプレフィックスは、その後に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プログラムがこれらの高性能な命令を透過的に利用できるようにするための基盤を構築しています。

関連リンク

参考にした情報源リンク