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

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

このコミットは、Goコンパイラにおける冗長なnilポインタチェックを排除することを目的としています。特に、構造体のフィールドアクセス(&x.Field)におけるnilチェックの約75%を削減し、生成されるバイナリのサイズと実行時のオーバーヘッドを削減します。Go 1.2のリリースが近い時期であったため、シンプルかつ効果的で安全なアプローチが採用されました。

コミット

commit aa0439ba65bfa762c26a3dbb3f0c0474267b5582
Author: Russ Cox <rsc@golang.org>
Date:   Tue Sep 17 16:54:22 2013 -0400

    cmd/gc: eliminate redundant &x.Field nil checks
    
    This eliminates ~75% of the nil checks being emitted,
    on all architectures. We can do better, but we need
    a bit more general support from the compiler, and
    I don't want to do that so close to Go 1.2.
    What's here is simple but effective and safe.
    
    A few small code generation cleanups were required
    to make the analysis consistent on all systems about
    which nil checks are omitted, at least in the test.
    
    Fixes #6019.
    
    R=ken2
    CC=golang-dev
    https://golang.org/cl/13334052

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

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

元コミット内容

Goコンパイラ(cmd/gc)において、冗長な&x.Field形式のnilポインタチェックを排除します。これにより、全てのアーキテクチャで生成されるnilチェックの約75%が削減されます。この変更は、Go 1.2リリース直前であったため、シンプルで効果的かつ安全な方法に限定されています。nilチェックの省略に関する分析を全てのシステムで一貫させるために、いくつかの小さなコード生成のクリーンアップも行われました。

変更の背景

Go言語では、ポインタのデリファレンス(*p)や構造体・配列のフィールドアクセス(p.Field, p[index])を行う際に、ポインタpnilでないことを保証するためのnilポインタチェックが自動的に挿入されます。これは、Goプログラムの安全性と堅牢性を高めるための重要な機能です。しかし、コンパイラが生成するnilチェックの中には、既にそのポインタがnilでないことが保証されている文脈で挿入される冗長なものが存在しました。

例えば、x.Field1.Field2のようなアクセスでは、xがnilでないことが確認された後、x.Field1がnilでないことも確認されます。もしxがnilでないことが既にチェック済みであれば、x.Field1へのアクセス時に再度xのnilチェックを行うのは冗長です。このような冗長なチェックは、生成されるバイナリのサイズを増やし、実行時のオーバーヘッド(特にチェックが頻繁に行われるループ内など)を発生させる可能性があります。

このコミットは、このような冗長なnilチェックを特定し、排除することで、コンパイラが生成するコードの効率を向上させることを目的としています。コミットメッセージにある「Fixes #6019」は、この問題がGoのIssueトラッカーで報告されていたことを示唆しています。

前提知識の解説

Goコンパイラの構造

Goコンパイラ(cmd/gc)は、複数のフェーズに分かれて動作します。

  • フロントエンド: ソースコードの字句解析、構文解析、型チェック、AST(抽象構文木)の構築を行います。
  • ミドルエンド(最適化): ASTに対して様々な最適化を適用します。このコミットで導入されるnilチェックの最適化もこのフェーズの一部です。
  • バックエンド(コード生成): 最適化されたASTから、ターゲットアーキテクチャの機械語コードを生成します。

nilポインタチェック

Go言語では、ポインタがnilである場合にデリファレンスを行うと、ランタイムパニックが発生します。これを防ぐため、コンパイラはポインタのデリファレンスやフィールドアクセスが行われる前に、そのポインタがnilでないことを確認するコード(nilチェック)を自動的に挿入します。このチェックは、通常、ポインタの値を0と比較し、0であればパニックを発生させるような命令として実装されます。

ACCHECKNIL命令

Goコンパイラの内部では、nilチェックが必要な箇所にACCHECKNILという擬似命令が挿入されます。これは、最終的な機械語命令に変換される前の中間表現です。この最適化の目的は、このACCHECKNIL命令の冗長なインスタンスを削除することです。

制御フローグラフ (Control Flow Graph, CFG)

