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

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

このコミットは、Go言語のmathパッケージにおけるIsInfおよびIsNaN関数の手動インライン化を元に戻す変更です。これにより、コンパイラがこれらの関数を自動的にインライン化するようになったため、コードの冗長性が解消され、可読性と保守性が向上します。

コミット

commit 8dd3de4d4b304989019dac9be49e53a0f280908b
Author: Luuk van Dijk <lvd@golang.org>
Date:   Wed Feb 1 16:08:31 2012 +0100

    pkg/math: undo manual inlining of IsInf and IsNaN
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/5484076

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

https://github.com/golang/go/commit/8dd3de4d4b304989019dac9be49e53a0f280908b

元コミット内容

pkg/math: undo manual inlining of IsInf and IsNaN

このコミットメッセージは、mathパッケージ内でIsInf(無限大のチェック)とIsNaN(非数のチェック)関数の手動インライン化を元に戻すことを明確に示しています。

変更の背景

Go言語の初期のコンパイラは、特定の最適化、特に小さな関数のインライン化を自動的に行わない場合がありました。そのため、パフォーマンスを向上させるために、開発者がIsInfIsNaNのような頻繁に呼び出されるユーティリティ関数を、呼び出し箇所で直接そのロジックを展開する「手動インライン化」を行うことがありました。

しかし、コンパイラの進化に伴い、これらの関数を自動的にインライン化する能力が向上しました。手動インライン化は、コードの重複、可読性の低下、そして保守性の悪化を招きます。コンパイラが自動的に最適化できるようになった場合、手動インライン化は不要となり、むしろ負の影響を与えることになります。

このコミットは、コンパイラの改善により手動インライン化が不要になったため、その冗長なコードを削除し、よりクリーンで保守しやすいコードベースに戻すことを目的としています。元のコードには// TODO(rsc): Remove manual inlining of IsNaN // when compiler does it for usのようなコメントが残されており、将来的なコンパイラの改善を見越して手動インライン化が行われていたことが伺えます。

前提知識の解説

浮動小数点数と特殊な値 (NaN, Inf)

コンピュータにおける浮動小数点数(float32, float64など)は、実数を近似的に表現するためのものです。しかし、全ての実数を正確に表現できるわけではなく、また特定の演算結果として特殊な値が発生することがあります。

  • NaN (Not a Number): 「非数」と訳されます。0/0、無限大/無限大、無限大 - 無限大、負の数の平方根など、数学的に未定義または表現不可能な演算結果として生成されます。NaNは、それ自身を含むいかなる値とも等しくありません(NaN == NaNfalse)。
  • Inf (Infinity): 「無限大」と訳されます。正の無限大 (+Inf) と負の無限大 (-Inf) があります。例えば、非ゼロの数を0で割った場合や、表現可能な最大値を超える計算結果として生成されます。

これらの特殊な値は、浮動小数点演算においてエラー状態や特定の境界条件を扱う上で重要です。

math.IsNaNmath.IsInf

Go言語の標準ライブラリmathパッケージには、これらの特殊な値をチェックするための関数が提供されています。

  • func IsNaN(f float64) bool: 引数fがNaNである場合にtrueを返します。
  • func IsInf(f float64, sign int) bool: 引数fが無限大である場合にtrueを返します。sign引数は、正の無限大 (+1)、負の無限大 (-1)、またはどちらでもよい (0) かを指定します。

これらの関数は、浮動小数点演算の結果を適切に処理するために不可欠です。

インライン化 (Inlining)

インライン化とは、コンパイラ最適化の一種で、関数呼び出しをその関数の本体のコードで直接置き換えるプロセスです。

メリット:

  • 関数呼び出しのオーバーヘッド削減: 関数呼び出しに伴うスタックフレームの作成、引数のプッシュ、戻り値の処理などのコストがなくなります。
  • さらなる最適化の機会: インライン化されたコードは、呼び出し元のコンテキストと統合されるため、より広範な最適化(定数伝播、デッドコード削除など)が可能になります。

デメリット:

  • コードサイズの増加: 関数本体が複数回コピーされるため、実行可能ファイルのサイズが増加する可能性があります。
  • キャッシュ効率の低下: コードサイズが増加すると、CPUの命令キャッシュの効率が低下する可能性があります。

現代のコンパイラは、インライン化のメリットとデメリットを考慮し、ヒューリスティックに基づいて自動的にインライン化を行うかどうかを決定します。

