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

[インデックス 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命令を積極的に利用しています。基本的なロジックは以下のステップで構成されます。

  1. 入力値 x の取得: MOVQ x+0(FP), AX で引数 xAX レジスタにロードします。
  2. 絶対値の計算と特殊ケースの処理:
    • Big 定数 (0x4330000000000000、これは 2^52 を表す浮動小数点数) を定義しています。
    • |x| >= 2^52 の場合、または xNaN (Not a Number) の場合、あるいは x0 の場合、x をそのまま返すという最適化が行われています。これは、2^52 以上の浮動小数点数では、整数部分がすべて表現可能であり、丸め処理が不要になるためです。IsNaN のチェックも含まれています。
    • MOVQ ~(1<<63), DX で符号ビット以外のマスクを作成し、ANDQ AX, DXx の絶対値を取得します。
    • CMPQ 命令で |x|Big 定数を比較し、条件付きジャンプ (JAE) で特殊ケース (isBig_floor, isBig_ceil, isBig_trunc) に分岐します。
  3. 浮動小数点数から整数への変換 (Truncation):
    • MOVQ AX, X0x をXMMレジスタ X0 に移動します。
    • CVTTSD2SQ X0, AX 命令は、X0 の倍精度浮動小数点数をゼロ方向への丸め(Truncate)を行い、結果を64ビット符号付き整数として AX レジスタに格納します。
  4. 整数から浮動小数点数への変換:
    • CVTSQ2SD AX, X1 (または X0 for Trunc) 命令は、AX レジスタの64ビット符号付き整数を倍精度浮動小数点数に変換し、X1 (または X0) に格納します。これにより、float(int(x)) に相当する値が得られます。
  5. 丸めロジックの適用:
    • Floor: xfloat(int(x)) を比較します。もし x < float(int(x)) であれば、x は負の小数部分を持つため、結果から 1.0 を引く必要があります。CMPSD X1, X0, 1 (compare LT) を使用して比較し、結果に応じて -1.0 または 0.0 を生成し、float(int(x)) に加算します。
    • Ceil: xfloat(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: CVTTSD2SQCVTSQ2SD の組み合わせにより、既にゼロ方向への丸めが行われているため、追加の調整は不要です。符号付きゼロのケース (-0.0) を正しく扱うために、元の符号ビットを結果に適用する処理 (ORPD X2, X0) が行われます。
  6. 結果の返却: 最終的な結果を 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 を使用することで、より効率的なデータ転送が可能になります。特に、コンパイラやアセンブラがレジスタのアラインメントを保証できる場合、MOVAPDMOVSD よりも高速に実行される可能性があります。これは、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 アセンブリによる詳細な実装に置き換えられています。

共通のパターン:

  1. 引数 x のロード: MOVQ x+0(FP), AX で、スタックフレームポインタ FP から引数 x の値を AX レジスタにロードします。
  2. 符号ビットのマスク: MOVQ ~(1<<63), DX で、64ビット値の最上位ビット(符号ビット)を0にするマスクを作成します。
  3. 絶対値の計算: ANDQ AX, DX (または ANDQ DX, BX for Ceil/Trunc) で、x の絶対値を取得します。
  4. 特殊ケース (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| が非常に大きい場合、丸め処理が不要になるため、計算をスキップして高速化を図るものです。また、NaNInf (無限大) のような特殊な浮動小数点値もこのパスで処理されます。
  5. 浮動小数点数と整数の変換:
    • MOVQ AX, X0x を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 に元の xX1float(int(x)) が入ります。
  • CMPSD X1, X0, 1X0 < X1 (つまり x < float(int(x))) を比較します。結果は X0 に格納され、真であればすべてのビットが1 (0xffffffffffffffff)、偽であれば0になります。
  • MOVSD $(-1.0), X2-1.0X2 にロードします。
  • ANDPD X2, X0 は、x < float(int(x)) が真の場合 (X0 がすべて1) は X2 の値 (-1.0) を X0 にコピーし、偽の場合 (X0 がすべて0) は 0.0X0 にコピーします。
  • ADDSD X1, X0float(int(x)) に、上記の条件付きで -1.0 または 0.0 を加算します。これにより、Floor の正しい結果が得られます。例えば、Floor(3.14) の場合、float(int(3.14))3.03.14 < 3.0 は偽なので 0.0 が加算され 3.0Floor(-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, 2X0 <= X1 (つまり x <= float(int(x))) を比較します。結果は X0 に格納されます。
  • ORPD X2, X1 は、float(int(x))0.0 の場合に、元の x の符号ビットを X1 に適用します。これは -0.0 のようなケースを正しく扱うためです。
  • MOVSD $1.0, X31.0X3 にロードします。
  • ANDNPD X3, X0 は、x <= float(int(x)) が真の場合 (X0 がすべて1) は X3 の値 (1.0) を X0 にコピーし、偽の場合 (X0 がすべて0) は 0.0X0 にコピーします。
  • ORPD X2, X0 は、X00.0 の場合に、元の x の符号ビットを X0 に適用します。
  • ADDSD X1, X0float(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
  • CVTTSD2SQCVTSQ2SD の組み合わせにより、既にゼロ方向への丸めが行われています。
  • ORPD X2, X0 は、結果が 0.0 の場合に、元の x の符号ビットを X0 に適用します。これにより、Trunc(-0.5)-0.0 を返すなど、符号付きゼロのセマンティクスが正しく維持されます。

src/pkg/math/log_amd64.s の変更点

Log 関数内で、中間結果をXMMレジスタ間でコピーする際に、MOVSDMOVAPD に変更されました。

  • 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 を使用することで、より効率的なレジスタ間データ転送が可能になり、パフォーマンスが向上します。

関連リンク

参考にした情報源リンク