コンパイラの最適化フェーズでは、プログラムの実行パスを表現するために制御フローグラフがよく用いられます。CFGは、プログラムの各命令やブロックをノードとし、命令の実行順序や条件分岐、ループなどをエッジで表現します。このコミットで導入される最適化は、CFG上を辿ることで、あるnilチェックが実行される前に、同じポインタに対するnilチェックが既に実行されているかどうかを判断します。

Peephole最適化

Peephole最適化は、コンパイラのバックエンドで行われる最適化の一種で、生成された機械語命令の小さなシーケンス(peephole、覗き穴)を検査し、より効率的な同等のシーケンスに置き換えるものです。このコミットでは、peep.cファイル群に変更が加えられており、これはPeephole最適化に関連する部分です。

技術的詳細

このコミットの主要な変更は、Goコンパイラの最適化フェーズにniloptという新しい関数を導入し、冗長なnilポインタチェックを削除することです。

nilopt関数の概要

nilopt関数は、生成された中間コード(Prog構造体のリスト)を走査し、ACCHECKNIL命令を特定します。そして、各ACCHECKNIL命令について、そのチェックが冗長であるかどうかを判断し、冗長であれば削除します。

最適化の基本的な考え方は以下の通りです。

  1. 前方探索 (nilwalkfwd): あるACCHECKNIL命令から前方(プログラムの実行順序)に探索し、そのポインタがデリファレンスされる(つまり、暗黙的にnilチェックが行われる)箇所や、ポインタの値が変更される箇所を探します。もし、明示的なACCHECKNIL命令の前に、そのポインタがデリファレンスされるような命令(例: *xx.Field)があり、かつそのオフセットが小さい場合(通常、最初のページに収まる範囲)、その暗黙的なチェックに任せることができるため、明示的なACCHECKNILを削除します。
  2. 後方探索 (nilwalkback): あるACCHECKNIL命令から後方(プログラムの逆実行順序)に探索し、同じポインタに対する別のACCHECKNIL命令や、ポインタの値が初期化される箇所を探します。もし、同じポインタに対する別のACCHECKNIL命令が既に実行されているパスが存在する場合、現在のACCHECKNILは冗長であると判断し、削除します。

niloptの具体的なロジック

nilopt関数は、まず制御フローグラフ(CFG)を構築します。その後、ACCHECKNIL命令を一つずつ処理します。

  • スタック変数に対するチェックの削除: stackaddr(&p->from)が真の場合、つまりチェック対象のポインタがスタック上のアドレスを指している場合、そのnilチェックは無条件に削除されます。これは、スタック上のアドレスは通常nilになることがないためです。
  • nilwalkfwdによる前方最適化: nilwalkfwd(r)を呼び出し、現在のACCHECKNIL命令rから前方(実行順序)に探索します。
    • smallindir関数を用いて、ポインタが小さなオフセットで間接参照されているかどうかをチェックします。もし、p->fromまたはp->torcheck->f.prog->from(チェック対象のポインタ)を小さなオフセットで間接参照している場合、その間接参照自体がnilチェックの役割を果たすため、現在のACCHECKNILは冗長と判断され、rcheck->kill = 1が設定されます。
    • 他のACCHECKNIL命令が見つかった場合、またはポインタの値が変更される(RightWrite)ような命令が見つかった場合、探索を停止します。
  • nilwalkbackによる後方最適化: nilwalkfwdで削除されなかったACCHECKNIL命令について、nilwalkback(r)を呼び出し、後方(逆実行順序)に探索します。
    • 同じポインタに対する別のACCHECKNIL命令が見つかった場合、現在のACCHECKNILは冗長と判断され、rcheck->kill = 1が設定されます。
    • ポインタの値が初期化される(RightWrite)ような命令が見つかった場合、そのパスではnilチェックがまだ行われていない可能性があるため、現在のACCHECKNILは保持されます。
  • 命令の削除: 最終的にr->kill1に設定されたACCHECKNIL命令は、excise(&r->f)によって中間コードから削除されます。

