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

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

このコミットは、Go言語のARMアーキテクチャ向けアセンブラである cmd/5g において、命令に関する情報を一元的に管理するためのリファクタリングを導入します。具体的には、各命令の特性(オペランドの読み書き、サイズ、命令の種類など)を定義する ProgInfo 構造体と、その情報を取得する proginfo 関数を導入し、命令のプロパティをハードコードされた switch 文からデータ駆動型のテーブルに移行します。これにより、コードの可読性、保守性、拡張性が向上します。

コミット

commit a07218385ce652f55547dc06e664eaab6a47be43
Author: Russ Cox <rsc@golang.org>
Date:   Mon Aug 12 13:42:23 2013 -0400

    cmd/5g: factor out prog information
    
    Like CL 12637051, but for 5g instead of 6g.
    
    R=ken2
    CC=golang-dev
    https://golang.org/cl/12779043

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

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

元コミット内容

cmd/5g: factor out prog informationcmd/5g: プログラム情報をファクタリングする)

Like CL 12637051, but for 5g instead of 6g. (CL 12637051 と同様だが、6g の代わりに 5g 向け。)

変更の背景

Goコンパイラのバックエンド、特にアセンブラ(cmd/5g はARM向け、cmd/6g はAMD64向け)では、各機械語命令がどのような特性を持つか(例:どのレジスタを読み書きするか、メモリを操作するか、分岐命令かなど)を判断する必要がありました。これまでの実装では、これらの命令特性が peep.c(ピーフホール最適化)や reg.c(レジスタ割り当て)などのファイル内で、命令の種類ごとに個別の switch 文を使ってハードコードされていました。

このアプローチにはいくつかの問題がありました。

  1. 冗長性: 同じ命令特性のチェックが複数の場所で繰り返され、コードの重複が生じていました。
  2. 保守性の低さ: 新しい命令が追加されたり、既存の命令の特性が変更されたりした場合、関連するすべての switch 文を更新する必要があり、エラーが発生しやすかった。
  3. 可読性の低下: 命令の特性がコード全体に散らばっているため、全体像を把握しにくく、コードの意図が不明瞭になることがありました。

このコミットは、cmd/6g で先行して行われた同様のリファクタリング(CL 12637051)の cmd/5g 版として、これらの問題を解決することを目的としています。命令の特性をデータ駆動型のテーブルに集約することで、コードの重複を排除し、保守性と可読性を向上させます。

前提知識の解説

このコミットを理解するためには、以下の概念が役立ちます。

  • Goコンパイラとアセンブラ (cmd/5g): Go言語のコンパイラは、Goのソースコードを機械語に変換する過程で、中間表現(IR)を生成し、最終的に各アーキテクチャ向けのアセンブラ(例: cmd/5g はARM、cmd/6g はAMD64)が機械語コードを生成します。cmd/5g は、Goコンパイラの一部としてARMアーキテクチャ用の機械語命令を生成・最適化する役割を担います。
  • 中間表現 (IR): コンパイラがソースコードを直接機械語に変換するのではなく、途中で用いる抽象的な表現です。Goコンパイラでは、Prog 構造体がこの中間表現における個々の命令を表します。
  • ピーフホール最適化 (Peephole Optimization): コンパイラ最適化の一種で、生成された機械語コードの小さな「窓」(ピーフホール)を覗き、非効率な命令シーケンスをより効率的なものに置き換える手法です。peep.c はこの最適化を担当します。
  • レジスタ割り当て (Register Allocation): プログラムの実行速度を向上させるため、頻繁に使用される変数をCPUの高速なレジスタに割り当てるプロセスです。reg.c はこのレジスタ割り当てを担当します。
  • データ駆動型プログラミング: ロジックをコード内にハードコードするのではなく、データ構造(テーブル、設定ファイルなど)によってロジックを制御するプログラミングパラダイムです。これにより、ロジックの変更が容易になり、コードの柔軟性と保守性が向上します。
  • Prog 構造体: src/cmd/5g/go.h (または src/cmd/5g/gg.h を含むヘッダ) で定義されている、アセンブラの命令を表す主要な構造体です。各 Prog インスタンスは、オペコード (as)、ソースオペランド (from)、デスティネーションオペランド (to) などの情報を含みます。
  • CL (Change List): Goプロジェクトにおけるコミットや変更セットを指す用語です。CL 12637051 は、AMD64アセンブラ (6g) で同様の変更が行われた先行コミットを指します。

技術的詳細

