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

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

このコミットは、Goコンパイラ(gc)におけるインライン化された関数の行番号の扱いを改善するものです。具体的には、インライン化によって生成されたコードのデバッグ情報における行番号の正確性を向上させ、デバッグ時のユーザー体験を改善することを目的としています。

コミット

commit 86deacc0bc271a7188db6f4413be6491f013f233
Author: Luuk van Dijk <lvd@golang.org>
Date:   Thu Dec 22 17:31:54 2011 +0100

    gc: better linenumbers for inlined functions
    
    Fixes #2580 up to a point.
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/5498068

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

https://github.com/golang/go/commit/86deacc0bc271a7188db6f4413be6491f013f233

元コミット内容

gc: better linenumbers for inlined functions

Fixes #2580 up to a point.

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

変更の背景

このコミットの背景には、Goコンパイラが関数をインライン化する際に発生していた、デバッグ情報の行番号の不正確さという問題があります。具体的には、GoのIssue #2580「inlined code has invalid line numbers」(インライン化されたコードの行番号が不正である)がこの問題を示しています。

関数がインライン化されると、呼び出し元のコードに関数の本体が直接埋め込まれます。これにより、関数呼び出しのオーバーヘッドが削減され、パフォーマンスが向上する可能性があります。しかし、デバッグ時には、インライン化されたコードが元のソースファイルのどの行に対応するのかを正確に追跡することが重要になります。もし行番号が正しくない場合、デバッガでステップ実行したり、スタックトレースを解析したりする際に、ユーザーは混乱し、問題の特定が困難になります。

このコミットは、この行番号の不正確さを解消し、インライン化された関数に対してもより正確なデバッグ情報を提供することを目指しています。「up to a point」(ある程度まで)という表現は、このコミットが問題の完全な解決ではなく、段階的な改善であることを示唆しています。

前提知識の解説

Goコンパイラ (gc)

Go言語の公式コンパイラであり、Goソースコードを機械語に変換する役割を担っています。gcは最適化の一環として関数のインライン化を行います。

関数のインライン化 (Function Inlining)

コンパイラ最適化の一種で、関数呼び出しの代わりに、呼び出される関数の本体を呼び出し元のコードに直接挿入する手法です。 メリット:

  • 関数呼び出しのオーバーヘッド(スタックフレームの作成、引数のプッシュ、戻り値の処理など)を削減し、実行速度を向上させます。
  • インライン化されたコードに対して、さらにコンパイラが最適化を適用できる機会が増えます。 デメリット:
  • コードサイズが増加する可能性があります(特に小さな関数が多数インライン化される場合)。
  • デバッグが難しくなることがあります。元の関数呼び出しのコンテキストが失われたり、行番号が混乱したりするためです。

行番号 (Line Numbers)

ソースコードの各行に対応する番号です。デバッグ情報において非常に重要であり、エラーメッセージ、スタックトレース、デバッガでのステップ実行などで、プログラムの実行がソースコードのどの位置にあるかを示すために使用されます。

抽象構文木 (Abstract Syntax Tree, AST)

ソースコードの抽象的な構文構造を木構造で表現したものです。コンパイラはソースコードをパースしてASTを構築し、このASTに対して様々な解析や変換(最適化など)を行います。Goコンパイラも内部でASTを扱っており、NodeはASTの各ノードを表す構造体です。

Nodelineno

GoコンパイラのASTにおけるNode構造体は、プログラムの各要素(変数、式、文、関数など)を表します。Nodeには通常、そのノードがソースコードのどの行に対応するかを示すlineno(行番号)フィールドが含まれています。

ONAMEノード

GoコンパイラのASTにおけるONAMEは、名前(識別子)を表すノードの一種です。変数名、関数名などがこれに該当します。

技術的詳細

このコミットは、Goコンパイラのインライン化処理を行うsrc/cmd/gc/inl.cファイルに修正を加えています。主な変更点は、インライン化されたコードのASTノードに行番号を適切に伝播させるための新しいヘルパー関数setlnosetlnolistの導入です。

以前のmkinlcall関数では、インライン化された関数の呼び出しを表すcallノードのlinenoを、呼び出し元のノードnlinenoに直接設定していました。しかし、これだけではインライン化された関数の本体内の各ノードに正しい行番号が伝播されず、デバッグ時に問題が生じていました。

新しいsetlno関数は、与えられたNodeとその子ノード(left, right, list, rlist, ninit, ntest, nincr, nbody, nelseなど)を再帰的に走査し、指定された行番号lnoを設定します。重要なのは、ONAMEノードの場合、既存のlineno0(つまり、新しく合成されたノードである可能性が高い)でない限り、linenoを上書きしないという条件がある点です。これは、コンパイラが内部的に生成した一時的な名前など、ソースコードに直接対応しないノードの行番号を不必要に変更しないための配慮と考えられます。

setlnolist関数は、NodeListNodeのリンクリスト)を走査し、リスト内の各Nodeに対してsetlnoを呼び出します。

これらの関数を導入することで、mkinlcallがインライン化された関数のASTを構築した後、setlno(call, n->lineno);を呼び出すことで、インライン化された関数全体のASTツリーに対して、呼び出し元の行番号を適切に「塗りつける」ことができるようになります。これにより、デバッガがインライン化されたコードをステップ実行する際に、より意味のある行番号情報を提供できるようになります。

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

