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

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

このコミットは、Goコンパイラのcmd/6g(AMD64アーキテクチャ向けコンパイラ)におけるパフォーマンス改善を目的としています。具体的には、レジスタ間の浮動小数点数移動命令であるMOVSDMOVAPDに置き換えることで、コードの実行速度を向上させています。

コミット

commit a768de8347c4aab00e48d4566274198c2e35e9bd
Author: Russ Cox <rsc@golang.org>
Date:   Wed May 30 14:41:19 2012 -0400

    cmd/6g: avoid MOVSD between registers
    
    MOVSD only copies the low half of the packed register pair,
    while MOVAPD copies both halves.  I assume the internal
    register renaming works better with the latter, since it makes
    our code run 25% faster.
    
    Before:
    mandelbrot 16000
            gcc -O2 mandelbrot.c    28.44u 0.00s 28.45r
            gc mandelbrot   44.12u 0.00s 44.13r
            gc_B mandelbrot 44.17u 0.01s 44.19r
    
    After:
    mandelbrot 16000
            gcc -O2 mandelbrot.c    28.22u 0.00s 28.23r
            gc mandelbrot   32.81u 0.00s 32.82r
            gc_B mandelbrot 32.82u 0.00s 32.83r
    
    R=ken2
    CC=golang-dev
    https://golang.org/cl/6248068

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

https://github.com/golang/go/commit/a768de8347c4aab00e48d4566274198c2e35e9bd

元コミット内容

cmd/6g: avoid MOVSD between registers

MOVSDはパックドレジスタペアの下位半分のみをコピーしますが、MOVAPDは両方の半分をコピーします。後者の方が内部のレジスタリネーミングがより良く機能すると考えられ、これによりコードが25%高速化しました。

変更前: mandelbrot 16000 gcc -O2 mandelbrot.c 28.44u 0.00s 28.45r gc mandelbrot 44.12u 0.00s 44.13r gc_B mandelbrot 44.17u 0.01s 44.19r

変更後: mandelbrot 16000 gcc -O2 mandelbrot.c 28.22u 0.00s 28.23r gc mandelbrot 32.81u 0.00s 32.82r gc_B mandelbrot 32.82u 0.00s 32.83r

R=ken2 CC=golang-dev https://golang.org/cl/6248068

変更の背景

この変更の背景には、Goコンパイラ(特にcmd/6g、AMD64アーキテクチャ向け)が生成するアセンブリコードのパフォーマンス最適化があります。浮動小数点演算において、レジスタ間でデータを移動する際に使用される命令の選択が、CPUの内部動作に大きく影響することが判明しました。

具体的には、MOVSD命令が単精度または倍精度浮動小数点数の下位64ビット(または32ビット)のみを移動するのに対し、MOVAPD命令はパックド倍精度浮動小数点数(つまり、128ビットレジスタの2つの倍精度浮動小数点数)全体を移動します。Goコンパイラは、浮動小数点演算にSSE/SSE2命令セットを使用しますが、その際にMOVSDを多用していました。

しかし、現代のCPUは「レジスタリネーミング」という最適化技術を用いて、命令の並列実行を促進します。MOVSDのようにレジスタの一部のみを操作する命令は、CPUがレジスタの依存関係を正確に追跡する上で複雑さを増し、結果としてリネーミングの効率を低下させる可能性があります。一方で、MOVAPDのようにレジスタ全体を操作する命令は、CPUにとって依存関係の管理が容易であり、より効率的なレジスタリネーミングと命令の並列実行を可能にします。

このコミットは、mandelbrotベンチマーク(浮動小数点演算を多用する)において、Goコンパイラが生成したコードがGCCに比べて遅いという問題意識から生まれました。MOVSDMOVAPDに置き換えることで、CPUの内部パイプラインがより効率的に動作し、結果としてGoコンパイラが生成するコードの実行速度が大幅に向上することが期待されました。実際に、ベンチマークでは約25%の高速化が確認されています。