このコミットの核心は、命令の特性を記述するための新しいデータ構造 ProgInfo と、その情報を取得するための proginfo 関数、そして命令ごとの特性を定義する progtable の導入です。

ProgInfo 構造体とフラグ

src/cmd/5g/opt.h に新しく追加された ProgInfo 構造体は、uint32 flags; という単一のフィールドを持ちます。この flags フィールドは、以下のようなビットフラグの組み合わせで命令の特性を表します。

  • 命令の種類:

    • Pseudo: 擬似命令(例: TEXT, GLOBL, TYPE など)。コード生成には直接関係しないが、コンパイラ内部で使われる。
    • OK: 命令に特筆すべき情報はないが、有効な命令である。
    • Move: 単純なデータ移動命令。
    • Conv: 型変換命令。
    • Cjmp: 条件付きジャンプ命令。
    • Break: 制御フローを中断する命令(フォールスルーしない)。例: RET
    • Call: 関数呼び出し命令。
    • Jump: 無条件ジャンプ命令。
    • Skip: データ命令(最適化時にスキップされるべき命令)。
  • オペランドのサイズ:

    • SizeB, SizeW, SizeL, SizeQ: バイト、ワード、ロング、クアッドワードのサイズ。
    • SizeF, SizeD: 単精度浮動小数点数、倍精度浮動小数点数のサイズ。
  • オペランドのアクセス特性:

    • LeftAddr: 左オペランドがアドレスとして扱われる。
    • LeftRead: 左オペランドが読み取られる。
    • LeftWrite: 左オペランドが書き込まれる。
    • RegRead: 中間レジスタが読み取られる(p->reg フィールド)。
    • CanRegRead: 中間レジスタが読み取られる可能性がある。
    • RightAddr: 右オペランドがアドレスとして扱われる。
    • RightRead: 右オペランドが読み取られる。
    • RightWrite: 右オペランドが書き込まれる。

これらのフラグを組み合わせることで、各命令の動作を詳細かつ効率的に表現できます。

progtableproginfo 関数

src/cmd/5g/prog.c に新しく追加された progtable は、ALAST(命令オペコードの最大値)までの配列で、各インデックスにその命令に対応する ProgInfo 構造体が格納されています。このテーブルは、コンパイラが生成する命令の基本的な情報を一元的に定義します。

proginfo(ProgInfo *info, Prog *p) 関数は、与えられた Prog (p) のオペコード (p->as) を使用して progtable から対応する ProgInfo を取得し、info ポインタが指す構造体にコピーします。また、この関数はいくつかの特殊なケース(例: RegRead フラグの調整や、条件付き命令での RightRead の追加)を処理し、最終的な ProgInfo を返します。

peep.creg.c の変更

peep.creg.c では、これまで命令のオペコードに基づいて個別に記述されていた switch 文の多くが、proginfo 関数を呼び出して ProgInfo フラグをチェックする形に置き換えられました。

例えば、peep.cpeep 関数では、以前は ADATA, AGLOBL などの擬似命令をスキップするために明示的な case 文がありましたが、変更後は proginfo(&info, p); if(info.flags & Skip) continue; のように、Skip フラグをチェックするだけでよくなりました。

同様に、reg.cregopt 関数では、オペランドの読み書きを判断するための複雑な switch 文が、info.flags & LeftRead, info.flags & RegRead, info.flags & (RightAddr | RightRead | RightWrite) といった簡潔なフラグチェックに置き換えられています。

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

このコミットによる主要なコード変更は以下の4つのファイルにわたります。

  1. src/cmd/5g/opt.h:

    • ProgInfo 構造体の定義を追加。
    • 命令の特性を表すビットフラグの enum を追加。
    • proginfo 関数のプロトタイプ宣言を追加。
  2. src/cmd/5g/peep.c:

    • peep 関数と subprop 関数内で、命令の特性を判断するために ProgInfo 構造体と proginfo 関数を使用するように変更。
    • 命令の種類に応じた冗長な switch 文を削除し、ProgInfo フラグによるチェックに置き換え。
  3. src/cmd/5g/prog.c: (新規ファイル)

    • progtable という静的配列を定義し、各命令オペコードに対応する ProgInfo フラグを初期化。
    • proginfo 関数の実装を提供。この関数は progtable を参照し、命令の特性フラグを返す。
  4. src/cmd/5g/reg.c:

    • regopt 関数内で、命令の特性(特にオペランドの読み書き)を判断するために ProgInfo 構造体と proginfo 関数を使用するように変更。
    • 命令の種類に応じた複雑な switch 文を削除し、ProgInfo フラグによる簡潔なチェックに置き換え。

