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

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

このコミットは、Go言語の実験的なロケール照合パッケージ exp/locale/collate における16ビット演算の使用を回避し、それによって長年のコンパイラバグを回避することを目的としています。具体的には、colelem.go 内の splitContractIndex 関数における uint16 キャストとそれに続く演算が問題を引き起こしていました。この変更は、コンパイラのレジスタ移動に関するバグを露呈させていたため、そのバグをテストするための新しいテストケース test/bugs/bug440.go も追加されています。

コミット

commit ce69666273bab23b5b4597acb4dbd1c18aba7270
Author: Russ Cox <rsc@golang.org>
Date:   Thu May 24 14:50:36 2012 -0400

    exp/locale/collate: avoid 16-bit math
    
    There's no need for the 16-bit arithmetic here,
    and it tickles a long-standing compiler bug.
    Fix the exp code not to use 16-bit math and
    create an explicit test for the compiler bug.
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/6256048

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

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

元コミット内容

このコミットは、exp/locale/collate パッケージ内の colelem.go ファイルにおいて、splitContractIndex 関数が uint16 型へのキャストを使用していた部分を修正しています。また、この修正に関連して、コンパイラのバグを再現させるための新しいテストファイル test/bugs/bug440.go を追加しています。

変更の背景

この変更の主な背景は、Goコンパイラ、特に8g(32ビットx86アーキテクチャ向けのGoコンパイラ)における長年のバグにあります。コミットメッセージによると、exp/locale/collate パッケージ内のコードが16ビット演算を使用しており、これがこのコンパイラバグを「くすぐって」いた(triggerしていた)とのことです。

具体的には、splitContractIndex 関数内で uint16(ce) のように uint16 への型キャストが行われ、その結果に対してビットシフトやビットマスク演算が適用されていました。Goコンパイラは、異なるサイズの整数型間でのレジスタ移動や演算の最適化において、特定の条件下で誤ったコードを生成することがありました。このバグは、特に16ビット値が32ビットレジスタにロードされ、その後演算が行われる際に、レジスタの内容が正しく扱われない場合に発生したと考えられます。

このバグは、Go言語の標準ライブラリの一部である exp/locale/collate のような、比較的重要なパッケージの動作に影響を与える可能性がありました。そのため、このバグを回避するために、問題のある16ビット演算を排除し、同時にバグの存在を明確にするためのテストケースを追加することが決定されました。

前提知識の解説

Go言語の型システムと整数型

Go言語には、int8, int16, int32, int64 および対応する符号なし整数型 uint8, uint16, uint32, uint64 があります。これらの型はそれぞれ異なるビット幅を持ち、格納できる値の範囲が異なります。演算を行う際には、Goの型変換ルールに従う必要があります。異なる型の値を直接演算しようとすると、コンパイルエラーになるか、暗黙の型変換によって予期せぬ結果を招くことがあります。明示的な型キャストは、プログラマが意図的に型を変換する際に使用されます。

ビット演算

Go言語では、他の多くの言語と同様にビット演算子(& (AND), | (OR), ^ (XOR), &^ (AND NOT), << (左シフト), >> (右シフト))が提供されています。これらの演算子は、数値の個々のビットを操作するために使用されます。

  • >> (右シフト): 数値のビットを右に指定された数だけ移動させます。これにより、実質的に2のべき乗で除算する効果があります。
  • & (AND): 2つの数値の対応するビットが両方とも1の場合にのみ、結果のビットを1にします。これは、特定のビットを抽出(マスク)するためによく使用されます。

Goコンパイラと8g

Go言語のコンパイラは、ソースコードを機械語に変換するツールです。Goの初期のコンパイラは、ターゲットアーキテクチャごとに異なる名前を持っていました。例えば、8g は32ビットx86アーキテクチャ(Intel/AMDの32ビットCPU)向けのGoコンパイラを指します。6g は64ビットx86アーキテクチャ向け、5g はARMアーキテクチャ向けなどです。これらのコンパイラは、コードの最適化も行いますが、その過程で特定の条件下でバグを抱えることがありました。

ロケール照合(Locale Collation)

ロケール照合とは、異なる言語や地域(ロケール)の規則に従って文字列をソートするプロセスです。例えば、ドイツ語では 'ä' は 'a' と 'b' の間にソートされることがありますが、スウェーデン語では 'z' の後にソートされることがあります。このプロセスは複雑で、文字の重み付け、アクセントの無視、契約文字(複数の文字が1つのソート単位として扱われる)や展開文字(1つの文字が複数のソート単位として扱われる)の処理など、多くのルールを含みます。