前提知識の解説

1. Goコンパイラとcmd/6g

Go言語は、その設計思想の一つとして高速なコンパイルを掲げています。Goのツールチェインには、各アーキテクチャに対応するコンパイラが含まれており、cmd/6gはAMD64(x86-64)アーキテクチャ向けのGoコンパイラを指します。このコンパイラは、GoのソースコードをAMD64のアセンブリコードに変換し、最終的に実行可能なバイナリを生成します。peep.cは、このコンパイラのバックエンドの一部であり、生成されたアセンブリコードに対して「peephole optimization(ピーフホール最適化)」と呼ばれる局所的な最適化を行う役割を担っています。

2. SSE/SSE2命令セットと浮動小数点レジスタ

IntelおよびAMDのx86-64アーキテクチャには、SIMD(Single Instruction, Multiple Data)演算を高速化するための拡張命令セットが導入されています。SSE(Streaming SIMD Extensions)およびSSE2はその代表的なもので、浮動小数点演算や整数演算を並列に処理する能力を提供します。

これらの命令は、専用の128ビットレジスタであるXMMレジスタ(XMM0からXMM15まで)を使用します。XMMレジスタは、複数の単精度浮動小数点数(32ビット)または倍精度浮動小数点数(64ビット)を「パック」して保持することができます。例えば、1つのXMMレジスタは2つの倍精度浮動小数点数、または4つの単精度浮動小数点数を格納できます。

3. MOVSDMOVAPD命令

  • MOVSD (Move Scalar Double-precision Floating-point): この命令は、倍精度浮動小数点数(64ビット)を移動するために使用されます。名前の「Scalar」が示すように、これはXMMレジスタの「下位半分」(つまり、最初の64ビット)のみを操作します。例えば、MOVSD XMM0, XMM1は、XMM1の下位64ビットをXMM0の下位64ビットにコピーし、XMM0の上位64ビットは変更しません。

  • MOVAPD (Move Aligned Packed Double-precision Floating-point): この命令は、パックド倍精度浮動小数点数(つまり、XMMレジスタ全体、128ビット)を移動するために使用されます。名前の「Packed」が示すように、これはXMMレジスタの全体を操作します。例えば、MOVAPD XMM0, XMM1は、XMM1の128ビット全体をXMM0の128ビット全体にコピーします。この命令は、メモリ上のデータが16バイト境界にアラインされていることを前提とします。

4. レジスタリネーミング (Register Renaming)

現代の高性能CPUは、アウトオブオーダー実行(Out-of-Order Execution)と呼ばれる技術を用いて、プログラムの命令を元の順序とは異なる順序で実行し、パイプラインのストール(停止)を減らしてスループットを向上させます。このアウトオブオーダー実行を可能にする重要な技術の一つが「レジスタリネーミング」です。

レジスタリネーミングは、プログラムが参照する「アーキテクチャレジスタ」(例: XMM0)を、CPU内部のより多くの「物理レジスタ」に動的にマッピングする技術です。これにより、異なる命令が同じアーキテクチャレジスタを使用しているように見えても、実際には異なる物理レジスタに割り当てられるため、WAR(Write-After-Read)やWAW(Write-After-Write)といったデータ依存性によるストールを回避し、より多くの命令を並列に実行できるようになります。

MOVSDのようにレジスタの一部のみを更新する命令は、CPUがレジスタの依存関係を追跡する上で複雑さを増す可能性があります。なぜなら、CPUはレジスタのどの部分が有効で、どの部分が古い値を持っているかを管理する必要があるからです。これにより、レジスタリネーミングの効率が低下し、結果として命令の並列実行が妨げられることがあります。一方、MOVAPDのようにレジスタ全体を更新する命令は、CPUにとって依存関係の管理が単純であり、より効率的なリネーミングと並列実行を促進します。

技術的詳細

このコミットの技術的な核心は、Goコンパイラのピーフホール最適化フェーズにおいて、特定の条件下でMOVSD命令をMOVAPD命令に置き換えることです。

