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

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

このコミットは、Go言語のコンパイラ(cmd/gc)におけるrangeループの最適化に関するものです。具体的には、rangeループで2番目の戻り値(通常はインデックスまたは値)がブランク識別子(_)に割り当てられている場合に、その値の計算をスキップすることで、特に配列に対する不要なコピー処理を削減し、パフォーマンスを向上させることを目的としています。

コミット

commit 7936ab58f7feb25f8c9b367585886d9872a17ad3
Author: Daniel Morsing <daniel.morsing@gmail.com>
Date:   Wed Sep 26 21:10:29 2012 +0200

    cmd/gc: Don't calculate second value in range if it is blank.
    
    Low hanging fruit optimization. Will remove an expensive copy if the range variable is an array.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/6564052

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

https://github.com/golang/go/commit/7936ab58f7feb25f8c9b367585886d9872a17ad3

元コミット内容

cmd/gc: Don't calculate second value in range if it is blank.

Low hanging fruit optimization. Will remove an expensive copy if the range variable is an array.

R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6564052

変更の背景

Go言語のrangeキーワードは、スライス、配列、文字列、マップ、チャネルなどのコレクションを反復処理するために使用されます。for ... rangeループは、通常、2つの値を返します。1つ目はインデックス(またはキー)、2つ目は対応する要素の値です。

しかし、プログラマがインデックスや値のいずれか一方のみに関心がある場合、Goではブランク識別子(_)を使用して不要な値を破棄することができます。例えば、for _, value := range slice {} のように記述することで、インデックスを無視し、値のみを使用できます。

このコミットが導入される以前は、たとえブランク識別子で値が破棄される場合でも、コンパイラは内部的にその値を計算し、場合によってはコピーを生成していました。特に、rangeの対象が配列である場合、配列全体のコピーが発生する可能性があり、これはパフォーマンス上のオーバーヘッドとなっていました。

この変更は、このような「不要な計算とコピー」を排除し、コンパイラがより効率的なコードを生成できるようにするための「ローハンギングフルーツ(簡単に達成できる最適化)」として位置づけられています。

前提知識の解説

Go言語のfor ... rangeループ

for ... rangeはGo言語におけるイテレーションの主要な構文です。

  • スライスと配列: for index, value := range arrayOrSlice の形式で、インデックスと要素のコピーを返します。
  • 文字列: for index, runeValue := range string の形式で、バイトオフセットとUnicodeコードポイント(rune)を返します。
  • マップ: for key, value := range map の形式で、キーと値のコピーを返します。マップのイテレーション順序は保証されません。
  • チャネル: for value := range channel の形式で、チャネルから受信した値を返します。チャネルが閉じられるまでブロックします。

