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

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

このコミットは、Goコンパイラ (cmd/gc) におけるマップのバケット作成処理を改善するものです。具体的には、マップのキーと値のサイズが、マップバケットを構築する前に正確に計算されるように修正されています。これにより、マップの内部構造が正しく定義され、潜在的なバグやメモリ割り当ての問題が解決されます。

コミット

commit 375b7bb76786ad61711771c7703f4c71b4489987
Author: Keith Randall <khr@golang.org>
Date:   Fri Apr 4 12:58:19 2014 -0700

    cmd/gc: compute size of keys & values before making map bucket
    
    Fixes #7547
    
    LGTM=iant
    R=iant, khr
    CC=golang-codereviews
    https://golang.org/cl/84470046
---
 src/cmd/gc/reflect.c        |  2 ++
 test/fixedbugs/issue7547.go | 17 +++++++++++++++++
 2 files changed, 19 insertions(+)

diff --git a/src/cmd/gc/reflect.c b/src/cmd/gc/reflect.c
index 3f4734ef52..75d7d8c1c8 100644
--- a/src/cmd/gc/reflect.c
+++ b/src/cmd/gc/reflect.c
@@ -125,6 +125,8 @@ mapbucket(Type *t)
  
  	keytype = t->down;
  	valtype = t->type;
+	dowidth(keytype);
+	dowidth(valtype);
  	if(keytype->width > MAXKEYSIZE)
  		keytype = ptrto(keytype);\n \tif(valtype->width > MAXVALSIZE)
diff --git a/test/fixedbugs/issue7547.go b/test/fixedbugs/issue7547.go
new file mode 100644
index 0000000000..f75a33036f
--- /dev/null
+++ b/test/fixedbugs/issue7547.go
@@ -0,0 +1,17 @@
+// compile
+
+// Copyright 2014 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.
+
+package main
+
+func f() map[string]interface{} {
+	var p *map[string]map[string]interface{}\n\t_ = p
+	return nil
+}
+
+func main() {
+	f()
+}

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

https://github.com/golang/go/commit/375b7bb76786ad61711771c7703f4c71b4489987

元コミット内容

Goコンパイラ (cmd/gc) において、マップのバケットを生成する前に、そのキーと値のサイズを確実に計算するように修正します。

変更の背景

このコミットは、GoのIssue #7547を修正するために行われました。この問題は、Goコンパイラがマップのバケット構造を定義する際に、そのバケットに格納されるキーと値のメモリ上のサイズを事前に正確に計算していなかったことに起因します。

Goのマップは内部的にハッシュテーブルとして実装されており、データは「バケット」と呼ばれる固定サイズのメモリチャンクに格納されます。各バケットは、複数のキーと値のペアを保持できるように設計されています。このバケットのレイアウトと効率的なメモリ割り当ては、格納されるキーと値の正確なサイズに大きく依存します。

もしコンパイラがバケットを作成する時点でキーと値のサイズを把握していなければ、不正確なバケット構造が生成されたり、メモリ割り当てが非効率になったり、最悪の場合、ランタイムエラーやデータ破損につながる可能性がありました。特に、複雑な型(例えば、ネストされたマップのポインタなど)がキーや値として使用される場合に、この問題が顕在化しやすかったと考えられます。

このコミットは、mapbucket関数がキーと値のサイズ情報を利用する前に、それらのサイズが確実に計算されていることを保証することで、この根本的な問題を解決します。

前提知識の解説

Goのマップ (map)

Go言語の map は、キーと値のペアを格納するための組み込みのデータ構造です。他の言語におけるハッシュマップ、ハッシュテーブル、連想配列、辞書などと同等の機能を提供します。Goのマップは、内部的にハッシュテーブルとして実装されており、キーのハッシュ値に基づいて値が格納され、高速な検索、挿入、削除が可能です。

マップの内部構造とバケット

Goのマップは、hmap という構造体で表現されます。データは bmap と呼ばれるバケットに格納されます。各 bmap は、複数のキーと値のペアを保持できる固定サイズの配列です。マップのパフォーマンスとメモリ効率は、これらのバケットの設計と、キーおよび値のメモリ上のサイズに大きく依存します。コンパイラは、マップの型情報に基づいて、これらのバケットのレイアウトを決定します。

コンパイル時の型情報と reflect.c

Goコンパイラ (cmd/gc) は、プログラムのコンパイル時に、各型のメモリ上の表現(サイズ、アライメントなど)を決定します。src/cmd/gc/reflect.c は、Goの型システム、特にリフレクションに関連する型情報の処理を担当するコンパイラのソースファイルの一部です。ここでは、実行時に型情報を利用するためのメタデータが生成されます。マップの型情報もここで処理され、その内部構造(バケットのレイアウトなど)が定義されます。

dowidth 関数

dowidth 関数は、Goコンパイラ内部で使用される重要な関数の一つです。この関数は、与えられた Type(型)のメモリ上のサイズ (width) とアライメントを計算し、その Type 構造体に設定します。Goのコンパイラは、メモリ割り当てやデータアクセスを最適化するために、すべての型の正確なサイズ情報を必要とします。dowidth は、このサイズ計算プロセスの中核をなすものです。

技術的詳細

このコミットの技術的な核心は、Goコンパイラがマップのバケット構造を生成する際の、キーと値の型情報の処理順序にあります。