Goコンパイラは、浮動小数点演算においてXMMレジスタを使用しますが、Goの型システムやコンパイラの内部表現では、通常、個々の倍精度浮動小数点数(float64)を扱います。そのため、XMMレジスタの128ビットのうち、実際に使用するのは下位64ビットのみであり、上位64ビットは未使用または意味のないデータを含んでいることがほとんどです。

従来のGoコンパイラは、レジスタ間でfloat64を移動する際に、この「下位64ビットのみを移動する」という性質に合致するMOVSD命令を選択していました。しかし、コミットメッセージが指摘するように、この選択は現代のCPUの内部動作、特にレジスタリネーミングの効率を阻害していました。

CPUは、MOVSDのような部分的なレジスタ書き込み命令に遭遇すると、そのレジスタの残りの部分(上位64ビット)が変更されないことを保証するために、追加の内部処理を行う必要があります。これは、レジスタリネーミングユニットが新しい物理レジスタを割り当てる際に、古い物理レジスタの上位部分と新しい物理レジスタの下位部分を結合するような複雑な操作を必要とする場合があります。このような操作は、パイプラインのストールを引き起こしたり、命令のスループットを低下させたりする可能性があります。

一方、MOVAPD命令はXMMレジスタ全体(128ビット)を移動します。GoコンパイラがXMMレジスタの上位64ビットを積極的に使用していなくても、MOVAPDを使用することで、CPUはレジスタ全体が新しい値で上書きされることを認識します。これにより、CPUはより単純なレジスタリネーミング戦略を採用でき、古い物理レジスタの残りの部分を考慮する必要がなくなります。結果として、命令のディスパッチと実行がよりスムーズになり、パイプラインの効率が向上します。

コミットメッセージに示されているベンチマーク結果は、この最適化の有効性を明確に示しています。mandelbrotベンチマークは浮動小数点演算を多用するため、この変更の影響が顕著に現れました。

  • 変更前: gc mandelbrot44.13r
  • 変更後: gc mandelbrot32.82r

これは、約25%の実行時間短縮に相当し、Goコンパイラが生成するコードのパフォーマンスが大幅に改善されたことを意味します。この最適化は、Go言語が科学技術計算や数値解析など、浮動小数点演算が重要な分野での利用を促進する上で重要な一歩となりました。

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

変更はsrc/cmd/6g/peep.cファイル内で行われています。

--- a/src/cmd/6g/peep.c
+++ b/src/cmd/6g/peep.c
@@ -283,6 +283,12 @@ loop1:
 	// copyprop.  Now that copyprop is done, remov MOVLQZX R1, R2
 	// if it is dominated by an earlier ADDL/MOVL/etc into R1 that
 	// will have already cleared the high bits.
+\t//
+\t// MOVSD removal.
+\t// We never use packed registers, so a MOVSD between registers
+\t// can be replaced by MOVAPD, which moves the pair of float64s
+\t// instead of just the lower one.  We only use the lower one, but
+\t// the processor can do better if we do moves using both.
 	for(r=firstr; r!=R; r=r->link) {
 	\tp = r->prog;
 	\tif(p->as == AMOVLQZX)
@@ -290,6 +296,11 @@ loop1:
 	\tif(p->from.type == p->to.type)\n \t\tif(prevl(r, p->from.type))\n \t\t\texcise(r);\n+\t\t\n+\t\tif(p->as == AMOVSD)\n+\t\tif(regtyp(&p->from))\n+\t\tif(regtyp(&p->to))\n+\t\t\tp->as = AMOVAPD;\n     	}\n     \n     	// load pipelining

追加されたコードは以下の部分です。

		if(p->as == AMOVSD)
		if(regtyp(&p->from))
		if(regtyp(&p->to))
			p->as = AMOVAPD;

コアとなるコードの解説