ブランク識別子(_

Go言語のブランク識別子(_)は、変数を宣言したが使用しない場合にコンパイラのエラーを回避するために使用される特別な識別子です。これは、値を明示的に破棄したい場合や、関数の複数の戻り値のうち一部のみに関心がある場合などに利用されます。_に割り当てられた値は、実際にはどこにも保存されず、コンパイラによって無視されます。

Goコンパイラ(cmd/gc

cmd/gcは、Go言語の公式コンパイラであり、Goのソースコードを機械語に変換する役割を担っています。コンパイラは、コードの構文解析、型チェック、最適化、コード生成などの複数のフェーズを経て実行されます。このコミットは、型チェックフェーズの一部であるrangeループの処理ロジックに手を入れることで、最適化を実現しています。

配列のコピー

Goにおいて、配列は値型です。関数に配列を渡したり、配列を別の変数に代入したりすると、配列全体のコピーが作成されます。大きな配列の場合、このコピー操作はメモリとCPUの大きなオーバーヘッドとなる可能性があります。スライスは参照型であり、基盤となる配列へのポインタと長さ、容量を持つため、スライスのコピーはポインタとメタデータのコピーに過ぎず、配列全体のコピーは発生しません。このコミットの最適化が特に配列に言及しているのは、この値型としての特性とコピーコストが背景にあります。

技術的詳細

この最適化は、Goコンパイラのsrc/cmd/gc/range.cファイル内のtypecheckrange関数に実装されています。この関数は、for ... rangeループの型チェックと、ループ変数の設定を担当しています。

変更前は、rangeループの2番目の変数(v2)が常に設定され、その型チェックが行われていました。しかし、v2がブランク識別子である場合、その値はプログラム内で使用されないため、計算やコピーは不要です。

コミットによって追加されたロジックは以下の通りです。

  1. v2(2番目のループ変数)がブランク識別子であるかどうかをisblank(v2)関数でチェックします。
  2. もしv2がブランク識別子であれば、以下の処理を行います。
    • n->list = list1(v1);: rangeループの変数リストを、1番目の変数(v1、インデックスまたはキー)のみを含むように再構築します。これにより、コンパイラは2番目の変数を処理する必要がなくなります。
    • v2 = N;: v2N(nilノード、つまり存在しないことを示す)に設定します。これにより、後続の処理でv2が無視されるようになります。

この変更により、コンパイラは、ブランク識別子に割り当てられた2番目のrange変数のためのコード生成を完全にスキップできるようになります。特に、rangeの対象が配列である場合、配列要素の不要なコピーが回避され、実行時のパフォーマンスが向上します。これは、コンパイラレベルでのデッドコード排除の一種と見なすことができます。

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

変更はsrc/cmd/gc/range.cファイルにあります。

--- a/src/cmd/gc/range.c
+++ b/src/cmd/gc/range.c
@@ -71,6 +71,11 @@ typecheckrange(Node *n)
 		v2 = N;
 	if(n->list->next)
 		v2 = n->list->next->n;
+	
+	if(isblank(v2)) {
+		n->list = list1(v1);
+		v2 = N;
+	}
 
 	if(v1->defn == n)
 		v1->type = t1;

コアとなるコードの解説

上記のコードスニペットは、typecheckrange関数内での変更を示しています。

  • v2 = N;if(n->list->next) v2 = n->list->next->n; の行は、rangeループの2番目の変数(存在する場合)をv2に割り当てる既存のロジックです。
  • 新しく追加されたブロック if(isblank(v2)) { ... } がこの最適化の核心です。
    • isblank(v2): これは、v2がブランク識別子(_)を表すコンパイラ内部のノードであるかどうかをチェックする関数です。
    • n->list = list1(v1);: n->listrangeループの変数を表すノードのリストです。この行は、リストをv1(1番目の変数、インデックスまたはキー)のみを含む新しいリストに置き換えます。これにより、コンパイラは2番目の変数を処理する必要がなくなります。
    • v2 = N;: v2N(nilノード)に設定することで、後続のコンパイラフェーズでこの変数が存在しないものとして扱われるようにします。

この変更により、for _, value := range array のようなコードがコンパイルされる際、valueに対応する内部的な処理(値の計算や配列要素のコピーなど)が完全にスキップされるようになります。これは、特に大きな配列をrangeで反復処理し、値が不要な場合に顕著なパフォーマンス向上をもたらします。

関連リンク

参考にした情報源リンク

  • Go言語のforステートメントに関する公式ドキュメント: https://go.dev/ref/spec#For_statements
  • Go言語のブランク識別子に関する公式ドキュメント: https://go.dev/ref/spec#Blank_identifier
  • Go言語の配列とスライスに関する公式ドキュメント: https://go.dev/blog/go-slices-usage-and-internals
  • Goコンパイラ(cmd/gc)の内部構造に関する一般的な情報源(Goのバージョンや時期によって詳細は異なる可能性がありますが、基本的な概念は共通です)
    • "Go compiler internals" などのキーワードで検索すると、関連するブログ記事やプレゼンテーションが見つかります。
    • Goのソースコード自体が最も正確な情報源です。
      • src/cmd/gc/range.c
      • src/cmd/gc/walk.go (コンパイラのウォークフェーズ、最適化に関連)
      • src/cmd/compile/internal/gc/range.go (現代のGoコンパイラにおけるrange処理の関連ファイル)
  • Go言語のパフォーマンス最適化に関する一般的な情報源。