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

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

このコミットは、Goコンパイラのcmd/gc部分におけるインターフェース比較時の二重評価バグを修正するものです。具体的には、インターフェースの比較対象となるオペランドが複数回評価される可能性があり、そのオペランドが変換のための関数呼び出しを含む場合に、パフォーマンスの低下や予期せぬ副作用を引き起こす問題を解決します。修正は、比較前にオペランドを「安価な式」(cheap expression)に変換することで、この二重評価を防ぎます。

コミット

commit 804a43ca76a00c207a39d2846e3fe754d761ca2e
Author: Daniel Morsing <daniel.morsing@gmail.com>
Date:   Tue Sep 18 17:40:53 2012 +0200

    cmd/gc: fix double evaluation in interface comparison

    During interface compare, the operands will be evaluated twice. The operands might include function calls for conversion, so make them cheap before comparing them.

    R=rsc
    CC=golang-dev
    https://golang.org/cl/6498133
---
 src/cmd/gc/walk.c | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/cmd/gc/walk.c b/src/cmd/gc/walk.c
index 935fa6d65d..c6b7e4278f 100644
--- a/src/cmd/gc/walk.c
+++ b/cmd/gc/walk.c
@@ -1194,6 +1194,9 @@ walkexpr(Node **np, NodeList **init)\n 		fn = syslook("efaceeq", 1);\n 	else\n 		fn = syslook("ifaceeq", 1);\n+\n+\tn->right = cheapexpr(n->right, init);\n+\tn->left = cheapexpr(n->left, init);\n 		argtype(fn, n->right->type);\n 		argtype(fn, n->left->type);\n 		r = mkcall1(fn, n->type, init, n->left, n->right);\

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

https://github.com/golang/go/commit/804a43ca76a00c207a39d2846e3fe754d761ca2e

元コミット内容

cmd/gc: fix double evaluation in interface comparison

During interface compare, the operands will be evaluated twice. The operands might include function calls for conversion, so make them cheap before comparing them.

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

変更の背景

Go言語において、インターフェース型の値の比較は、その内部に保持されている具体的な型と値の両方を比較することで行われます。この比較処理はコンパイラによってefaceeq(空インターフェースinterface{}の場合)またはifaceeq(非空インターフェースの場合)といったヘルパー関数に変換されます。

問題は、これらの比較関数が呼び出される前に、比較対象となるオペランド(左辺と右辺)が複数回評価される可能性があったことです。特に、これらのオペランドが型変換を伴う関数呼び出しなど、評価にコストがかかる式である場合、二重評価は以下のような問題を引き起こします。

  1. パフォーマンスの低下: 不要な再評価により、プログラムの実行速度が低下します。
  2. 副作用の発生: オペランドの評価が状態を変更するような副作用を持つ関数呼び出しを含む場合、二重評価によってその副作用が意図せず複数回発生し、プログラムの動作が不安定になったり、バグの原因となったりする可能性があります。

このコミットは、このような潜在的な問題を防ぐために、インターフェース比較のオペランドが常に一度だけ評価されるように修正することを目的としています。

前提知識の解説

Go言語のインターフェース

Go言語のインターフェースは、メソッドのシグネチャの集合を定義する型です。インターフェース型の変数は、そのインターフェースが定義するすべてのメソッドを実装する任意の具体的な型の値を保持できます。

インターフェース型の値は、内部的に「型情報」と「値」の2つの要素から構成されます。

  • 型情報 (Type): インターフェースが現在保持している具体的な値の型。
  • 値 (Value): インターフェースが現在保持している具体的な値。

インターフェースの比較

Go言語では、インターフェース型の値は==演算子や!=演算子を使って比較できます。比較は以下のように行われます。

  1. 両方のインターフェースがnilである場合、等しいと判断されます。
  2. どちらか一方がnilで、もう一方がnilでない場合、等しくないと判断されます。
  3. 両方のインターフェースがnilでない場合、それらが保持する具体的な型具体的な値の両方が等しい場合にのみ、等しいと判断されます。

この比較ロジックは、コンパイラによって内部的に特別なヘルパー関数に変換されます。

  • efaceeq(interface{}, interface{}) bool: 空インターフェースinterface{}の比較に使用されます。
  • ifaceeq(interface{}, interface{}) bool: 非空インターフェース(特定のメソッドを持つインターフェース)の比較に使用されます。

これらの関数は、インターフェースの内部構造(型情報と値)を比較する役割を担います。

Goコンパイラのcmd/gc

cmd/gcは、Go言語の公式コンパイラの一部であり、Goソースコードを機械語に変換する主要な役割を担っています。コンパイルプロセスには、構文解析、型チェック、中間表現の生成、最適化、コード生成など、複数のステージがあります。

walk.cwalkexpr関数

src/cmd/gc/walk.cは、Goコンパイラの「ウォーク(walk)」ステージに関連するコードが含まれるファイルです。ウォークステージは、抽象構文木(AST)を走査し、高レベルのGoの構文をより低レベルの中間表現に変換する役割を担います。このステージでは、最適化や特定の言語機能の変換が行われます。

walkexpr関数は、式ノードを走査し、必要に応じて変換や最適化を適用する主要な関数の一つです。インターフェースの比較もこの関数内で処理されます。

cheapexpr関数