コアとなるコードの解説

src/cmd/5g/opt.h の変更

// ... 既存のコード ...

typedef struct ProgInfo ProgInfo;
struct ProgInfo
{
	uint32 flags; // the bits below
};

enum
{
	// Pseudo-op, like TEXT, GLOBL, TYPE, PCDATA, FUNCDATA.
	Pseudo = 1<<1,
	
	// There's nothing to say about the instruction,
	// but it's still okay to see.
	OK = 1<<2,

	// Size of right-side write, or right-side read if no write.
	SizeB = 1<<3,
	SizeW = 1<<4,
	SizeL = 1<<5,
	SizeQ = 1<<6,
	SizeF = 1<<7, // float aka float32
	SizeD = 1<<8, // double aka float64

	// Left side: address taken, read, write.
	LeftAddr = 1<<9,
	LeftRead = 1<<10,
	LeftWrite = 1<<11,
	
	// Register in middle; never written.
	RegRead = 1<<12,
	CanRegRead = 1<<13,
	
	// Right side: address taken, read, write.
	RightAddr = 1<<14,
	RightRead = 1<<15,
	RightWrite = 1<<16,

	// Instruction kinds
	Move = 1<<17, // straight move
	Conv = 1<<18, // size conversion
	Cjmp = 1<<19, // conditional jump
	Break = 1<<20, // breaks control flow (no fallthrough)
	Call = 1<<21, // function call
	Jump = 1<<22, // jump
	Skip = 1<<23, // data instruction
};

void proginfo(ProgInfo*, Prog*);

この部分では、ProgInfo 構造体と、命令の様々な特性を表すビットフラグが定義されています。これらのフラグは、命令が擬似命令であるか (Pseudo)、どのオペランドを読み書きするか (LeftRead, RightWrite など)、命令の種類 (Move, Call, Jump など) を示します。proginfo 関数のプロトタイプもここで宣言されています。

src/cmd/5g/prog.c の新規追加

// Copyright 2013 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.

#include <u.h>
#include <libc.h>
#include "gg.h"
#include "opt.h"

enum
{
	RightRdwr = RightRead | RightWrite,
};

// This table gives the basic information about instruction
// generated by the compiler and processed in the optimizer.
// See opt.h for bit definitions.
//
// Instructions not generated need not be listed.
// As an exception to that rule, we typically write down all the
// size variants of an operation even if we just use a subset.
//
// The table is formatted for 8-space tabs.
static ProgInfo progtable[ALAST] = {
	[ATYPE]=	{Pseudo | Skip},
	[ATEXT]=	{Pseudo},
	[AFUNCDATA]=	{Pseudo},
	[APCDATA]=	{Pseudo},

	// NOP is an internal no-op that also stands
	// for USED and SET annotations, not the Intel opcode.
	[ANOP]=		{LeftRead | RightWrite},
	
	// Integer.
	[AADC]=		{SizeL | LeftRead | RegRead | RightWrite},
	// ... 多くの命令の定義が続く ...
	
	// Jumps.
	[AB]=		{Jump},
	[ABL]=		{Call},
	[ABEQ]=		{Cjmp},
	// ... 他の条件付きジャンプ命令 ...
	[ARET]=		{Break},
};

void
proginfo(ProgInfo *info, Prog *p)
{
	*info = progtable[p->as];
	if(info->flags == 0)
		fatal("unknown instruction %P", p);

	if((info->flags & RegRead) && p->reg == NREG) {
		info->flags &= ~RegRead;
		info->flags |= CanRegRead | RightRead;
	}
	
	if(((p->scond & C_SCOND) != C_SCOND_NONE) && (info->flags & RightWrite))
		info->flags |= RightRead;
}

このファイルは、progtable という静的配列を定義しています。このテーブルは、Goアセンブラの各命令オペコード (ATYPE, ATEXT, AADD など) に対応する ProgInfo フラグをマッピングします。これにより、命令の特性がコードからデータに分離されます。

proginfo 関数は、Prog オブジェクトを受け取り、そのオペコードに基づいて progtable から対応する ProgInfo をルックアップします。また、特定の条件(例: RegRead フラグと p->regNREG の場合、または条件付き命令で RightWrite がある場合)に基づいてフラグを調整するロジックも含まれています。これにより、命令の動的な特性も考慮されます。