この変更は、peep.c内のloop1という最適化ループの中に挿入されています。このループは、生成されたアセンブリ命令(Prog構造体で表現される)を一つずつ走査し、特定のパターンに合致する命令に対して最適化を適用します。

追加されたコードブロックは、以下の条件をチェックします。

  1. if(p->as == AMOVSD): 現在処理している命令がMOVSD命令であるかどうかをチェックします。AMOVSDはGoコンパイラ内部でMOVSD命令を表す定数です。
  2. if(regtyp(&p->from)): MOVSD命令のソースオペランド(p->from)がレジスタであるかどうかをチェックします。regtyp関数は、与えられたアドレスがレジスタ型であるかを判定するヘルパー関数です。
  3. if(regtyp(&p->to)): MOVSD命令のデスティネーションオペランド(p->to)がレジスタであるかどうかをチェックします。

これら3つの条件がすべて真である場合、つまり、ソースとデスティネーションの両方がレジスタであるMOVSD命令が見つかった場合、以下の行が実行されます。

p->as = AMOVAPD;

この行は、現在の命令のオペコードをAMOVSDからAMOVAPDに書き換えます。これにより、コンパイラが最終的に出力するアセンブリコードにおいて、レジスタ間のMOVSD命令がMOVAPD命令に置き換えられることになります。

この変更は、GoコンパイラがXMMレジスタの上位64ビットを積極的に使用しないという前提に基づいています。そのため、MOVSDで下位64ビットのみを移動しても、MOVAPDで128ビット全体を移動しても、Goプログラムのセマンティクスには影響を与えません。しかし、前述の通り、CPUの内部動作においてはMOVAPDの方が効率的であるため、この置き換えがパフォーマンス向上に寄与します。

このピーフホール最適化は、コンパイラのバックエンドの非常に低いレベルで行われるため、Go言語のユーザーが直接意識することはありませんが、Goプログラムの実行速度に大きな影響を与える可能性があります。

関連リンク

参考にした情報源リンク

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

このコミットは、Goコンパイラのcmd/6g(AMD64アーキテクチャ向けコンパイラ)におけるパフォーマンス改善を目的としています。具体的には、レジスタ間の浮動小数点数移動命令であるMOVSDMOVAPDに置き換えることで、コードの実行速度を向上させています。

コミット

commit a768de8347c4aab00e48d4566274198c2e35e9bd
Author: Russ Cox <rsc@golang.org>
Date:   Wed May 30 14:41:19 2012 -0400

    cmd/6g: avoid MOVSD between registers
    
    MOVSD only copies the low half of the packed register pair,
    while MOVAPD copies both halves.  I assume the internal
    register renaming works better with the latter, since it makes
    our code run 25% faster.
    
    Before:
    mandelbrot 16000
            gcc -O2 mandelbrot.c    28.44u 0.00s 28.45r
            gc mandelbrot   44.12u 0.00s 44.13r
            gc_B mandelbrot 44.17u 0.01s 44.19r
    
    After:
    mandelbrot 16000
            gcc -O2 mandelbrot.c    28.22u 0.00s 28.23r
            gc mandelbrot   32.81u 0.00s 32.82r
            gc_B mandelbrot 32.82u 0.00s 32.83r
    
    R=ken2
    CC=golang-dev
    https://golang.org/cl/6248068

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

https://github.com/golang/go/commit/a768de8347c4aab00e48d4566274198c2e35e9bd

元コミット内容

cmd/6g: avoid MOVSD between registers

MOVSDはパックドレジスタペアの下位半分のみをコピーしますが、MOVAPDは両方の半分をコピーします。後者の方が内部のレジスタリネーミングがより良く機能すると考えられ、これによりコードが25%高速化しました。

変更前: mandelbrot 16000 gcc -O2 mandelbrot.c 28.44u 0.00s 28.45r gc mandelbrot 44.12u 0.00s 44.13r gc_B mandelbrot 44.17u 0.01s 44.19r