変更はsrc/cmd/gc/inl.cファイルに集中しています。

  1. static void setlno(Node*, int); の追加:

    @@ -28,6 +28,8 @@ static Node*	newlabel(void);
     static Node*	inlsubst(Node *n);
     static NodeList* inlsubstlist(NodeList *ll);
     
    +static void	setlno(Node*, int);
    +
     // Used during inlsubst[list]
     static Node *inlfn;		// function currently being inlined
     static Node *inlretlabel;	// target of the goto substituted in place of a return
    

    setlno関数の前方宣言が追加されました。

  2. mkinlcall関数内の変更:

    @@ -496,9 +498,10 @@ mkinlcall(Node **np, Node *fn)\n 	call->nbody = body;\n 	call->rlist = inlretvars;\n 	call->type = n->type;\n-\tcall->lineno = n->lineno;\n \tcall->typecheck = 1;\n \n+\tsetlno(call, n->lineno);\n+\n \t*np = call;\n \n \tinlfn =	saveinlfn;
    

    call->lineno = n->lineno; の直接代入が削除され、代わりに setlno(call, n->lineno); が呼び出されています。これにより、callノードだけでなく、その子孫ノードにも行番号が再帰的に設定されるようになります。

  3. setlnolist関数の追加:

    @@ -686,3 +689,32 @@ inlsubst(Node *n)\n \n 	return m;\n }\n+\n+// Plaster over linenumbers\n+static void\n+setlnolist(NodeList *ll, int lno)\n+{\n+\tfor(;ll;ll=ll->next)\n+\t\tsetlno(ll->n, lno);\n+}\n    ```
    `NodeList`内の各ノードに対して`setlno`を呼び出すヘルパー関数が追加されました。
    
    
  4. setlno関数の追加:

    +static void\n+setlno(Node *n, int lno)\n+{\n+\tif(!n)\n+\t\treturn;\n+\n+\t// don\'t clobber names, unless they\'re freshly synthesized\n+\tif(n->op != ONAME || n->lineno == 0)\n+\t\tn->lineno = lno;\n+\t\n+\tsetlno(n->left, lno);\n+\tsetlno(n->right, lno);\n+\tsetlnolist(n->list, lno);\n+\tsetlnolist(n->rlist, lno);\n+\tsetlnolist(n->ninit, lno);\n+\tsetlno(n->ntest, lno);\n+\tsetlno(n->nincr, lno);\n+\tsetlnolist(n->nbody, lno);\n+\tsetlnolist(n->nelse, lno);\n+}\n    ```
    ASTノードを再帰的に走査し、行番号を設定する主要なロジックが実装されています。`ONAME`ノードに対する特別な処理が含まれています。
    
    

コアとなるコードの解説

setlno関数

static void
setlno(Node *n, int lno)
{
    if(!n)
        return;

    // don't clobber names, unless they're freshly synthesized
    if(n->op != ONAME || n->lineno == 0)
        n->lineno = lno;
    
    setlno(n->left, lno);
    setlno(n->right, lno);
    setlnolist(n->list, lno);
    setlnolist(n->rlist, lno);
    setlnolist(n->ninit, lno);
    setlno(n->ntest, lno);
    setlno(n->nincr, lno);
    setlnolist(n->nbody, lno);
    setlnolist(n->nelse, lno);
}

この関数は、GoコンパイラのASTノードnに対して、指定された行番号lnoを設定します。

  • if(!n) return;: nullポインタチェック。
  • if(n->op != ONAME || n->lineno == 0) n->lineno = lno;: この条件がこの関数の核心です。
    • n->op != ONAME: 現在のノードがONAME(名前を表すノード)でない場合、無条件に行番号を設定します。
    • n->lineno == 0: 現在のノードがONAMEであっても、そのlineno0である場合(これはコンパイラによって新しく合成されたノードであることを示唆します)、行番号を設定します。
    • この条件により、ソースコードに直接対応する既存のONAMEノードの行番号が、インライン化によって不適切に上書きされることを防ぎつつ、インライン化によって生成された新しいノードには適切な行番号が割り当てられるようになります。
  • 続くsetlnosetlnolistの呼び出しは、現在のノードの子ノードや関連するノードリストに対して再帰的に同じ行番号設定処理を適用し、ASTツリー全体に行番号を伝播させます。これにより、インライン化された関数の本体内のすべての要素が、呼び出し元の行番号を持つことになります。

setlnolist関数

static void
setlnolist(NodeList *ll, int lno)
{
    for(;ll;ll=ll->next)
        setlno(ll->n, lno);
}

この関数は、NodeListNodeのリンクリスト)を受け取り、リスト内の各Nodeに対してsetlno関数を呼び出します。これは、複数のノードがリストとして管理されている場合に、それらすべてに行番号を適用するためのユーティリティ関数です。

mkinlcall関数内の変更

mkinlcallは、インライン化された関数呼び出しを表すASTノードを構築する関数です。 以前はcall->lineno = n->lineno;と直接代入していましたが、これはcallノード自体にしか行番号を設定しませんでした。 setlno(call, n->lineno);に変更することで、callノードをルートとするインライン化された関数のASTサブツリー全体に、呼び出し元の行番号n->linenoが再帰的に適用されるようになります。これにより、インライン化されたコードのデバッグ情報がより正確になります。

関連リンク

参考にした情報源リンク

  • Go Issue #2580に関するWeb検索結果
  • Go言語のコンパイラに関する一般的な知識
  • 抽象構文木(AST)に関する一般的な知識
  • 関数のインライン化に関する一般的な知識
  • src/cmd/gc/inl.cのコード構造とGoコンパイラの内部動作に関する推測