Goコンパイラの cmd/gc は、ソースコードを解析し、実行可能なバイナリを生成する役割を担っています。この過程で、Goの map 型が検出されると、コンパイラはランタイムが使用するマップの内部構造(hmapbmap)を定義するためのメタデータを生成します。このメタデータには、バケットのサイズや、バケット内にキーと値がどのように配置されるかといった情報が含まれます。

src/cmd/gc/reflect.c 内の mapbucket 関数は、このマップバケットの型情報を構築する責任を負っています。この関数は、マップのキー型 (keytype) と値型 (valtype) を引数として受け取ります。

変更前は、mapbucket 関数内で keytype->widthvaltype->width といったフィールドが参照される際に、これらの width フィールドがまだ正確に計算されていない可能性がありました。これは、dowidth 関数がまだ呼び出されていないか、あるいは依存関係の解決が不十分であったためです。もし width が未計算のままであれば、MAXKEYSIZEMAXVALSIZE との比較、あるいはバケットのレイアウト計算が不正確になり、結果としてマップの動作が不安定になったり、メモリ効率が悪化したりする原因となります。

このコミットでは、mapbucket 関数内で keytypevaltype を取得した直後に、明示的に dowidth(keytype);dowidth(valtype); を呼び出すように変更されました。これにより、mapbucket 関数がキーと値のサイズ情報を使用する前に、それらの型に対する width フィールドが確実に計算され、最新かつ正確な値が設定されることが保証されます。この修正は、コンパイル時の型情報の依存関係を正しく解決し、マップのバケット構造が常に正確なサイズ情報に基づいて構築されるようにするために不可欠です。

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

このコミットによるコアとなるコードの変更は以下の2点です。

  1. src/cmd/gc/reflect.cmapbucket 関数への変更:

    • mapbucket 関数内で、マップのキー型 (keytype) と値型 (valtype) を取得した直後に、dowidth(keytype);dowidth(valtype); の2行が追加されました。
  2. test/fixedbugs/issue7547.go の新規追加:

    • Issue #7547で報告されたバグを再現し、このコミットによる修正が正しく機能することを検証するための新しいテストファイルが追加されました。このテストケースは、ネストされたマップのポインタを含む関数 f() を定義しており、このような複雑な型がマップのキーや値として扱われる場合に問題が発生しやすかったことを示唆しています。

コアとなるコードの解説

src/cmd/gc/reflect.c の変更

@@ -125,6 +125,8 @@ mapbucket(Type *t)
  
  	keytype = t->down;
  	valtype = t->type;
+	dowidth(keytype);
+	dowidth(valtype);
  	if(keytype->width > MAXKEYSIZE)
  		keytype = ptrto(keytype);
  	if(valtype->width > MAXVALSIZE)

この変更は、mapbucket 関数がマップのバケット構造を定義する際に、キーと値の型 (keytypevaltype) のメモリ上のサイズ (width) が確実に計算されていることを保証します。

  • keytype = t->down;valtype = t->type; で、それぞれマップのキー型と値型を取得します。
  • 追加された dowidth(keytype); は、keytype のメモリ上のサイズを計算し、その width フィールドに設定します。
  • 同様に、dowidth(valtype); は、valtype のメモリ上のサイズを計算し、その width フィールドに設定します。

これらの dowidth の呼び出しにより、その後の if(keytype->width > MAXKEYSIZE)if(valtype->width > MAXVALSIZE) といったサイズに基づく条件分岐や、バケットのメモリレイアウト計算において、常に正確なサイズ情報が利用されるようになります。これにより、マップのバケットが正しく、かつ効率的に構築されることが保証されます。

test/fixedbugs/issue7547.go の追加

+// compile
+
+// Copyright 2014 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.
+
+package main
+
+func f() map[string]interface{} {
+	var p *map[string]map[string]interface{}\n\t_ = p
+	return nil
+}
+
+func main() {
+	f()
+}

この新しいテストファイルは、Issue #7547で報告された特定のシナリオを再現するために作成されました。

  • func f() map[string]interface{} は、map[string]interface{} 型のマップを返す関数です。
  • 関数 f の内部で、var p *map[string]map[string]interface{} という変数が宣言されています。これは、string をキーとし、値がさらに map[string]interface{} 型のマップへのポインタであるという、複雑なネストされたマップの型定義を含んでいます。
  • _ = p は、p が使用されていないというコンパイラのエラーを回避するためのものです。

このテストの目的は、このような複雑な型(特にネストされたマップのポインタ)がGoコンパイラによって正しく処理され、マップのキーと値のサイズが正確に計算されることを確認することです。変更前は、このようなケースで mapbucket 関数がキーと値のサイズを誤って解釈し、コンパイルエラーやランタイムエラーを引き起こす可能性がありました。このテストがコンパイル時にエラーなく通過することで、コミットによる修正が意図通りに機能していることが検証されます。

関連リンク

  • Go CL 84470046: https://golang.org/cl/84470046

参考にした情報源リンク

  • Go CL 84470046 の詳細情報 (Web検索結果より)
  • Go言語のマップの内部実装に関する一般的な知識
  • Goコンパイラの型システムと reflect.c の役割に関する一般的な知識
  • dowidth 関数の目的と機能に関する一般的な知識