cheapexprは、Goコンパイラ内部で使用される関数で、与えられた式を「安価な式」(cheap expression)に変換することを目的としています。ここでいう「安価」とは、評価にコストがかからない、あるいは副作用がないことを意味します。

具体的には、cheapexprは、評価にコストがかかる可能性のある式(例: 関数呼び出し、複雑な計算)を、一時変数への代入と、その一時変数への参照に変換します。これにより、元の式が複数回参照される場合でも、実際の評価は一度だけ行われるようになります。これは、コンパイラの最適化手法の一つであり、共通部分式除去(Common Subexpression Elimination: CSE)や、副作用を持つ式の安全な扱いに関連します。

技術的詳細

インターフェースの比較は、walkexpr関数内で処理されます。この関数は、比較対象のインターフェースが空インターフェースか非空インターフェースかに応じて、efaceeqまたはifaceeqという内部ヘルパー関数を呼び出すコードを生成します。

コミット前のコードでは、mkcall1関数(関数呼び出しを生成する関数)に渡される前に、n->leftn->right(比較対象の左右のオペランド)が直接使用されていました。問題は、これらのオペランドが、例えば型変換のための関数呼び出しなど、評価にコストがかかる式であった場合です。

Goコンパイラの内部処理において、これらのオペランドがmkcall1に渡される過程や、その後のargtype関数での型チェック、さらにはefaceeq/ifaceeq関数の実際の呼び出しにおいて、複数回評価される可能性がありました。これは、コンパイラがコードを生成する際に、式の値を必要とするたびにその式を再評価するような最適化を行わない場合に発生しえます。

この二重評価は、特に以下のようなシナリオで問題となります。

package main

import "fmt"

type MyInt int

func (m MyInt) String() string {
    fmt.Println("String() called") // 副作用
    return fmt.Sprintf("%d", m)
}

func getInterface() interface{} {
    return MyInt(10)
}

func main() {
    // インターフェース比較
    // getInterface() が複数回評価されると問題
    if getInterface() == getInterface() {
        fmt.Println("Interfaces are equal")
    }
}

上記の例では、getInterface()がインターフェース比較のオペランドとなります。もしgetInterface()が複数回評価されると、String()メソッドが複数回呼び出され、「String() called」が複数回出力される可能性があります。これは、開発者の意図しない動作であり、バグにつながります。

このコミットは、cheapexpr関数を導入することでこの問題を解決します。cheapexprは、オペランドが評価にコストがかかる式である場合、その評価結果を一時変数に格納し、以降はその一時変数を参照するようにコードを変換します。これにより、元の式が何回参照されようとも、実際の評価は一度だけ行われることが保証されます。

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

変更はsrc/cmd/gc/walk.cファイル内のwalkexpr関数にあります。

--- a/src/cmd/gc/walk.c
+++ b/src/cmd/gc/walk.c
@@ -1194,6 +1194,9 @@ walkexpr(Node **np, NodeList **init)\n 		fn = syslook("efaceeq", 1);\n 	else\n 		fn = syslook("ifaceeq", 1);\n+\n+\t\tn->right = cheapexpr(n->right, init);\n+\t\tn->left = cheapexpr(n->left, init);\n 		argtype(fn, n->right->type);\n 		argtype(fn, n->left->type);\n 		r = mkcall1(fn, n->type, init, n->left, n->right);\

追加された行は以下の3行です。

		fn = syslook("efaceeq", 1);
	else
		fn = syslook("ifaceeq", 1);

		n->right = cheapexpr(n->right, init); // 追加
		n->left = cheapexpr(n->left, init);   // 追加
		argtype(fn, n->right->type);
		argtype(fn, n->left->type);
		r = mkcall1(fn, n->type, init, n->left, n->right);

コアとなるコードの解説

追加された2行は、インターフェース比較の左右のオペランド(n->leftn->right)を、それぞれcheapexpr関数に通しています。

  • n->right = cheapexpr(n->right, init);
  • n->left = cheapexpr(n->left, init);

ここで、nは現在のASTノードを表し、n->leftn->rightはそれぞれ比較演算子の左辺と右辺の式を表す子ノードです。initは、コンパイラが一時変数の宣言や初期化などの補助的なステートメントを追加するために使用するノードリストです。

cheapexpr関数は、渡された式ノードを分析し、もしその式が評価にコストがかかる(例えば、関数呼び出しを含む)と判断した場合、その式を評価した結果を格納するための一時変数を導入します。そして、元の式ノードを、その一時変数への参照に置き換えます。この一時変数の宣言と初期化は、initリストに追加されます。

この変更により、argtype関数がオペランドの型をチェックする際や、最終的にmkcall1によってefaceeqまたはifaceeq関数が呼び出される際に、n->leftn->rightが参照する値は、既に一度だけ評価されて一時変数に格納された「安価な」値となります。これにより、インターフェース比較のオペランドが複数回評価される問題が根本的に解決され、パフォーマンスの向上と副作用の安全な管理が保証されます。

関連リンク

参考にした情報源リンク

  • Go言語のインターフェースに関する公式ドキュメントやブログ記事
  • Goコンパイラのソースコード(特にsrc/cmd/gc/walk.cおよびcheapexpr関数の定義)
  • コンパイラの最適化に関する一般的な情報(共通部分式除去など)
  • Go言語のインターフェース比較に関する議論や解説記事