exp/locale/collate パッケージは、Go言語でこのようなロケール依存の文字列照合を実験的に実装するためのものでした。照合要素(collation elements)は、文字列内の各文字または文字シーケンスに割り当てられる数値であり、これらを比較することで文字列の順序を決定します。splitContractIndex 関数は、これらの照合要素から特定のインデックス、数、オフセット情報を抽出するために使用されていたと考えられます。

技術的詳細

このコミットが修正している問題は、Goコンパイラが特定の16ビット演算を誤って最適化してしまうバグです。src/pkg/exp/locale/collate/colelem.go の元のコードでは、colElem 型(おそらく uint32 またはそれ以上のサイズの整数型)の変数 ceuint16 にキャストしていました。

func splitContractIndex(ce colElem) (index, n, offset int) {
	h := uint16(ce) // ここで16ビットにキャスト
	return int(h >> maxNBits), int(h & (1<<maxNBits - 1)), int(ce>>16) & (1<<maxContractOffsetBits - 1)
}

この uint16(ce) というキャストが、8g コンパイラにおいて問題を引き起こしていました。コンパイラは、32ビットの ce から下位16ビットを抽出し、それを h に代入する際に、レジスタの扱いを誤ることがあったようです。その後の h を使ったビットシフト (h >> maxNBits) やビットマスク (h & (1<<maxNBits - 1)) 演算の結果が、期待される値と異なることがありました。

修正は、この明示的な uint16 キャストを削除し、代わりに ce & 0xffff というビットマスク演算を使用することです。

func splitContractIndex(ce colElem) (index, n, offset int) {
	h := ce & 0xffff // 16ビットにキャストせず、下位16ビットを抽出
	return int(h >> maxNBits), int(h & (1<<maxNBits - 1)), int(ce>>16) & (1<<maxContractOffsetBits - 1)
}

ce & 0xffff は、ce の下位16ビットのみを保持し、上位ビットをゼロクリアします。これにより、結果として得られる h の値は、元の uint16(ce) と論理的に同じになりますが、コンパイラが uint16 型のレジスタ操作で誤りを犯す可能性のあるパスを回避できます。h は依然として colElem と同じ基底型(おそらく uint32)を持つことになりますが、その値は16ビットの範囲に収まります。

test/bugs/bug440.go は、このコンパイラバグを再現させるための最小限のテストケースです。このテストファイルは、splitContractIndex 関数の簡略版を定義し、特定の定数 c (0x12345678) を入力として使用します。そして、期待される結果と実際の関数の出力が一致するかどうかを検証します。

func splitContractIndex(ce uint32) (index, n, offset int) {
	h := uint16(ce) // ここでバグを再現させるための16ビットキャストを使用
	return int(h >> 5), int(h & (1<<5 - 1)), int(ce>>16) & (1<<14 - 1)
}

このテストは、$G $D/$F.go && $L $F.$A && ./$A.out というコマンドで実行されることを意図しており、特に 8g コンパイラで問題が発生することを示唆しています。テストが失敗した場合(BUG が出力された場合)、それはコンパイラバグがまだ存在することを示します。

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

src/pkg/exp/locale/collate/colelem.go

--- a/src/pkg/exp/locale/collate/colelem.go
+++ b/src/pkg/exp/locale/collate/colelem.go
@@ -102,7 +102,7 @@ const (
 )
 
 func splitContractIndex(ce colElem) (index, n, offset int) {
-	h := uint16(ce)
+	h := ce & 0xffff
 	return int(h >> maxNBits), int(h & (1<<maxNBits - 1)), int(ce>>16) & (1<<maxContractOffsetBits - 1)
 }

test/bugs/bug440.go (新規追加)

--- /dev/null
+++ b/test/bugs/bug440.go
@@ -0,0 +1,21 @@
+// $G $D/$F.go && $L $F.$A && ./$A.out
+// # switch above to 'run' when bug gets fixed.
+// # right now it only breaks on 8g
+
+// Test for 8g register move bug.  The optimizer gets confused
+// about 16- vs 32-bit moves during splitContractIndex.
+
+package main
+
+func main() {
+	const c = 0x12345678
+	index, n, offset := splitContractIndex(c)
+	if index != int((c&0xffff)>>5) || n != int(c & (1<<5-1)) || offset != (c>>16)&(1<<14-1) {
+		println("BUG", index, n, offset)
+	}
+}
+
+func splitContractIndex(ce uint32) (index, n, offset int) {
+	h := uint16(ce)
+	return int(h >> 5), int(h & (1<<5 - 1)), int(ce>>16) & (1<<14 - 1)
+}