変更後: mandelbrot 16000 gcc -O2 mandelbrot.c 28.22u 0.00s 28.23r gc mandelbrot 32.81u 0.00s 32.82r gc_B mandelbrot 32.82u 0.00s 32.83r

R=ken2 CC=golang-dev https://golang.org/cl/6248068

変更の背景

この変更の背景には、Goコンパイラ(特にcmd/6g、AMD64アーキテクチャ向け)が生成するアセンブリコードのパフォーマンス最適化があります。浮動小数点演算において、レジスタ間でデータを移動する際に使用される命令の選択が、CPUの内部動作に大きく影響することが判明しました。

具体的には、MOVSD命令が単精度または倍精度浮動小数点数の下位64ビット(または32ビット)のみを移動するのに対し、MOVAPD命令はパックド倍精度浮動小数点数(つまり、128ビットレジスタの2つの倍精度浮動小数点数)全体を移動します。Goコンパイラは、浮動小数点演算にSSE/SSE2命令セットを使用しますが、その際にMOVSDを多用していました。

しかし、現代のCPUは「レジスタリネーミング」という最適化技術を用いて、命令の並列実行を促進します。MOVSDのようにレジスタの一部のみを操作する命令は、CPUがレジスタの依存関係を正確に追跡する上で複雑さを増し、結果としてリネーミングの効率を低下させる可能性があります。一方で、MOVAPDのようにレジスタ全体を操作する命令は、CPUにとって依存関係の管理が容易であり、より効率的なレジスタリネーミングと命令の並列実行を可能にします。

このコミットは、mandelbrotベンチマーク(浮動小数点演算を多用する)において、Goコンパイラが生成したコードがGCCに比べて遅いという問題意識から生まれました。MOVSDMOVAPDに置き換えることで、CPUの内部パイプラインがより効率的に動作し、結果としてGoコンパイラが生成するコードの実行速度が大幅に向上することが期待されました。実際に、ベンチマークでは約25%の高速化が確認されています。

前提知識の解説

1. Goコンパイラとcmd/6g

Go言語は、その設計思想の一つとして高速なコンパイルを掲げています。Goのツールチェインには、各アーキテクチャに対応するコンパイラが含まれており、cmd/6gはAMD64(x86-64)アーキテクチャ向けのGoコンパイラを指します。このコンパイラは、GoのソースコードをAMD64のアセンブリコードに変換し、最終的に実行可能なバイナリを生成します。peep.cは、このコンパイラのバックエンドの一部であり、生成されたアセンブリコードに対して「peephole optimization(ピーフホール最適化)」と呼ばれる局所的な最適化を行う役割を担っています。

2. SSE/SSE2命令セットと浮動小数点レジスタ

IntelおよびAMDのx86-64アーキテクチャには、SIMD(Single Instruction, Multiple Data)演算を高速化するための拡張命令セットが導入されています。SSE(Streaming SIMD Extensions)およびSSE2はその代表的なもので、浮動小数点演算や整数演算を並列に処理する能力を提供します。

これらの命令は、専用の128ビットレジスタであるXMMレジスタ(XMM0からXMM15まで)を使用します。XMMレジスタは、複数の単精度浮動小数点数(32ビット)または倍精度浮動小数点数(64ビット)を「パック」して保持することができます。例えば、1つのXMMレジスタは2つの倍精度浮動小数点数、または4つの単精度浮動小数点数を格納できます。

3. MOVSDMOVAPD命令

  • MOVSD (Move Scalar Double-precision Floating-point): この命令は、倍精度浮動小数点数(64ビット)を移動するために使用されます。名前の「Scalar」が示すように、これはXMMレジスタの「下位半分」(つまり、最初の64ビット)のみを操作します。例えば、MOVSD XMM0, XMM1は、XMM1の下位64ビットをXMM0の下位64ビットにコピーし、XMM0の上位64ビットは変更しません。

  • MOVAPD (Move Aligned Packed Double-precision Floating-point): この命令は、パックド倍精度浮動小数点数(つまり、XMMレジスタ全体、128ビット)を移動するために使用されます。名前の「Packed」が示すように、これはXMMレジスタの全体を操作します。例えば、MOVAPD XMM0, XMM1は、XMM1の128ビット全体をXMM0の128ビット全体にコピーします。この命令は、メモリ上のデータが16バイト境界にアラインされていることを前提とします。