src/cmd/5g/peep.c の変更例

// ... 既存のコード ...
peep(void)
{
	Reg *r, *r1, *r2;
	Prog *p;
	int t;
	ProgInfo info; // ProgInfo 構造体の宣言

/*
 * complete R structure
 */
	for(r=firstr; r!=R; r=r->link) {
		r1 = r->link;
		if(r1 == R)
			break;
		for(p = r->prog->link; p != r1->prog; p = p->link) {
			proginfo(&info, p); // proginfo を呼び出して命令情報を取得
			if(info.flags & Skip) // Skip フラグをチェック
				continue;

			r2 = rega();
			r->link = r2;
			r2->link = r1;

			r2->prog = p;
			p->regp = r2;

			r2->p1 = r;
			r->s1 = r2;
			r2->s1 = r1;
			r1->p1 = r2;

			r = r2;
		}
	}
// ... 既存のコード ...
}

peep 関数では、以前は ADATA, AGLOBL などの擬似命令をスキップするために明示的な switch 文を使用していましたが、この変更により proginfo(&info, p); if(info.flags & Skip) continue; のように、Skip フラグをチェックするだけでよくなりました。これにより、コードがより簡潔になり、新しい擬似命令が追加された場合でも peep.c を変更する必要がなくなります。

src/cmd/5g/reg.c の変更例

// ... 既存のコード ...
regopt(Prog *firstp)
{
	int i, z, nr;
	uint32 vreg;
	Bits bit;
	ProgInfo info, info2; // ProgInfo 構造体の宣言

	// ... 既存のコード ...

	/*
	 * build reg graph
	 */
	nr = 0;
	for(p=firstp; p != P; p = p->link) {
		proginfo(&info, p); // proginfo を呼び出して命令情報を取得
		if(info.flags & Skip) // Skip フラグをチェック
			continue;
		// ... 既存のコード ...
	}

	// ... 既存のコード ...

	if(r1 != R) {
		proginfo(&info2, r1->prog); // proginfo を呼び出して命令情報を取得
		if(info2.flags & Break) { // Break フラグをチェック
			r->p1 = R;
			r1->s1 = R;
		}
	}

	// ... 既存のコード ...

	if(info.flags & LeftRead) { // LeftRead フラグをチェック
		bit = mkvar(r, &p->from);
		for(z=0; z<BITS; z++)
			r->use1.b[z] |= bit.b[z];
	}

	if(info.flags & RegRead) { // RegRead フラグをチェック
		if(p->from.type != D_FREG)
			r->use1.b[0] |= RtoB(p->reg);
		else
			r->use1.b[0] |= FtoB(p->reg);
	}

	if(info.flags & (RightAddr | RightRead | RightWrite)) { // 右オペランドのアクセス特性をチェック
		bit = mkvar(r, &p->to);
		if(info.flags & RightAddr)
			setaddrs(bit);
		if(info.flags & RightRead)
			for(z=0; z<BITS; z++)
				r->use2.b[z] |= bit.b[z];
		if(info.flags & RightWrite)
			for(z=0; z<BITS; z++)
				r->set.b[z] |= bit.b[z];
	}
	// ... 既存のコード ...
}

regopt 関数でも、命令の特性を判断するための多くの switch 文が proginfo 関数と ProgInfo フラグのチェックに置き換えられています。これにより、コードが大幅に簡素化され、命令の特性に関するロジックが一元化されました。特に、オペランドの読み書きに関する複雑な条件分岐が、LeftRead, RegRead, RightAddr, RightRead, RightWrite といった明確なフラグの組み合わせで表現できるようになりました。

関連リンク

  • Go言語の公式リポジトリ: https://github.com/golang/go
  • Go言語のコンパイラに関するドキュメント: Go言語のコンパイラの内部構造に関する公式ドキュメントは、Goのソースコードリポジトリ内の src/cmd/compile/internal/ ディレクトリや、Goのブログ記事などで見つけることができます。
  • CL 12637051 (6g版の先行コミット): https://golang.org/cl/12637051 (このコミットの元となった cmd/6g 向けのリファクタリング)

参考にした情報源リンク

  • Go言語のソースコード (特に src/cmd/5g ディレクトリ)
  • Go言語のコミット履歴と変更リスト (Change List)
  • コンパイラ最適化に関する一般的な知識 (ピーフホール最適化、レジスタ割り当てなど)
  • データ駆動型プログラミングの概念