手動インライン化

コンパイラが自動的にインライン化を行わない場合や、特定のパフォーマンス要件がある場合に、開発者が意図的に関数呼び出しをその本体のコードで置き換えることを「手動インライン化」と呼びます。これは通常、以下のような形で現れます。

// 変更前 (関数呼び出し)
if IsNaN(x) {
    // ...
}

// 変更後 (手動インライン化)
if x != x { // IsNaN(x) のロジックを直接記述
    // ...
}

IsNaN(x)は、浮動小数点数の特性としてx != xtrueになるのはxがNaNの場合のみであるという事実を利用して実装されることが多いため、手動インライン化ではこのx != xという比較が直接記述されていました。同様に、IsInfMaxFloat64との比較などを用いて手動でチェックされていました。

技術的詳細

このコミットの技術的詳細は、Goコンパイラの進化と、それに伴うコードベースのクリーンアップにあります。

Go言語の初期のコンパイラは、IsNaNIsInfのような単純な関数であっても、その呼び出しを自動的にインライン化する能力が限定的でした。そのため、mathパッケージ内の多くの関数(Acosh, Asinh, Atan2, Cbrt, Dim, Erf, Exp, Expm1, Floor, Frexp, Gamma, Hypot, J0, J1, Jn, Ldexp, Lgamma, Log, Log1p, Logb, Mod, Nextafter, Pow, Remainder, Sin, Sincos, Sqrt, Tanなど)では、パフォーマンス上の理由から、IsNaN(x)の代わりにx != xを、IsInf(x, 0)の代わりにx > MaxFloat64 || x < -MaxFloat64といった形で、関数のロジックを直接埋め込む「手動インライン化」が行われていました。

コミットメッセージとコードの変更履歴から、当時のGoコンパイラ開発チーム(特にrscことRuss Cox氏)は、将来的にコンパイラがこれらの最適化を自動的に行えるようになることを認識しており、そのためのTODOコメントを残していました。

このコミットが行われた2012年2月1日の時点で、Goコンパイラは十分に成熟し、IsNaNIsInfのような単純な関数呼び出しを自動的にインライン化できるようになりました。これにより、手動インライン化されたコードは冗長となり、削除することが可能になりました。

変更の具体的な内容は、各ファイルで手動インライン化されていたx != xx > MaxFloat64 || x < -MaxFloat64といった条件式を、対応するIsNaN(x)IsInf(x, 0)の関数呼び出しに置き換えることです。これにより、コードの意図がより明確になり、mathパッケージの関数が提供する抽象化が適切に利用されるようになります。

例えば、src/pkg/math/acosh.goの変更では、case x < 1 || x != x: // x < 1 || IsNaN(x):という行がcase x < 1 || IsNaN(x):に変更されています。これは、x != xという手動インライン化されたNaNチェックが、より意図が明確なIsNaN(x)関数呼び出しに置き換えられたことを示しています。

この変更は、Go言語のコンパイラが進化し、より高度な最適化を自動的に行えるようになったことの証であり、言語とツールの成熟を示すものです。開発者は、手動での最適化に時間を費やす代わりに、より高レベルの抽象化を利用してコードを記述できるようになります。

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

このコミットは、src/pkg/mathディレクトリ内の29のファイルにわたる変更を含んでいます。主な変更は、IsNaNIsInf関数の手動インライン化を元に戻し、それぞれの関数呼び出しに置き換えることです。

以下に、代表的な変更箇所の例をいくつか示します。

src/pkg/math/acosh.go

--- a/src/pkg/math/acosh.go
+++ b/src/pkg/math/acosh.go
@@ -44,11 +44,9 @@ func Acosh(x float64) float64 {
 		Ln2   = 6.93147180559945286227e-01 // 0x3FE62E42FEFA39EF
 		Large = 1 << 28                    // 2**28
 	)