4. レジスタリネーミング (Register Renaming)

現代の高性能CPUは、アウトオブオーダー実行(Out-of-Order Execution)と呼ばれる技術を用いて、プログラムの命令を元の順序とは異なる順序で実行し、パイプラインのストール(停止)を減らしてスループットを向上させます。このアウトオブオーダー実行を可能にする重要な技術の一つが「レジスタリネーミング」です。

レジスタリネーミングは、プログラムが参照する「アーキテクチャレジスタ」(例: XMM0)を、CPU内部のより多くの「物理レジスタ」に動的にマッピングする技術です。これにより、異なる命令が同じアーキテクチャレジスタを使用しているように見えても、実際には異なる物理レジスタに割り当てられるため、WAR(Write-After-Read)やWAW(Write-After-Write)といったデータ依存性によるストールを回避し、より多くの命令を並列に実行できるようになります。

MOVSDのようにレジスタの一部のみを更新する命令は、CPUがレジスタの依存関係を追跡する上で複雑さを増す可能性があります。なぜなら、CPUはレジスタのどの部分が有効で、どの部分が古い値を持っているかを管理する必要があるからです。これは、レジスタリネーミングユニットが新しい物理レジスタを割り当てる際に、古い物理レジスタの上位部分と新しい物理レジスタの下位部分を結合するような複雑な操作を必要とする場合があります。このような操作は、パイプラインのストールを引き起こしたり、命令のスループットを低下させたりする可能性があります。

一方、MOVAPDのようにレジスタ全体を更新する命令は、CPUにとって依存関係の管理が単純であり、より効率的なリネーミングと並列実行を促進します。

技術的詳細

このコミットの技術的な核心は、Goコンパイラのピーフホール最適化フェーズにおいて、特定の条件下でMOVSD命令をMOVAPD命令に置き換えることです。

Goコンパイラは、浮動小数点演算においてXMMレジスタを使用しますが、Goの型システムやコンパイラの内部表現では、通常、個々の倍精度浮動小数点数(float64)を扱います。そのため、XMMレジスタの128ビットのうち、実際に使用するのは下位64ビットのみであり、上位64ビットは未使用または意味のないデータを含んでいることがほとんどです。

従来のGoコンパイラは、レジスタ間でfloat64を移動する際に、この「下位64ビットのみを移動する」という性質に合致するMOVSD命令を選択していました。しかし、コミットメッセージが指摘するように、この選択は現代のCPUの内部動作、特にレジスタリネーミングの効率を阻害していました。

CPUは、MOVSDのような部分的なレジスタ書き込み命令に遭遇すると、そのレジスタの残りの部分(上位64ビット)が変更されないことを保証するために、追加の内部処理を行う必要があります。これは、レジスタリネーミングユニットが新しい物理レジスタを割り当てる際に、古い物理レジスタの上位部分と新しい物理レジスタの下位部分を結合するような複雑な操作を必要とする場合があります。このような操作は、パイプラインのストールを引き起こしたり、命令のスループットを低下させたりする可能性があります。

一方、MOVAPD命令はXMMレジスタ全体(128ビット)を移動します。GoコンパイラがXMMレジスタの上位64ビットを積極的に使用していなくても、MOVAPDを使用することで、CPUはレジスタ全体が新しい値で上書きされることを認識します。これにより、CPUはより単純なレジスタリネーミング戦略を採用でき、古い物理レジスタの残りの部分を考慮する必要がなくなります。結果として、命令のディスパッチと実行がよりスムーズになり、パイプラインの効率が向上します。