コアとなるコードの解説

src/pkg/exp/locale/collate/colelem.go の変更

splitContractIndex 関数は、colElem 型の ce から3つの整数値 index, n, offset を抽出する役割を担っています。この関数は、ロケール照合における契約(contraction)のインデックス情報を解析するために使用されます。

元のコードでは、h := uint16(ce) という行で、ce の値を明示的に16ビットの符号なし整数にキャストしていました。これは、ce の下位16ビットのみを抽出し、その後の演算 (h >> maxNBitsh & (1<<maxNBits - 1)) で使用することを意図していました。しかし、前述の通り、この uint16 キャストが特定のコンパイラ(8g)でバグを引き起こしていました。

変更後のコード h := ce & 0xffff は、ce の下位16ビットを抽出するという同じ論理的な目的を達成しますが、明示的な uint16 キャストを回避します。0xffff は16進数で 0000FFFF であり、これは下位16ビットがすべて1、上位ビットがすべて0の32ビット(またはそれ以上)のマスクです。ce とこのマスクをビットAND演算することで、ce の下位16ビットのみが保持され、上位ビットはゼロになります。これにより、h は依然として colElem と同じ基底型(例えば uint32)を持つことになりますが、その値は16ビットの範囲に収まり、コンパイラのバグを回避できます。

test/bugs/bug440.go の新規追加

このファイルは、Goコンパイラのバグをテストするために特別に作成されたものです。

  • // $G $D/$F.go && $L $F.$A && ./$A.out: これは、Goのテストシステムがこのファイルをどのようにコンパイル・リンク・実行するかを示すコメントです。$G はGoコンパイラ、$D/$F.go は現在のファイル、$L はGoリンカ、$F.$A は生成される実行可能ファイル名、./$A.out はその実行を示します。
  • // # switch above to 'run' when bug gets fixed.: このコメントは、このテストが一時的なものであり、コンパイラバグが修正されたら通常のテスト実行フローに統合されるべきであることを示唆しています。
  • // # right now it only breaks on 8g: このバグが特に 8g コンパイラで発生することを示しています。
  • // Test for 8g register move bug. The optimizer gets confused // about 16- vs 32-bit moves during splitContractIndex.: このテストの目的を明確に説明しています。コンパイラの最適化が16ビットと32ビットのレジスタ移動を混同することが原因であると指摘しています。
  • func main():
    • const c = 0x12345678: テスト用の入力値として、32ビットの定数 0x12345678 を定義しています。この値は、上位ビットと下位ビットの両方に意味のあるパターンが含まれているため、ビット演算のテストに適しています。
    • index, n, offset := splitContractIndex(c): テスト対象の splitContractIndex 関数を呼び出し、結果を取得します。
    • if index != int((c&0xffff)>>5) || n != int(c & (1<<5-1)) || offset != (c>>16)&(1<<14-1) { println("BUG", index, n, offset) }: ここがテストの核心です。splitContractIndex の出力 (index, n, offset) が、期待される正しい値と一致するかどうかを検証しています。期待される値は、uint16 キャストなしでビット演算を直接適用した場合の論理的な結果です。もし一致しない場合、BUG という文字列と実際の値が出力され、テストが失敗したことを示します。
  • func splitContractIndex(ce uint32) (index, n, offset int):
    • この関数は、src/pkg/exp/locale/collate/colelem.go にある元の splitContractIndex 関数の簡略版であり、バグを再現させるために意図的に h := uint16(ce) という問題のある行を含んでいます。これにより、このテストが実行されたときにコンパイラバグがトリガーされることを保証します。

このテストファイルは、コンパイラ開発者がバグの修正を確認するための重要なツールとして機能します。

関連リンク

  • Go言語の公式ドキュメント: https://golang.org/doc/
  • Go言語のソースコードリポジトリ: https://github.com/golang/go
  • Go言語のIssueトラッカー: https://github.com/golang/go/issues (このコミットに関連する具体的なバグ報告は、コミットメッセージの https://golang.org/cl/6256048 から辿れる可能性がありますが、直接的なIssue番号は記載されていません。)

参考にした情報源リンク

(注: 特定の「長年のコンパイラバグ」に関する詳細な公開ドキュメントやIssueは、このコミットメッセージからは直接特定できませんでしたが、Goのコンパイラ開発における一般的なバグ修正の文脈で理解されます。)