[インデックス 13250] ファイルの概要
このコミットは、Go言語の math
パッケージにおける Ceil
(切り上げ), Floor
(切り捨て), Trunc
(小数点以下切り捨て) 関数の amd64
アーキテクチャ向けアセンブリ実装を最適化し、パフォーマンスを大幅に向上させるものです。また、log_amd64.s
ファイル内の浮動小数点数移動命令を MOVSD
から MOVAPD
に変更することで、さらなる最適化を行っています。
コミット
commit 322057cbfce3c9c295aef4b87d1bf689f75c345f
Author: Charles L. Dorian <cldorian@gmail.com>
Date: Sat Jun 2 13:06:12 2012 -0400
math: amd64 versions of Ceil, Floor and Trunc
Ceil to 4.81 from 20.6 ns/op
Floor to 4.37 from 13.5 ns/op
Trunc to 3.97 from 14.3 ns/op
Also changed three MOVSDs to MOVAPDs in log_amd64.s
R=rsc, golang-dev
CC=golang-dev
https://golang.org/cl/6262048
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/322057cbfce3c9c295aef4b87d1bf689f75c345f
元コミット内容
Go言語の math
パッケージにおいて、amd64
アーキテクチャ向けに Ceil
, Floor
, Trunc
関数のアセンブリ実装を更新しました。これにより、各関数の実行速度が大幅に改善されました。
Ceil
: 20.6 ns/op から 4.81 ns/op へFloor
: 13.5 ns/op から 4.37 ns/op へTrunc
: 14.3 ns/op から 3.97 ns/op へ
また、log_amd64.s
ファイル内で使用されていた MOVSD
命令3箇所を MOVAPD
命令に変更しました。
変更の背景
このコミットの主な背景は、Go言語の標準ライブラリである math
パッケージの浮動小数点演算関数のパフォーマンス改善です。特に Ceil
, Floor
, Trunc
といった関数は、数値計算を多用するアプリケーションにおいて頻繁に呼び出される可能性があり、これらの関数の効率は全体のパフォーマンスに大きく影響します。
以前の実装では、これらの関数はより汎用的なコードパスを使用していたか、あるいは amd64
アーキテクチャの特定の命令セットを十分に活用していなかった可能性があります。アセンブリ言語で直接実装を最適化することで、CPUの浮動小数点演算ユニット(FPU)を最大限に活用し、命令レベルでの効率化を図ることが目的です。
log_amd64.s
における MOVSD
から MOVAPD
への変更も、同様にパフォーマンス改善を目的としています。これは、特定のデータ移動命令が、より効率的な代替命令に置き換えられることで、パイプラインの効率やレジスタの利用が改善されるためです。
前提知識の解説
1. amd64
アーキテクチャとアセンブリ言語
amd64
(x86-64) は、現在のパーソナルコンピュータやサーバーで広く使われている64ビットCPUアーキテクチャです。アセンブリ言語は、CPUが直接実行できる機械語命令を人間が読める形式で記述した低レベル言語です。Go言語では、パフォーマンスが重要な部分や、特定のハードウェア機能を利用するために、一部の標準ライブラリ関数がアセンブリ言語で実装されています。
2. 浮動小数点数とIEEE 754
浮動小数点数は、実数をコンピュータで表現するための形式で、通常はIEEE 754標準に従います。Go言語の float64
型は、この標準の倍精度浮動小数点数(64ビット)に対応しています。
3. Ceil
, Floor
, Trunc
関数
Ceil(x)
:x
以上の最小の整数値を返します。例:Ceil(3.14) = 4.0
,Ceil(-3.14) = -3.0
Floor(x)
:x
以下の最大の整数値を返します。例:Floor(3.14) = 3.0
,Floor(-3.14) = -4.0
Trunc(x)
:x
の小数点以下を切り捨てた整数値を返します。これは、ゼロ方向への丸めとも呼ばれます。例:Trunc(3.14) = 3.0
,Trunc(-3.14) = -3.0
これらの関数は、数学的な丸め処理において基本的な操作です。
4. SSE/SSE2 命令セットと浮動小数点命令
amd64
プロセッサには、SIMD (Single Instruction, Multiple Data) 処理を可能にするSSE (Streaming SIMD Extensions) およびSSE2命令セットが含まれています。これらは、複数のデータ要素に対して単一の命令で操作を行うことができ、特に浮動小数点演算の高速化に寄与します。
このコミットで登場する主要な命令は以下の通りです。
MOVSD
(Move Scalar Double-precision Floating-Point Value): XMMレジスタとメモリ間で、倍精度浮動小数点数(64ビット)を1つ転送します。スカラー(単一)値の移動に使用されます。MOVAPD
(Move Aligned Packed Double-precision Floating-Point Values): XMMレジスタとメモリ間で、アラインされたパックド倍精度浮動小数点数(128ビット、つまり2つの倍精度浮動小数点数)を転送します。この命令は、データが16バイト境界にアラインされていることを前提とします。CVTTSD2SQ
(Convert Truncate Scalar Double-precision Floating-Point to Signed Quadword Integer): 倍精度浮動小数点数を、ゼロ方向への丸め(Truncate)を行い、64ビット符号付き整数に変換します。CVTSQ2SD
(Convert Signed Quadword Integer to Scalar Double-precision Floating-Point): 64ビット符号付き整数を、倍精度浮動小数点数に変換します。CMPSD
(Compare Scalar Double-precision Floating-Point): 2つの倍精度浮動小数点数を比較し、結果をフラグレジスタに設定します。オペランドによって比較の種類(等しい、より小さい、より大きいなど)を指定できます。ANDPD
(Bitwise Logical AND of Packed Double-precision Floating-Point Values): 2つのXMMレジスタのパックド倍精度浮動小数点値に対してビット単位のAND演算を行います。ORPD
(Bitwise Logical OR of Packed Double-precision Floating-Point Values): 2つのXMMレジスタのパックド倍精度浮動小数点値に対してビット単位のOR演算を行います。ADDSD
(Add Scalar Double-precision Floating-Point Values): 2つの倍精度浮動小数点数を加算します。MULSD
(Multiply Scalar Double-precision Floating-Point Values): 2つの倍精度浮動小数点数を乗算します。
5. Goのアセンブリ構文
Go言語のアセンブリは、AT&T構文とIntel構文の中間のような独自の構文を使用します。レジスタ名には %
プレフィックスがなく、オペランドの順序はIntel構文に似ています(DEST, SRC
)。関数は TEXT
ディレクティブで定義され、SB
(Static Base) はグローバルシンボルへのオフセットを示します。FP
(Frame Pointer) は関数の引数やローカル変数へのアクセスに使用されます。
技術的詳細
このコミットの技術的詳細は、主に floor_amd64.s
における Ceil
, Floor
, Trunc
の新しいアセンブリ実装と、log_amd64.s
における MOVSD
から MOVAPD
への変更にあります。
Ceil
, Floor
, Trunc
の最適化
以前のバージョンでは、これらの関数は単に別の内部関数(·floor
, ·ceil
, ·trunc
)にジャンプするだけでした。これは、おそらくGoのランタイムが提供する汎用的な浮動小数点丸め関数を呼び出していたか、あるいはC言語で実装された関数を呼び出していた可能性があります。
新しい実装では、これらの関数が直接 amd64
アセンブリで記述され、SSE2命令を積極的に利用しています。基本的なロジックは以下のステップで構成されます。
- 入力値
x
の取得:MOVQ x+0(FP), AX
で引数x
をAX
レジスタにロードします。 - 絶対値の計算と特殊ケースの処理:
Big
定数 (0x4330000000000000
、これは2^52
を表す浮動小数点数) を定義しています。|x| >= 2^52
の場合、またはx
がNaN
(Not a Number) の場合、あるいはx
が0
の場合、x
をそのまま返すという最適化が行われています。これは、2^52
以上の浮動小数点数では、整数部分がすべて表現可能であり、丸め処理が不要になるためです。IsNaN
のチェックも含まれています。MOVQ ~(1<<63), DX
で符号ビット以外のマスクを作成し、ANDQ AX, DX
でx
の絶対値を取得します。CMPQ
命令で|x|
とBig
定数を比較し、条件付きジャンプ (JAE
) で特殊ケース (isBig_floor
,isBig_ceil
,isBig_trunc
) に分岐します。
- 浮動小数点数から整数への変換 (Truncation):
MOVQ AX, X0
でx
をXMMレジスタX0
に移動します。CVTTSD2SQ X0, AX
命令は、X0
の倍精度浮動小数点数をゼロ方向への丸め(Truncate)を行い、結果を64ビット符号付き整数としてAX
レジスタに格納します。
- 整数から浮動小数点数への変換:
CVTSQ2SD AX, X1
(またはX0
forTrunc
) 命令は、AX
レジスタの64ビット符号付き整数を倍精度浮動小数点数に変換し、X1
(またはX0
) に格納します。これにより、float(int(x))
に相当する値が得られます。
- 丸めロジックの適用:
Floor
:x
とfloat(int(x))
を比較します。もしx < float(int(x))
であれば、x
は負の小数部分を持つため、結果から1.0
を引く必要があります。CMPSD X1, X0, 1
(compare LT) を使用して比較し、結果に応じて-1.0
または0.0
を生成し、float(int(x))
に加算します。Ceil
:x
とfloat(int(x))
を比較します。もしfloat(int(x)) <= x
であれば、x
は正の小数部分を持つため、結果に1.0
を加算する必要があります。CMPSD X1, X0, 2
(compare LE) を使用して比較し、結果に応じて1.0
または0.0
を生成し、float(int(x))
に加算します。符号付きゼロ (-0.0
) の扱いにも注意が払われています。Trunc
:CVTTSD2SQ
とCVTSQ2SD
の組み合わせにより、既にゼロ方向への丸めが行われているため、追加の調整は不要です。符号付きゼロのケース (-0.0
) を正しく扱うために、元の符号ビットを結果に適用する処理 (ORPD X2, X0
) が行われます。
- 結果の返却: 最終的な結果を
r+8(FP)
(戻り値の格納場所) に格納し、RET
で関数から戻ります。
log_amd64.s
における MOVSD
から MOVAPD
への変更
src/pkg/math/log_amd64.s
ファイルでは、Log
関数の実装において、3箇所の MOVSD
命令が MOVAPD
に変更されています。
MOVSD X2, X3
->MOVAPD X2, X3
MOVSD X3, X4
->MOVAPD X3, X4
MOVSD X4, X5
->MOVAPD X4, X5
この変更は、パフォーマンスの最適化を目的としています。
MOVSD
はスカラー(単一の倍精度浮動小数点数)を移動する命令ですが、MOVAPD
はパックド(複数の倍精度浮動小数点数、この場合は2つ)を移動する命令であり、データが16バイト境界にアラインされていることを前提とします。
Goのアセンブリでは、XMMレジスタ間の移動において、MOVSD
は下位64ビットのみを移動し、上位64ビットは変更しません。一方、MOVAPD
はXMMレジスタ全体(128ビット)を移動します。
この特定のコンテキストでは、X2
, X3
, X4
, X5
は浮動小数点演算の中間結果を保持するXMMレジスタです。これらのレジスタが常に128ビット全体で有効な浮動小数点データ(またはその一部がゼロなど)を保持している場合、MOVAPD
を使用することで、より効率的なデータ転送が可能になります。特に、コンパイラやアセンブラがレジスタのアラインメントを保証できる場合、MOVAPD
は MOVSD
よりも高速に実行される可能性があります。これは、CPUの内部パイプラインやキャッシュの動作に起因するものです。
この変更は、Log
関数の計算フローにおいて、中間結果のレジスタ間コピーをより効率的に行うことで、全体の実行時間を短縮することを狙っています。
コアとなるコードの変更箇所
src/pkg/math/floor_amd64.s
--- a/src/pkg/math/floor_amd64.s
+++ b/src/pkg/math/floor_amd64.s
@@ -1,12 +1,74 @@
-// Copyright 2011 The Go Authors. All rights reserved.
+// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
+#define Big 0x4330000000000000 // 2**52
+
+// func Floor(x float64) float64
+TEXT ·Floor(SB),7,$0
+- JMP ·floor(SB)
++ MOVQ x+0(FP), AX
++ MOVQ $~(1<<63), DX // sign bit mask
++ ANDQ AX,DX // DX = |x|
++ SUBQ $1,DX
++ MOVQ $(Big - 1), CX // if |x| >= 2**52-1 or IsNaN(x) or |x| == 0, return x
++ CMPQ DX,CX
++ JAE isBig_floor
++ MOVQ AX, X0 // X0 = x
++ CVTTSD2SQ X0, AX
++ CVTSQ2SD AX, X1 // X1 = float(int(x))
++ CMPSD X1, X0, 1 // compare LT; X0 = 0xffffffffffffffff or 0
++ MOVSD $(-1.0), X2
++ ANDPD X2, X0 // if x < float(int(x)) {X0 = -1} else {X0 = 0}
++ ADDSD X1, X0
++ MOVSD X0, r+8(FP)
++ RET
++isBig_floor:
++ MOVQ AX, r+8(FP) // return x
++ RET
+
+// func Ceil(x float64) float64
+TEXT ·Ceil(SB),7,$0
+- JMP ·ceil(SB)
++ MOVQ x+0(FP), AX
++ MOVQ $~(1<<63), DX // sign bit mask
++ MOVQ AX, BX // BX = copy of x
++ ANDQ DX, BX // BX = |x|
++ MOVQ $Big, CX // if |x| >= 2**52 or IsNaN(x), return x
++ CMPQ BX, CX
++ JAE isBig_ceil
++ MOVQ AX, X0 // X0 = x
++ MOVQ DX, X2 // X2 = sign bit mask
++ CVTTSD2SQ X0, AX
++ ANDNPD X0, X2 // X2 = sign
++ CVTSQ2SD AX, X1 // X1 = float(int(x))
++ CMPSD X1, X0, 2 // compare LE; X0 = 0xffffffffffffffff or 0
++ ORPD X2, X1 // if X1 = 0.0, incorporate sign
++ MOVSD $1.0, X3
++ ANDNPD X3, X0
++ ORPD X2, X0 // if float(int(x)) <= x {X0 = 1} else {X0 = -0}
++ ADDSD X1, X0
++ MOVSD X0, r+8(FP)
++ RET
++isBig_ceil:
++ MOVQ AX, r+8(FP)
++ RET
+
+// func Trunc(x float64) float64
+TEXT ·Trunc(SB),7,$0
+- JMP ·trunc(SB)
++ MOVQ x+0(FP), AX
++ MOVQ $~(1<<63), DX // sign bit mask
++ MOVQ AX, BX // BX = copy of x
++ ANDQ DX, BX // BX = |x|
++ MOVQ $Big, CX // if |x| >= 2**52 or IsNaN(x), return x
++ CMPQ BX, CX
++ JAE isBig_trunc
++ MOVQ AX, X0
++ MOVQ DX, X2 // X2 = sign bit mask
++ CVTTSD2SQ X0, AX
++ ANDNPD X0, X2 // X2 = sign
++ CVTSQ2SD AX, X0 // X0 = float(int(x))
++ ORPD X2, X0 // if X0 = 0.0, incorporate sign
++ MOVSD X0, r+8(FP)
++ RET
++isBig_trunc:
++ MOVQ AX, r+8(FP) // return x
++ RET
src/pkg/math/log_amd64.s
--- a/src/pkg/math/log_amd64.s
+++ b/src/pkg/math/log_amd64.s
@@ -54,13 +54,13 @@ TEXT ·Log(SB),7,$0
// s := f / (2 + f)
MOVSD $2.0, X0
ADDSD X2, X0
- MOVSD X2, X3
+ MOVAPD X2, X3
DIVSD X0, X3 // x1=k, x2= f, x3= s
// s2 := s * s
- MOVSD X3, X4 // x1= k, x2= f, x3= s
+ MOVAPD X3, X4 // x1= k, x2= f, x3= s
MULSD X4, X4 // x1= k, x2= f, x3= s, x4= s2
// s4 := s2 * s2
- MOVSD X4, X5 // x1= k, x2= f, x3= s, x4= s2
+ MOVAPD X4, X5 // x1= k, x2= f, x3= s, x4= s2
MULSD X5, X5 // x1= k, x2= f, x3= s, x4= s2, x5= s4
// t1 := s2 * (L1 + s4*(L3+s4*(L5+s4*L7)))
MOVSD $L7, X6
コアとなるコードの解説
src/pkg/math/floor_amd64.s
の変更点
このファイルでは、Floor
, Ceil
, Trunc
の各関数が、以前の単純なジャンプ命令 (JMP ·floor(SB)
) から、amd64
アセンブリによる詳細な実装に置き換えられています。
共通のパターン:
- 引数
x
のロード:MOVQ x+0(FP), AX
で、スタックフレームポインタFP
から引数x
の値をAX
レジスタにロードします。 - 符号ビットのマスク:
MOVQ ~(1<<63), DX
で、64ビット値の最上位ビット(符号ビット)を0にするマスクを作成します。 - 絶対値の計算:
ANDQ AX, DX
(またはANDQ DX, BX
forCeil
/Trunc
) で、x
の絶対値を取得します。 - 特殊ケース (
Big
定数による高速パス):#define Big 0x4330000000000000 // 2**52
が定義されています。これは、float64
型で2^52
を表す値です。IEEE 754 倍精度浮動小数点数では、2^52
以上の整数は正確に表現できます。CMPQ
命令で|x|
とBig
定数(またはBig - 1
)を比較し、JAE
(Jump if Above or Equal) でisBig_xxx
ラベルにジャンプします。isBig_xxx
ラベルでは、入力x
をそのまま戻り値として返します (MOVQ AX, r+8(FP)
->RET
)。これは、|x|
が非常に大きい場合、丸め処理が不要になるため、計算をスキップして高速化を図るものです。また、NaN
やInf
(無限大) のような特殊な浮動小数点値もこのパスで処理されます。
- 浮動小数点数と整数の変換:
MOVQ AX, X0
でx
をXMMレジスタX0
に移動します。CVTTSD2SQ X0, AX
:X0
の倍精度浮動小数点数を、ゼロ方向への丸め(Truncate)を行い、64ビット符号付き整数としてAX
レジスタに格納します。CVTSQ2SD AX, X1
(またはX0
):AX
レジスタの64ビット符号付き整数を倍精度浮動小数点数に変換し、X1
(またはX0
) に格納します。これにより、float(int(x))
に相当する値が得られます。
Floor
の詳細:
MOVQ AX, X0 // X0 = x
CVTTSD2SQ X0, AX
CVTSQ2SD AX, X1 // X1 = float(int(x))
CMPSD X1, X0, 1 // compare LT; X0 = 0xffffffffffffffff or 0
MOVSD $(-1.0), X2
ANDPD X2, X0 // if x < float(int(x)) {X0 = -1} else {X0 = 0}
ADDSD X1, X0
MOVSD X0, r+8(FP)
RET
X0
に元のx
、X1
にfloat(int(x))
が入ります。CMPSD X1, X0, 1
はX0 < X1
(つまりx < float(int(x))
) を比較します。結果はX0
に格納され、真であればすべてのビットが1 (0xffffffffffffffff
)、偽であれば0になります。MOVSD $(-1.0), X2
で-1.0
をX2
にロードします。ANDPD X2, X0
は、x < float(int(x))
が真の場合 (X0
がすべて1) はX2
の値 (-1.0
) をX0
にコピーし、偽の場合 (X0
がすべて0) は0.0
をX0
にコピーします。ADDSD X1, X0
でfloat(int(x))
に、上記の条件付きで-1.0
または0.0
を加算します。これにより、Floor
の正しい結果が得られます。例えば、Floor(3.14)
の場合、float(int(3.14))
は3.0
、3.14 < 3.0
は偽なので0.0
が加算され3.0
。Floor(-3.14)
の場合、float(int(-3.14))
は-3.0
、-3.14 < -3.0
は真なので-1.0
が加算され-4.0
となります。
Ceil
の詳細:
MOVQ AX, X0 // X0 = x
MOVQ DX, X2 // X2 = sign bit mask
CVTTSD2SQ X0, AX
ANDNPD X0, X2 // X2 = sign
CVTSQ2SD AX, X1 // X1 = float(int(x))
CMPSD X1, X0, 2 // compare LE; X0 = 0xffffffffffffffff or 0
ORPD X2, X1 // if X1 = 0.0, incorporate sign
MOVSD $1.0, X3
ANDNPD X3, X0
ORPD X2, X0 // if float(int(x)) <= x {X0 = 1} else {X0 = -0}
ADDSD X1, X0
MOVSD X0, r+8(FP)
RET
CMPSD X1, X0, 2
はX0 <= X1
(つまりx <= float(int(x))
) を比較します。結果はX0
に格納されます。ORPD X2, X1
は、float(int(x))
が0.0
の場合に、元のx
の符号ビットをX1
に適用します。これは-0.0
のようなケースを正しく扱うためです。MOVSD $1.0, X3
で1.0
をX3
にロードします。ANDNPD X3, X0
は、x <= float(int(x))
が真の場合 (X0
がすべて1) はX3
の値 (1.0
) をX0
にコピーし、偽の場合 (X0
がすべて0) は0.0
をX0
にコピーします。ORPD X2, X0
は、X0
が0.0
の場合に、元のx
の符号ビットをX0
に適用します。ADDSD X1, X0
でfloat(int(x))
に、上記の条件付きで1.0
または0.0
を加算します。
Trunc
の詳細:
MOVQ AX, X0
MOVQ DX, X2 // X2 = sign bit mask
CVTTSD2SQ X0, AX
ANDNPD X0, X2 // X2 = sign
CVTSQ2SD AX, X0 // X0 = float(int(x))
ORPD X2, X0 // if X0 = 0.0, incorporate sign
MOVSD X0, r+8(FP)
RET
CVTTSD2SQ
とCVTSQ2SD
の組み合わせにより、既にゼロ方向への丸めが行われています。ORPD X2, X0
は、結果が0.0
の場合に、元のx
の符号ビットをX0
に適用します。これにより、Trunc(-0.5)
が-0.0
を返すなど、符号付きゼロのセマンティクスが正しく維持されます。
src/pkg/math/log_amd64.s
の変更点
Log
関数内で、中間結果をXMMレジスタ間でコピーする際に、MOVSD
が MOVAPD
に変更されました。
MOVSD X2, X3
->MOVAPD X2, X3
MOVSD X3, X4
->MOVAPD X3, X4
MOVSD X4, X5
->MOVAPD X4, X5
これは、XMMレジスタが128ビット幅であり、MOVAPD
が16バイトアラインされたメモリまたはレジスタ間で128ビット全体を転送するのに対し、MOVSD
は下位64ビットのみを転送するためです。このコンテキストでは、レジスタ間のコピーであり、データが適切にアラインされていると仮定できるため、MOVAPD
を使用することで、より効率的なレジスタ間データ転送が可能になり、パフォーマンスが向上します。
関連リンク
- Go Change-list: https://golang.org/cl/6262048
参考にした情報源リンク
- Intel® 64 and IA-32 Architectures Software Developer’s Manuals: https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html (特にVol. 2A: Instruction Set Reference, A-M および Vol. 2B: Instruction Set Reference, N-Z)
- IEEE 754 浮動小数点数標準: https://ja.wikipedia.org/wiki/IEEE_754
- Go Assembly Language (Go言語のアセンブリに関する公式ドキュメント): https://go.dev/doc/asm
- Go言語の
math
パッケージ: https://pkg.go.dev/math MOVSD
vsMOVAPD
(Stack Overflow): https://stackoverflow.com/questions/10907000/movsd-vs-movapdCVTTSD2SQ
andCVTSQ2SD
(Instruction Set Reference): https://www.felixcloutier.com/x86/cvttsd2sqCMPSD
(Instruction Set Reference): https://www.felixcloutier.com/x86/cmpsdANDPD
andORPD
(Instruction Set Reference): https://www.felixcloutier.com/x86/andpdADDSD
andMULSD
(Instruction Set Reference): https://www.felixcloutier.com/x86/addsdTEXT
pseudo-op (Go Assembly): https://go.dev/doc/asm#TEXTFP
andSB
(Go Assembly): https://go.dev/doc/asm#FP- Goの浮動小数点数とアセンブリに関する議論 (例: Go issue tracker, mailing lists)