コミットメッセージに示されているベンチマーク結果は、この最適化の有効性を明確に示しています。mandelbrotベンチマークは浮動小数点演算を多用するため、この変更の影響が顕著に現れました。

  • 変更前: gc mandelbrot44.13r
  • 変更後: gc mandelbrot32.82r

これは、約25%の実行時間短縮に相当し、Goコンパイラが生成するコードのパフォーマンスが大幅に改善されたことを意味します。この最適化は、Go言語が科学技術計算や数値解析など、浮動小数点演算が重要な分野での利用を促進する上で重要な一歩となりました。

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

変更はsrc/cmd/6g/peep.cファイル内で行われています。

--- a/src/cmd/6g/peep.c
+++ b/src/cmd/6g/peep.c
@@ -283,6 +283,12 @@ loop1:
 	// copyprop.  Now that copyprop is done, remov MOVLQZX R1, R2
 	// if it is dominated by an earlier ADDL/MOVL/etc into R1 that
 	// will have already cleared the high bits.
+\t//
+\t// MOVSD removal.
+\t// We never use packed registers, so a MOVSD between registers
+\t// can be replaced by MOVAPD, which moves the pair of float64s
+\t// instead of just the lower one.  We only use the lower one, but
+\t// the processor can do better if we do moves using both.
 	for(r=firstr; r!=R; r=r->link) {
 	\tp = r->prog;
 	\tif(p->as == AMOVLQZX)
@@ -290,6 +296,11 @@ loop1:
 	\tif(p->from.type == p->to.type)\n \t\tif(prevl(r, p->from.type))\n \t\t\texcise(r);\n+\t\t\n+\t\tif(p->as == AMOVSD)\n+\t\tif(regtyp(&p->from))\n+\t\tif(regtyp(&p->to))\n+\t\t\tp->as = AMOVAPD;\n     	}\n     \n     	// load pipelining

追加されたコードは以下の部分です。

		if(p->as == AMOVSD)
		if(regtyp(&p->from))
		if(regtyp(&p->to))
			p->as = AMOVAPD;

コアとなるコードの解説

この変更は、peep.c内のloop1という最適化ループの中に挿入されています。このループは、生成されたアセンブリ命令(Prog構造体で表現される)を一つずつ走査し、特定のパターンに合致する命令に対して最適化を適用します。

追加されたコードブロックは、以下の条件をチェックします。

  1. if(p->as == AMOVSD): 現在処理している命令がMOVSD命令であるかどうかをチェックします。AMOVSDはGoコンパイラ内部でMOVSD命令を表す定数です。
  2. if(regtyp(&p->from)): MOVSD命令のソースオペランド(p->from)がレジスタであるかどうかをチェックします。regtyp関数は、与えられたアドレスがレジスタ型であるかを判定するヘルパー関数です。
  3. if(regtyp(&p->to)): MOVSD命令のデスティネーションオペランド(p->to)がレジスタであるかどうかをチェックします。

これら3つの条件がすべて真である場合、つまり、ソースとデスティネーションの両方がレジスタであるMOVSD命令が見つかった場合、以下の行が実行されます。

p->as = AMOVAPD;

この行は、現在の命令のオペコードをAMOVSDからAMOVAPDに書き換えます。これにより、コンパイラが最終的に出力するアセンブリコードにおいて、レジスタ間のMOVSD命令がMOVAPD命令に置き換えられることになります。

この変更は、GoコンパイラがXMMレジスタの上位64ビットを積極的に使用しないという前提に基づいています。そのため、MOVSDで下位64ビットのみを移動しても、MOVAPDで128ビット全体を移動しても、Goプログラムのセマンティクスには影響を与えません。しかし、前述の通り、CPUの内部動作においてはMOVAPDの方が効率的であるため、この置き換えがパフォーマンス向上に寄与します。

このピーフホール最適化は、コンパイラのバックエンドの非常に低いレベルで行われるため、Go言語のユーザーが直接意識することはありませんが、Goプログラムの実行速度に大きな影響を与える可能性があります。

関連リンク

参考にした情報源リンク