補助関数の役割

  • sameaddr(Addr *a, Addr *v): 2つのアドレスが同じポインタを指しているかどうかを判断します。
  • regtyp(Addr *a): アドレスがレジスタタイプであるかどうかを判断します。
  • stackaddr(Addr *a): アドレスがスタックポインタ(REGSP)を指しているかどうかを判断します。
  • smallindir(Addr *a, Addr *reg): アドレスaがレジスタregをベースとした小さなオフセット(0から4095バイト)の間接参照であるかどうかを判断します。これは、メモリハードウェアがnilポインタによるアクセスを検出する際に、通常最初のページ(4KB)がマップされていないことを利用した最適化のヒントとなります。

アーキテクチャ固有の変更

このコミットは、src/cmd/5g (ARM), src/cmd/6g (x86), src/cmd/8g (x86-64) の各アーキテクチャ固有のコード生成およびPeephole最適化ファイルにも変更を加えています。これは、nilチェックの削除が全てのアーキテクチャで一貫して行われるように、コード生成の挙動を微調整するためです。例えば、ggen.cではwarnlメッセージが変更され、peep.cではsameaddr, stackaddr, smallindirといった補助関数が各アーキテクチャのニーズに合わせて実装されています。

test/nilptr3.go

このコミットでは、test/nilptr3.goという新しいテストファイルが追加されています。このテストは、様々なシナリオ(グローバル変数、ローカル変数、配列、構造体、大きな構造体、空の構造体など)におけるnilチェックの生成と削除の挙動を検証します。// ERROR "generated nil check"// ERROR "removed repeated nil check"といったコメントは、コンパイラが特定の箇所でnilチェックを生成するか、または削除するかを期待していることを示しています。これにより、最適化が正しく機能していることを確認できます。

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

  • src/cmd/gc/popt.c: nilopt関数が追加され、冗長なnilチェックを削除する主要なロジックが実装されました。nilwalkbacknilwalkfwdという補助関数もここで定義されています。
  • src/cmd/gc/popt.h: nilopt関数のプロトタイプおよび、sameaddr, smallindir, stackaddrといった補助関数のプロトタイプが追加されました。
  • src/cmd/gc/pgen.c: compile関数内で、レジスタ最適化(regopt)の後にnilopt(ptxt)が呼び出されるようになりました。これにより、nilチェックの最適化がコンパイルパイプラインに組み込まれます。また、cgen_checknil関数も変更され、nilチェックの対象となるノードの型チェックが厳密になりました。
  • src/cmd/gc/gen.c: cgen_discard関数にOIND(間接参照)の場合の処理が追加され、cgen_checknilが呼び出されるようになりました。
  • src/cmd/gc/walk.c: OINDの場合の冗長なnilチェック生成ロジックが削除されました。
  • src/cmd/5g/peep.c, src/cmd/6g/peep.c, src/cmd/8g/peep.c: 各アーキテクチャのPeephole最適化ファイルに、sameaddr, stackaddr, smallindirといった補助関数が追加されました。これらの関数は、niloptがアーキテクチャ固有のコード生成の挙動を考慮してnilチェックを削除するために使用されます。
  • src/cmd/5g/ggen.c, src/cmd/6g/ggen.c, src/cmd/8g/ggen.c: expandchecks関数内のwarnlメッセージが変更されました。
  • src/cmd/8g/cgen.c: OINDEX(配列インデックス)の処理が改善され、固定サイズ配列のインデックスアクセスにおいて、定数オフセットがアドレス計算に組み込まれるようになりました。これにより、一部のケースでnilチェックの必要性が減少する可能性があります。
  • test/nilptr3.go: 新しいテストファイルが追加され、nilチェックの最適化が正しく機能していることを検証します。

コアとなるコードの解説

src/cmd/gc/popt.c