-// TODO(rsc): Remove manual inlining of IsNaN
-// when compiler does it for us
 	// first case is special case
 	switch {
-	case x < 1 || x != x: // x < 1 || IsNaN(x):
+	case x < 1 || IsNaN(x):
 		return NaN()
 	case x == 1:
 		return 0

src/pkg/math/atan2.go

--- a/src/pkg/math/atan2.go
+++ b/src/pkg/math/atan2.go
@@ -29,11 +29,9 @@ package math
 func Atan2(y, x float64) float64
 
 func atan2(y, x float64) float64 {
-// TODO(rsc): Remove manual inlining of IsNaN, IsInf
-// when compiler does it for us
 	// special cases
 	switch {
-	case y != y || x != x: // IsNaN(y) || IsNaN(x):
+	case IsNaN(y) || IsNaN(x):
 		return NaN()
 	case y == 0:
 		if x >= 0 && !Signbit(x) {
@@ -42,22 +40,22 @@ func atan2(y, x float64) float64 {
 		return Copysign(Pi, y)
 	case x == 0:
 		return Copysign(Pi/2, y)
-	case x < -MaxFloat64 || x > MaxFloat64: // IsInf(x, 0):
-		if x > MaxFloat64 { // IsInf(x, 1) {
+	case IsInf(x, 0):
+		if IsInf(x, 1) {
 			switch {
-			case y < -MaxFloat64 || y > MaxFloat64: // IsInf(y, -1) || IsInf(y, 1):
+			case IsInf(y, 0):
 				return Copysign(Pi/4, y)
 			default:
 				return Copysign(0, y)
 			}
 		}
 		switch {
-		case y < -MaxFloat64 || y > MaxFloat64: // IsInf(y, -1) || IsInf(y, 1):
+		case IsInf(y, 0):
 			return Copysign(3*Pi/4, y)
 		default:
 			return Copysign(Pi, y)
 		}
-	case y < -MaxFloat64 || y > MaxFloat64: //IsInf(y, 0):
+	case IsInf(y, 0):
 		return Copysign(Pi/2, y)
 	}
 

これらの変更は、// TODO(rsc): Remove manual inlining of IsNaNのようなコメントが削除され、手動で展開されていた条件式がIsNaN()IsInf()の呼び出しに置き換えられていることを示しています。

コアとなるコードの解説

このコミットのコアとなる変更は、Go言語のmathパッケージ内の浮動小数点数演算関数における特殊な値(NaNとInf)のチェック方法の統一です。

以前のコードでは、パフォーマンス上の理由から、IsNaN(x)の代わりにx != xという比較が、またIsInf(x, sign)の代わりにx > MaxFloat64 || x < -MaxFloat64(または特定の符号をチェックするより複雑な条件)といった直接的な比較が用いられていました。これは、当時のGoコンパイラがこれらの小さなユーティリティ関数を自動的にインライン化する能力が限定的であったため、関数呼び出しのオーバーヘッドを避けるための「手動インライン化」でした。

このコミットでは、Goコンパイラの最適化能力が向上したことを受けて、これらの手動インライン化されたコードを、より意図が明確で保守性の高いmath.IsNaN()およびmath.IsInf()関数呼び出しに置き換えています。

例えば、acosh.goの変更を見てみましょう。

変更前:

case x < 1 || x != x: // x < 1 || IsNaN(x):

ここでは、x != xという条件がNaNチェックとして使われています。コメントでIsNaN(x)と同等であることが示されていますが、コード自体は低レベルな比較です。

変更後:

case x < 1 || IsNaN(x):

変更後では、直接IsNaN(x)関数が呼び出されています。これにより、コードの意図がより明確になり、mathパッケージが提供する抽象化が適切に利用されています。コンパイラがこのIsNaN(x)呼び出しを自動的にインライン化するため、パフォーマンス上のペナルティは発生しません。

同様に、atan2.goIsInfに関する変更も見てみましょう。

変更前:

case x < -MaxFloat64 || x > MaxFloat64: // IsInf(x, 0):
    if x > MaxFloat64 { // IsInf(x, 1) {
        // ...
    }

ここでは、MaxFloat64との比較によって無限大をチェックしています。

変更後:

case IsInf(x, 0):
    if IsInf(x, 1) {
        // ...
    }

変更後では、IsInf(x, 0)IsInf(x, 1)といった関数呼び出しに置き換えられています。これにより、コードの可読性が大幅に向上し、無限大のチェックという意図が直接的に表現されています。

この変更は、Go言語のコードベースが成熟し、コンパイラの最適化能力が向上した結果として、よりクリーンで表現力の高いコードへと進化していることを示しています。開発者は、低レベルな最適化の詳細に気を配る必要がなくなり、より高レベルなロジックに集中できるようになります。これは、言語設計とコンパイラ開発の成功例と言えるでしょう。

関連リンク

参考にした情報源リンク