void
nilopt(Prog *firstp)
{
    // ... (制御フローグラフの初期化など)

    for(r = (NilFlow*)g->start; r != nil; r = (NilFlow*)r->f.link) {
        p = r->f.prog;
        if(p->as != ACHECKNIL || !regtyp(&p->from))
            continue; // ACHECKNIL命令でないか、レジスタタイプでない場合はスキップ

        ncheck++; // nilチェックの総数をカウント

        if(stackaddr(&p->from)) {
            // スタックアドレスに対するnilチェックは無条件で削除
            if(debug_checknil && p->lineno > 1)
                warnl(p->lineno, "removed nil check of SP address");
            r->kill = 1; // 削除フラグを設定
            continue;
        }

        nilwalkfwd(r); // 前方探索
        if(r->kill) {
            // 前方探索で削除が決定した場合
            if(debug_checknil && p->lineno > 1)
                warnl(p->lineno, "removed nil check before indirect");
            continue;
        }

        nilwalkback(r); // 後方探索
        if(r->kill) {
            // 後方探索で削除が決定した場合
            if(debug_checknil && p->lineno > 1)
                warnl(p->lineno, "removed repeated nil check");
            continue;
        }
    }

    // killフラグが設定されたACCHECKNIL命令を実際に削除
    for(r = (NilFlow*)g->start; r != nil; r = (NilFlow*)r->f.link) {
        if(r->kill) {
            nkill++;
            excise(&r->f); // 命令を削除
        }
    }

    // ... (統計情報の出力など)
}

static void
nilwalkback(NilFlow *rcheck)
{
    Prog *p;
    ProgInfo info;
    NilFlow *r;

    // rcheckから後方(逆実行順序)に探索
    for(r = rcheck; r != nil; r = (NilFlow*)uniqp(&r->f)) {
        p = r->f.prog;
        proginfo(&info, p);

        // ポインタの値が書き換えられている場合、そのパスではnilチェックがまだ行われていない可能性がある
        if((info.flags & RightWrite) && sameaddr(&p->to, &rcheck->f.prog->from)) {
            // チェック対象の値の初期化が見つかった場合、このチェックは削除できない
            return;
        }
        // 別のACCHECKNIL命令が見つかり、同じポインタをチェックしている場合
        if(r != rcheck && p->as == ACHECKNIL && sameaddr(&p->from, &rcheck->f.prog->from)) {
            rcheck->kill = 1; // 現在のチェックは冗長なので削除
            return;
        }
    }
}

static void
nilwalkfwd(NilFlow *rcheck)
{
    NilFlow *r;
    Prog *p;
    ProgInfo info;

    // rcheckから前方(実行順序)に探索
    for(r = (NilFlow*)uniqs(&rcheck->f); r != nil; r = (NilFlow*)uniqs(&r->f)) {
        p = r->f.prog;
        proginfo(&info, p);

        // ポインタが小さなオフセットで間接参照されている場合(暗黙的なnilチェック)
        if((info.flags & LeftRead) && smallindir(&p->from, &rcheck->f.prog->from)) {
            rcheck->kill = 1; // 現在のチェックは冗長なので削除
            return;
        }
        if((info.flags & (RightRead|RightWrite)) && smallindir(&p->to, &rcheck->f.prog->from)) {
            rcheck->kill = 1; // 現在のチェックは冗長なので削除
            return;
        }

        // 別のnilチェックが発生した場合、探索を停止
        if(p->as == ACHECKNIL)
            return;
        // 値が失われた場合(ポインタが書き換えられた場合)、探索を停止
        if((info.flags & RightWrite) && sameaddr(&p->to, &rcheck->f.prog->from))
            return;
        // メモリへの書き込みが発生した場合、探索を停止
        if((info.flags & RightWrite) && !regtyp(&p->to))
            return;
    }
}

src/cmd/gc/popt.h

void	nilopt(Prog*); // nilチェック最適化関数の宣言
int	sameaddr(Addr*, Addr*); // 2つのアドレスが同じかどうかをチェック
int	smallindir(Addr*, Addr*); // 小さなオフセットでの間接参照かどうかをチェック
int	stackaddr(Addr*); // スタックアドレスかどうかをチェック

これらの関数は、Goコンパイラのバックエンドにおける中間表現(Prog)を操作し、nilチェックの冗長性を判断するための重要なロジックを提供します。特にniloptは、制御フロー分析に基づいて、プログラムの安全性とパフォーマンスのバランスを取りながら、不要なnilチェックを効率的に削除します。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード(上記GitHubリンク)
  • Go言語のコンパイラに関する一般的な知識
  • コンパイラ最適化に関する一般的な知識(制御フロー分析、Peephole最適化など)