1 RDBMS 2 徹・底・研・究...DB Magazine 2010 January の文字コード 1 RDBMS徹・底・研・究 文字コードの基礎 SQL Serverで使用できる文字コードを説明
コンパイラ解説資料集 3 - Mie Universityコンパイラ解説資料集3...
Transcript of コンパイラ解説資料集 3 - Mie Universityコンパイラ解説資料集3...
コンパイラ 解説資料集 3
中間表現の種類 2
抽象構文木と後置記法 3
構文主導翻訳 4
lex と yacc による構文主導翻訳 6
構文主導翻訳の例 8
構文主導翻訳による属性値の受け渡し 10
プログラム実行時のメモリの割り当て 12
手続き実行時のコールスタックの動作 13
コード最適化の有無の比較 14
基本ブロックと制御フローグラフ 15
局所最適化 16
大域最適化 18
データフロー解析 20
算術式の目的コード生成 22
グラフ彩色によるレジスタ割り当て 24
山田 俊行
http://www.cs.info.mie-u.ac.jp/~toshi/lectures/compiler/
2020年 6月
1
コンパイラ 解説資料 (山田)
中間表現の種類
コンパイラがフロントエンド (解析部)で得たソースコードの情報は,木構造や命令列の形の中間表現と
してバックエンド (合成部)に渡る.
●中間表現 (intermediate representation, IR)
ソースコードの情報を,目的コードの種類に依存しない形で表したもの
最適化やコード生成をしやすい形にしたプログラムの表現
●抽象構文木 (abstract syntax tree, AST)
演算子やキーワードを節とし,演算対象などの構成要素を子として表す木
y = (x + 10) * 2
=jjjjjj
SSSSSS
y *jjjjjj UUUUUU
+kkkkkk UUUUUU 2
x 10
ソースコードを複数回走査する場合に,2度目以降の走査対象として利用
※プログラムの構文木は,解説資料「字句解析と構文解析」の例を参照
●後置コード (postfix code)
(仮想的な)スタック計算機の機械語コード
y = (x + 10) * 2
load xloadc 10addload 2mulstore y
データがそろってから演算を実行し,中間結果はスタック上に保持
● 3番地コード (3-address code)
データを処理する命令が x = y op z の形に表せるコード (飛び越しなどの制御命令もある)
num = 62;ones = 0;while num > 0 do
if num % 2 == 1 thenones = ones + 1;
finum = num / 2;
done
num = 62ones = 0
L1: _t1 = num > 0ifnot _t1 goto L2_t2 = num % 2_t3 = _t2 == 1ifnot _t3 goto L3_t4 = ones + 1ones = _t4
L3: _t5 = num / 2num = _t5goto L1
L2:
中間結果を一時変数 (上記の例では _ti)に格納する形で実行
2
コンパイラ 解説資料 (山田)
抽象構文木と後置記法
抽象構文木や後置記法を使うと,コンパイラの意味解析や中間コード生成を見通しよく実現できる.
● (構文)解析木 (parse tree)
文法の規則に沿って入力字句列が生成される過程を表す木
※解析木の走査順が構文解析器の実行順に対応
例 (算術式 ((x + 10) * 2) の解析木)
exp
aaaaaaaaaaaaaaaaaaaaaaaaaadddddddddddddd
QQQQQXXXXXXXXX
exp
fffffffffmmmmm
QQQQQXXXXXXXXX exp
exp exp
( ( IDx
+ NUM10
) * NUM2
)
exp
ccccccccccccccccgggggggg
WWWWWWWW\\\\\\\\\\\\\\\\
( exp
dddddddddddkkkkkk
TTTTTTZZZZZZZZZZZ * exp )
( exp + exp ) NUM2
IDx
NUM10
●抽象構文木 (abstract syntax tree, AST)
演算子やキーワードを節とし,演算対象などの構成要素を部分木として表す木
解析木に字句の意味情報を追加,意味に無関係な部分(区切り記号など)を省略
例 (算術式 ((x + 10) * 2) の構文木)
nnnnnnPPPPPP
nnnnnnPPPPPP
●後置記法 (postfix notation)
演算子をオペランド (演算対象)の後に置く記法
算術式 eを後置記法で表した式 ⟨ e ⟩を,次の通り帰納的に定義(1) eが部分式 e1, e2と中置演算子 op からなる式 (e1 op e2)のとき,⟨ e ⟩は列 ⟨ e1 ⟩ ⟨ e2 ⟩ op(2) eが変数か定数のとき,⟨ e ⟩は e自身
例 (算術式 ((x + 10) * 2) の後置式への変換)
⟨ ((x + 10) * 2) ⟩
= ⟨ (x + 10) ⟩ ⟨ 2 ⟩ * ((1)より)
= ((1)より)
= ((2)より)
3
コンパイラ 解説資料 (山田)
構文主導翻訳
構文主導翻訳は,構文解析時の動作を,生成規則の右辺中に埋め込んで指定する.各文法記号に,意味
情報を表す属性を与えて,その値を 定義・参照 しながら,指定位置で動作を実行する.この方法で,
意味解析や中間コード生成を簡潔に書ける.
●上昇型の構文主導翻訳の例
構文主導翻訳
次の構文主導翻訳は,算術式の列の文法に,値の計算と表示の動作を付加したものである.
prog → prog com| ε
com → exp ; { /*(1)*/ print(exp.val); }exp → ( exp + exp ) { /*(2)*/ exp.val = exp1.val + exp2.val; }
| ( exp * exp ) { /*(3)*/ exp.val = exp1.val * exp2.val; }| NUM { /*(4)*/ exp.val = NUM.val; }
この文法は,上昇型の構文解析向けのものであり,規則右辺の末尾まで解析が進んだとき,
つまり,構文解析器の還元動作のときに,{ } 内の動作を実行する.
各文法記号の属性は 文法記号.属性名 で表す.上の例では,非終端記号 exp と字句 NUM
の値を表す属性 exp.val と NUM.val を使う.
左辺が com の規則の動作は,右辺の式の値 exp.val の表示である.左辺が exp の規則の
動作は,左辺の属性値 exp.val を右辺の部分式の属性値から求める.和や積の規則のよ
うに,規則に同じ文法記号が複数現れる場合,exp1, exp2 のように,右辺の各出現を数字
で区別する.例えば,exp.val = exp1.val + exp2.val は,( e1 + e2 ) の形の式の
値 exp.val を部分式 e1 と e2 の値 exp1.val と exp2.val の和で求める.なお,字句の
属性 NUM.val は,字句解析器から得られるものと想定する.
属性付き解析木 prog
eeeeeeeeeeeeeeeeeeeee
ZZZZZZZZZZZZZZZZZZZZZZZ
prog com
rrrrrrrr
QQQQQQQQQQ
exp.val =
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaddddddddddddddddd
SSSSSSSYYYYYYYYYYYY
exp.val =
eeeeeeeeeeeejjjjjjj
TTTTTTTYYYYYYYYYYYY
exp.val =
exp.val =
exp.val =
ε ( ( NUM.val =
+ NUM.val =
) * NUM.val =
) ;
( ( 10 + 30 ) * 50 ) ;
出力
属性付き解析木
属性付き字句列
入力文字列
属性付き解析木は,解析木の各節の属性値が,{ } 内の属性値の定義からどう決まるかを表す.上記の exp 規則の動作のように,規則左辺の属性値が規則右辺の属性値から定まる
場合,属性付き解析木では,子の属性値から親の属性値が決まる.
4
●下降型の構文主導翻訳の例
文脈自由文法による構文主導翻訳
算術式列の文法の左再帰を除去して共通部分を括り出せば,再帰下降型の構文解析に適し
た文法が得られる.
prog → com prog| ε
com → exp ; { print(exp.val); }exp → ( exp op exp ) { exp.val = eval(exp1.val, op.type, exp2.val); }
| NUM { exp.val = NUM.val; }op → + { op.type = ’+’; }
| * { op.type = ’*’; }
下降型の構文解析向けの文法の場合も,上昇型と同様に,生成規則の右辺中の適切な位置
の { } 内に動作を埋め込む.ただし,文法の変形に伴って動作にも手を加える必要がある.
動作記述中の eval(v1, ty, v2) は,二つの部分式の値 v1, v2 から演算子の種類 ty に
応じて式全体の値を求める関数である.
再帰下降型の構文主導翻訳で,右辺の最後に左辺の属性値を決める場合には,構文解析手
続きの戻り値としてその値を返すと,翻訳の動作を見通しよく実現できる.
拡張文脈自由文法による構文主導翻訳
算術式列の文法は,右辺に選択や反復を許すと次の形で簡潔に書ける.
prog → 【 exp ; 】⋆
exp → ( exp【 + | * 】exp )| NUM
拡張文脈自由文法に対する構文主導翻訳には,確立した表示法がない.作動を付加するに
は,動作番号 { i } を規則内に書き,動作を別に指定すると読み易い.
prog → 【 exp {1} ; 】⋆
exp → ( exp【 + {2} | * {3} 】exp ) {4}| NUM {5}
{ /*1*/ print(exp.val); }{ /*2*/ exp.op = ’+’; }{ /*3*/ exp.op = ’*’; }{ /*4*/ exp.val = eval(exp1.val, exp.op, exp2.val); }{ /*5*/ exp.val = NUM.val; }
●参考文献
構文主導翻訳 (やそれを理論的に整理した属性文法)の詳細は,以下の文献で解説されている.
• 『コンパイラ』,第 2版,A. V. エイホ,M. S. ラム,R. セシィ,J. D. ウルマン,サイエンス社,
2009.5章 構文主導翻訳.
• 『プログラミング言語処理系』,佐々政孝,岩波書店,1980.5.2節 属性文法.
5
コンパイラ 解説資料 (山田)
lex と yacc による構文主導翻訳
lex と yacc を併用して構文主導翻訳を実現するプログラムの例を示す.
●上昇型の構文主導翻訳
字句解析
/* calc.l -- 算術式の字句解析器の仕様 */
%{#include "calc.tab.h" // 構文解析器と共用の宣言void yyerror(char *); // 動作記述で使う関数を宣言%}
%% // 字句の属性値 yylval を決めて,// 字句の種類を表すコードを返す
[+*();] { return *yytext; }[0-9]+ { yylval = atoi(yytext); return NUM; }
[ \t\n]+ // 空白類を無視
. { yyerror("illegal character"); }
%%
// 誤り処理の関数を定義void yyerror(char *msg) { printf("%s at ’%c’\n", msg, *yytext); exit(1); }
構文解析と構文主導翻訳
/* calc.y -- 上昇型の構文主導翻訳による,算術式の値の計算と表示 */
%{// 動作記述に必要な宣言extern int yylex(void); // 字句解析関数 (字句解析器で定義)extern void yyerror(char *); // 誤り処理関数 (字句解析器で定義)#define YYSVAL int // 属性値の型#include <stdio.h> // 動作記述で使う printf() 向け%}
// 文法記述に必要な指定%token NUM
%%
// 文法(文脈自由文法) // 動作(プログラム片)
prog : prog com| /* 空 */;
com : exp ’;’ { printf("%d\n", $1); }; // $n は右辺の第 n 記号の属性
exp : ’(’ exp ’+’ exp ’)’ { $$ = $2 + $4; }| ’(’ exp ’*’ exp ’)’ { $$ = $2 * $4; }| NUM { $$ = $1; }; // $$ は左辺の記号の属性
%%
int main(void) { return yyparse(); }
実行例$ flex calc.l; bison -d calc.y; gcc lex.yy.c calc.tab.c -lfl # -d で flex と共用のヘッダを生成$ echo ’0; ((10 + 30) * 50);’ | a.out02000
6
●下降型の構文主導翻訳
字句解析
(上昇型と同じ calc.lを利用)
構文解析
/* calc.h -- 字句解析器と構文解析器で共用する宣言 */
enum { END = 0, NUM = 128 };int yylval;
/* calc.c -- 下降型の構文主導翻訳による,算術式の値の計算と表示 */
#include "calc.tab.h" // 字句解析器と共用の宣言extern int yylex(void); // 字句解析関数 (字句解析器で定義)extern void yyerror(char *); // 誤り処理関数 (字句解析器で定義)#include <stdio.h> // 動作記述で使う printf() 向けint token; // 字句の種類int lexval; // 字句の値
// 構文解析の補助関数void error(void) { yyerror("syntax error"); }void read_token(void) { token = yylex(); lexval = yylval; }void match_token(int tok) { if (token == tok) read_token(); else error(); }
int parse_exp(void); // 解析関数は規則左辺の属性値を返す
// 構文解析 // 意味動作
void parse_prog(void) {/* prog -> 【 exp ; 】☆ */while (token != END) { int val = // 右辺の exp の属性値parse_exp(); printf("%d\n", val);match_token(’;’);
}}
int parse_exp(void) { int val; // 左辺 exp の属性値if (token == ’(’) {/* exp -> ( exp 【 + │ * 】 exp ) */match_token(’(’); int val1 = // 右辺の一つ目の exp の属性値parse_exp(); int op = token;if (token != ’+’ && token != ’*’)
error();read_token(); int val2 = // 右辺の二つ目の exp の属性値parse_exp();match_token(’)’); val = (op == ’+’) ? val1+val2 : val1*val2;
} else {/* exp -> NUM */ val = lexval; // 字句 NUM の属性値match_token(NUM);
} return val;}
int main(void) {read_token();parse_prog();return 0;
}
実行例$ cp calc.h calc.tab.h; flex calc.l; gcc lex.yy.c calc.c -lfl$ echo ’0; ((10 + 30) * 50);’ | a.out02000
7
コンパイラ 解説資料 (山田)
構文主導翻訳の例
構文主導翻訳を使えば,木構造の走査に伴う動作として,意味解析や中間コード生成を簡潔に書ける.
●算術式の後置形式への変換
exp → ( exp op exp ) { /*(1)*/ gen(op.s); }| NUM { /*(2)*/ gen(NUM.s); }
op → + { /*(3)*/ op.s = "+"; }| * { /*(4)*/ op.s = "*"; }
op.s, NUM.s · · · 演算子や数の文字列gen() · · · 翻訳結果の生成手続き (表示,列への追加,など)
exp
ddddddddddddddddddddddddddddddddddddd
jjjjjjjjjjjjjjjjjjjj
>>>>
>>>>
>
OOOOOOOOOOOOOO
出力
exp
oooooooooooooo
����
����
�
>>>>
>>>>
>
OOOOOOOOOOOOOO op.s =
exp 属性付き解析木
exp op.s =
exp
( ( NUM.s =
+ NUM.s =
) * NUM.s =
) 属性付き字句列
( ( 10 + 30 ) * 50 ) 入力文字列
●算術式の構文木への変換
exp → ( exp op exp ) { exp.tr = make_op(op.ty, exp1.tr, exp2.tr); }| NUM { exp.tr = make_num(NUM.val); }
op → + { op.ty = ’+’; }| * { op.ty = ’*’; }
exp.tr · · · 部分木の根へのポインタop.ty · · · 演算子の種類NUM.val · · · 定数の値 (字句の属性値)
make_op(), make_exp() · · · 節を生成して部分木の根へのポインタを返す関数
8
●式や代入文に対する 3番地コードの生成
stm → ID { stm.var = use_var(ID.val); }= exp ; { gen_asmt(stm.var, exp.res); }
exp → exp + exp { exp.res = gen_opr(’+’, exp1.res, exp2.res); }| exp * exp { exp.res = gen_opr(’*’, exp1.res, exp2.res); }| ( exp ) { exp.res = exp1.res; }| ID { exp.res = use_var(ID.val); }| NUM { exp.res = use_con(NUM.val); }
※コード片は,右辺の途中にも書ける (例: 上の stm 規則)
※ *を +より優先し,*と +は左結合であると考えて,曖昧さを解消
※文法が左再帰なので,下降型より上昇型の構文解析に適する (構文木の走査にも使える)
stm.var · · · 代入文の左辺の変数exp.res · · · 部分式の結果の変数や定数ID.val, NUM.val · · · 識別子や数の属性値
gen_asmt(), gen_opr() · · · 一時変数を作り,代入命令や演算命令を生成use_var(), use_con() · · · 変数や定数の利用準備
●制御文に対する 3番地コードの生成
条件文 if 式 then 文列 1 else 文列 2 fi
式のコード (結果は tmp)
ifnot tmp goto L1
文列 1のコードgoto L2
L1 : 文列 2のコードL2 :
反復文 while 式 do 文列 done
L1 : 式のコード (結果は tmp)
ifnot tmp goto L2
文列のコードgoto L1
L2 :
seq → seq stm
| ε
stm → { stm.lab1 = new_lab(); stm.lab2 = new_lab(); }if exp { gen_ifnot_goto(exp.res, stm.lab1); }then seq { gen_goto(stm.lab2);
gen_lab(stm.lab1); }else seq fi { gen_lab(stm.lab2); }
| { stm.lab1 = new_lab(); stm.lab2 = new_lab();
gen_label(stm.lab1); }while exp { gen_ifnot_goto(exp.res, stm.lab2); }do seq done { gen_goto(stm.lab1);
gen_lab(stm.lab2); }
stm.lab1, stm.lab2 · · · 飛び越し命令に使うラベルexp.res · · · 式の評価結果の属性値
new_lab() · · · 新しいラベルを生成gen_ifnot_goto(), gen_goto() · · · 飛び越し命令を生成gen_label() · · · ラベルを生成
9
コンパイラ 解説資料 (山田)
構文主導翻訳による属性値の受け渡し
構文主導翻訳で,属性値を解析木の子から親へと上向きに計算できない場合,親や弟から受け渡した
属性値を使って計算できる.
●左結合の演算子を使う上昇型の構文主導翻訳
定数と減算からなる算術式の値を求めるための,構文主導翻訳を示す.
1 : exp → exp - NUM { exp.val = exp1.val - NUM.val; }2 : | NUM { exp.val = NUM.val; }
入力文字列が 9 - 3 - 2 のときの属性付き解析木は次の通りである.
exp.val = 4
eeeeeeeeeeeeeeeeeeTTTTTTTTTT
exp.val = 6
jjjjjjjjjjTTTTTTTTTT
exp.val = 9
NUM.val = 9
- NUM.val = 3
- NUM.val = 2
上記の文法は左再帰的なので,上昇型の構文解析に適しており,左結合での演算結果は,
値を葉から根へと上向きに計算すれば求まる.
●左結合の演算子を使う下降型の構文主導翻訳
上記の文法の左再帰を除去して,下降型の解析向けの文法を得る.
1 : exp → NUM exp′
2 : exp′ → - NUM exp′
3 : | ε
等価変換後の文法は,演算子を右結合で捉える構文木を生成する.
exp
iiiiiiiiiiVVVVVVVVV
exp′
hhhhhhhhh[[[[[[[[[[[[[[[[[[
exp′
hhhhhhhhhWWWWWWWWW
exp′
NUM.val = 9
- NUM.val = 3
- NUM.val = 2
ε
子から親へと上向きに値を計算するだけでは,左結合での演算結果が得られない.
10
●左結合の演算子を使う下降型の構文主導翻訳 (続き)
左再帰を除去した算術式の文法を再掲する.
1 : exp → NUM exp′
2 : exp′ → - NUM exp′
3 : | ε
左結合で値を求めるには,定めた値を渡す属性以外に加えて,受け取った値を渡す属性を
使う.ここでは,2属性 i (input) と o (output) を使って属性付き解析木を作る.
exp .o =
kkkkkkkkkkkTTTTTTTTTTT
.i = exp′ .o =
jjjjjjjjjjjZZZZZZZZZZZZZZZZZZZZ
.i = exp′ .o =
jjjjjjjjjjjUUUUUUUUUU
.i = exp′ .o =
NUM.o = 9
- NUM.o = 3
- NUM.o = 2
ε
解析木の子から親へと属性値を上向きに定めるだけでなく,属性値を定めるために親や弟
から受け継いだ属性値を使う場所もあることに注意する.属性値を定義する動作を変形後
の文法に付加して,下降型の構文主導翻訳を得る.
1 : exp → NUM {1a} exp′ {1b}2 : exp′ → - NUM {2a} exp′ {2b}3 : | ε {3}
{ /*1a*/ exp’.i = NUM.o; } { /*1b*/ exp.o = exp’.o; }{ /*2a*/ exp’1.i = exp’.i - NUM.o; } { /*2b*/ exp’.o = exp’1.o; }
{ /*3*/ exp’.o = exp’.i; }
再帰下降型の構文主導翻訳で,受け渡す属性値を右辺の最後に決める場合,値を受け取る
属性を構文解析手続きの引き数に指定し,値を渡す属性を手続きの戻り値として指定する
と,翻訳の動作を見通しよく実現できる.
なお,文法の右辺に反復や選択を許せば,より簡潔な構文主導翻訳が得られる.
exp → NUM {1}【 - NUM {2} 】⋆
{ /*1*/ exp.val = NUM.val; }{ /*2*/ exp.val = exp.val - NUM.val; }
この例では,値の受け渡しの両方に一つの属性 val を使う.属性値を一度だけ定めて参照
する使い方と違い,属性値を再定義していることに注意する.
11
コンパイラ 解説資料 (山田)
プログラム実行時のメモリの割り当て
プログラム実行時には,データの利用目的に応じて,メモリの適切な場所が使われる.
●実行時のメモリの分割
一つのプログラムの実行用に割り当てられたメモリ全体は,複数の領域に分けて使われる.
コード領域 機械語コード
静的領域 大域データ (コンパイル時に割り当て)
ヒープ領域 動的データ (実行時に 確保・開放)
↓
.↑
スタック領域 局所データ (手続きの実行で 確保・開放)
●コールスタックの構成
コールスタックには,手続き呼び出しごとにスタックフレームが下図のように作られる.
スタック成長↑
作業領域← SP
局所変数
制御領域← BP
退避した BP〃 PC (戻り番地)
呼び出し先
引き数
呼び出し元
●参考文献
実行時環境については,以下の文献が参考になる.
• 『コンパイラ』,第 2版,A. V. エイホ,M. S. ラム,R. セシィ,J. D. ウルマン,サイエンス社,
2009.7章 実行時環境.
• 『プログラミング言語処理系』,佐々政孝,岩波書店,1980.6章 実行時環境.
12
コンパイラ 解説資料 (山田)
手続き実行時のコールスタックの動作
手続きの呼び出しや復帰の際には,コールスタックを使って,実行に必要なデータを 退避・復元 する.
●呼び出し木 と コールスタック
手続きの実行がどのように実現されるか,簡単なプログラム例で解説する.非負整数の 2
進表示を求める次の Cプログラムの動作を考える.2進表示を得るには,与えられた整数
を繰り返し 2で割って,余りを逆順に並べればよい.
void binary(int n) {int b = n%2;if (n >= 2) binary(n/2);putchar(’0’ + b);
}int main(void) {binary(6);return 0;
}
2進表示の再帰手続き binary()は,自身を呼び出すたびに整数を 2で割り,呼び出しから
戻るたびに最下位ビットを putchar()で表示する.手続き main(), binary(), putchar()
の呼び出しと復帰の様子は,呼び出し木 (call tree) で表せる.
main()
binary(6)
mmmmmmQQQQQQ
binary(3)
mmmmmmQQQQQQ
putchar(’0’)
binary(1)
QQQQQQputchar(’1’)
putchar(’1’)
実行中の手続きの情報は,コールスタックという記憶域に格納される.手続きの呼び出し
ごとに,1回の呼び出しに必要なデータを格納する記憶域であるスタックフレームがコー
ルスタック上に積まれ,復帰時に取り除かれる.
//
oo
//
oo
//
oo
OO
��
OO
��
OO
��
13
コンパイラ 解説資料 (山田)
コード最適化の有無の比較
コンパイラのコード最適化では,中間コードや目的コードを効率の良いものに変える.
●最適化の有無の比較
コード最適化で目的コードがどう変わるか,gcc を使って比べる.
$ gcc -S ones.c -o ones.s # -S アセンブリコードを出力$ gcc -S -O3 ones.c -o ones-opt.s # -O3 最高度の最適化を実行$ wc -l ones.s; wc -l ones-opt.s # -l 行数を表示
62 ones.s38 ones-opt.s
$ cat ones.c ones.s ones-opt.s # ソースコードと各アセンブリコード (x86-64)を表示
int num_ones(int num) { num_ones: num_ones:int ones = 0; pushq %rbp xorl %eax, %eaxwhile (num > 0) { movq %rsp, %rbp testl %edi, %edi
if (num % 2 == 1) movl %edi, -20(%rbp) jle .L3ones++; movl $0, -4(%rbp) .L8:
num /= 2; jmp .L2 leal 1(%rax), %edx} .L4: testb $1, %dilreturn ones; movl -20(%rbp), %eax cmovne %edx, %eax
} movl %eax, %edx sarl %edisarl $31, %edx jne .L8
int main(void) { shrl $31, %edx .L3:num_ones(62); addl %edx, %eax repreturn 0; andl $1, %eax ret
} subl %edx, %eax main:cmpl $1, %eax xorl %eax, %eaxjne .L3 retaddl $1, -4(%rbp)
.L3:movl -20(%rbp), %eaxmovl %eax, %edxshrl $31, %edxleal (%rdx,%rax), %eaxsarl %eaxmovl %eax, -20(%rbp)
.L2:cmpl $0, -20(%rbp)jg .L4movl -4(%rbp), %eaxleaveret
main:pushq %rbpmovq %rsp, %rbpmovl $62, %edicall num_onesmovl $0, %eaxleaveret
gcc には,最適化オプションが多数あり,実行する最適化の種類を細かく指定できる.詳細は,オンラインマニュアル man gcc などで調べられる.
14
コンパイラ 解説資料 (山田)
基本ブロックと制御フローグラフ
制御フローグラフは,命令間の制御の流れを表す有向グラフで,コード最適化に使う中間表現である.
●ソースコード
num = 62;
ones = 0;
while num > 0 do
if num % 2 == 1 then
ones = ones + 1;
fi
num = num / 2;
done
●中間コード
num = 62
ones = 0
L1: _t1 = num > 0
ifnot _t1 goto L2
_t2 = num % 2
_t3 = _t2 == 1
ifnot _t3 goto L3
_t4 = ones + 1
ones = _t4
L3: _t5 = num / 2
num = _t5
goto L1
L2:
●基本ブロックと制御フローグラフ
基本ブロック順次実行される命令列下の 2条件を満たす命令列のうち極大なもの
制御フローグラフ基本ブロック間の制御の流れを表す有向グラフ
(条件 1) 他のブロックから入れるのはブロック先頭だけ
(条件 2) ジャンプ命令が許されるのはブロック末尾だけ
num = 62
ones = 0
L1: _t1 = num > 0
ifnot _t1 goto L2
_t2 = num % 2
_t3 = _t2 == 1
ifnot _t3 goto L3
_t4 = ones + 1
ones = _t4
L3: _t5 = num / 2
num = _t5
goto L1
L2:
15
コンパイラ 解説資料 (山田)
局所最適化
局所最適化とは,基本ブロック内の命令列に対して使う最適化である.目的コードの種類に依存しない(機械独立の)最適化は,通常,中間コードに適用するが,この解説では分かりやすさを重視して,最適化の例を主にソースコードの変換として示す.
●局所最適化の代表例
定数伝播値の変わらない変数をその定数値で置換low = 0;
high = 10;
mid = (low + high) / 2;
low = 0;
high = 10;
mid = ( + ) / 2;
定数畳み込み定数式の値をコンパイル時に計算
if (size > 10 * 5) ... if (size > ) ...
恒等式の利用演算子の性質を使い式を簡単化
(1 + n) - 1 n * 8 x & x *&p
共通部分式の除去同じ値の部分式を一度だけ計算
s1 = size * n;.........
s2 = size * n; s2 =
コピー伝播変数への値のコピー dst = src の後,dst も src も変わらないとき,dst を src に置換
old = n;.........
new = old + 1; new =
不要コードの除去実行結果に影響しないコードを削除
上の例で,old を他で使わないなら,old = n; を削除
16
●DAG を使った局所最適化
DAG (directed acyclic graph)
閉路のない有向グラフ.基本ブロック中の命令列の中間表現に使える.
中間コードの命令列
1: j = i + 1
2: i = k
3: x = i + 1
4: y = k + 1
命令列を表す DAG
DAG の構成法(1) 各変数 v の初期値 v0 を表す節点と,各定数を表す節点を作る.(2) 命令列の先頭から順に,各命令 i (による変数の定義)を,以下の手順で節点 nに対応
させる.その際,節点 nには変数の最新の定義を表す列を添える.• 命令 iが v = a op bの形なら,nのラベルは演算子 opで,nの左右の子は a, b の値(の最新の定義)に対応する節点である.そのような節点 nがなければ追加する.• 命令 iが v = aの形なら,nは aの値 (の最新の定義)に対応する節点である.
DAG に基づく最適化
1: j = i + 1
2: i = k
3: x = i + 1
4: y = k + 1
j = i + 1
i = k
x =
y =
●参考文献
機械独立の最適化については,以下の文献が参考になる.
• 『コンパイラ』,第 2版,A. V. エイホ,M. S. ラム,R. セシィ,J. D. ウルマン,サイエンス社,2009.8.5節 基本ブロックの最適化,9章 機械独立の最適化.• 『プログラミング言語処理系』,佐々政孝,岩波書店,1980.9章 コード最適化.
17
コンパイラ 解説資料 (山田)
大域最適化
大域最適化とは,基本ブロックをまたぐ最適化である.通常,この最適化は中間コードに適用するが,この解説では分かりやすさを重視して,最適化の例をソースコードの変換として示す.
●ループ最適化
ループ不変コードの移動繰り返し中に値の変わらない式の計算を,ループ外に移動
do {
c = a + b;......
} while ( ... )
do {
} while ( ... )
ループ内の演算の強さの軽減繰り返される演算を,結果が同じで計算コストの小さいものに置換
x = 0
for (i = 1; ... ; i++) {
x = 5*i;
}
x = 0
for (i = 1; ... ; i++) {
x
}
帰納変数の除去ループの反復ごとに一定値だけ変化する変数が不要なら削除
for ( ; ... ; ) {
}
ループ展開ループの繰り返し部分を必要な回数だけ展開し,ループ制御の手間を削減
for (i = 3; i > 0; i--)
val *= 5;
val *= 5;
for ( ... ; i++) {
}
for ( ... ; i += 2) {
}
18
●呼び出し最適化
インライン展開手続き呼び出しを手続き本体で置換し,呼び出しと復帰の手間を削減
int power(int m, int n) {
int i, val = 1;
for (i = n; i > 0; i--)
val *= m;
return val;
}...
y = power(x, 3);...
int i, val = 1;
for (i = n; i > 0; i--)
val *= m;
末尾再帰の除去手続き終了直前の再帰呼び出しを goto で置き換え,呼び出しと復帰の手間を削減
int pwr(int m, int n, int val) {
if (n > 0)
return pwr(m, n-1, val*m);
else
return val;
}
int pwr(int m, int n, int val) {
if (n > 0) {
} else {
return val;
}
}
●参考文献
機械独立の最適化については,以下の文献が参考になる.
• 『コンパイラ』,第 2版,A. V. エイホ,M. S. ラム,R. セシィ,J. D. ウルマン,サイエンス社,2009.8.5節 基本ブロックの最適化,9章 機械独立の最適化.• 『プログラミング言語処理系』,佐々政孝,岩波書店,1980.9章 コード最適化.
19
コンパイラ 解説資料 (山田)
データフロー解析
データフロー解析は,変数や式についての,定義と使用の依存関係を系統的に調べる方法である.この解説では,到達定義解析を題材に,データフロー解析の基本的な考え方を学ぶ.
●プログラム
データフロー解析は,制御フローグラフで表されたプログラムに適用する.具体例として,3 の n乗を y に代入するプログラムを扱う.
m = 3
val = 1
i = n
L1: ifnot i > 0 goto L2
val = val * m
i = i - 1
goto L1
L2: y = val
��
��
@A
GF //
ED
BCoo
●データフロー方程式
ブロック i の入り口と出口に到達する定義の集合 Ini, Outi は,集合に関する等式からなる次の連立方程式を満たす.
上記のプログラムに関するデータフロー方程式は,以下の 8個の等式からなる.
i Gen(i) Kill(i)
��
��
@A
GF //
ED
BCoo
In1 =Out1 = ∪ (In1\ )
In2 =Out2 = ∪ (In2\ )
In3 =Out3 = ∪ (In3\ )
In4 =Out4 = ∪ (In4\ )
20
●データフロー方程式の反復解法
データフロー方程式の解は,反復して近似解を改良する方法で求められる.
In1 = ∅Out1 = {1m, 1v, 1i} ∪ (In1 \ {3v, 3i})In2 = Out1 ∪Out3Out2 = In2
In3 = Out2Out3 = {3v, 3i} ∪ (In3 \ {1v, 1i})In4 = Out2Out4 = {4y} ∪ In4
方程式を右辺から左辺への代入とみなす.全てが空集合であるという近似解から始めて,上から順に代入することを,結果が変わらなくなるまで繰り返せば,(最小)解が得られる.
+1m
+1v −3v+1i −3i
+3v −1v+3i −1i
+4y
��
��
@A
GF //
ED
BCoo
反復 0 1 2 3
In1 ∅Out1 ∅
In2 ∅Out2 ∅
In3 ∅Out3 ∅
In4 ∅Out4 ∅
解析結果から 1v, 3v ∈ In4 なので,第 1ブロックと第 3ブロックの valの定義が,第 4ブロック (の valの使用)に到達するとわかる.→ 定数伝播が不可能
また,In3 に属す mの定義は 1mだけなので,第 1ブロックの mの定義だけが第 3ブロック (の mの使用)に到達するとわかる.→ 定数伝播が可能
●参考文献
データフロー解析については,以下の文献が参考になる.
• 『コンパイラ』,第 2版,A. V. エイホ,M. S. ラム,R. セシィ,J. D. ウルマン,サイエンス社,2009.9.2節 データフロー解析の概要,9.3節 データフローの基礎.• 『プログラミング言語処理系』,佐々政孝,岩波書店,1980.9.5節 データフロー解析と各種最適化変換.
21
コンパイラ 解説資料 (山田)
算術式の目的コード生成
使用レジスタをなるべく節約して算術式の値を計算する目的コード,の生成方法を学ぶ.
●目的コードの命令
目的コードの生成に,次の 2種類の命令が使えることを想定する.
演算命令
op reg loc op は四則 +, -, *, / のどれかreg はレジスタ,loc は変数か定数かレジスタ命令の意味は reg ← reg op loc
転送命令
LD reg loc reg はレジスタ,loc は変数か定数かレジスタ命令の意味は reg ← loc
●必要レジスタ数の計算
算術式の各部分式 e の値の計算に必要なレジスタ数 numreg(e) を,次の場合分けで再帰的に求める.
e が変数か定数のとき
numreg(e) = 0
e が e1 op e2 の形で,numreg(e1) = n1, numreg(e2) = n2 のとき
numreg(e)は,次の表で定まる.
\ n2 = 0 > 0
n1 \ op :可換 op :非可換
= 0 1 n2 max(2, n2)
> 0 n2 + 1 max(n1, n2)
n1 // n2 = n1 ̸= n1
22
●目的コード生成
具体例な算術式に対して numregを求めて,目的コードを生成する例を以下に示す.
\ n2 = 0 > 0n1\ op :可換 op :非可換
= 0 1 n2 max(2, n2)
> 0 n2 + 1 max(n1, n2)
n1// n2 = n1 ̸= n1
例 1
*
sssss LLL
LL
x +
sssss LLL
LL
w *
sssss MMMM
v 5
例 2
/
oooo NNNN
y -
sssss NNNN
x /
qqqq NNNN
w *
sssss MMMM
v 5
LD R1 v
* R1 5
+ R1 w
* R1 x
LD R1 v
* R1 5
LD R2 w
/ R2 R1
LD R1 x
- R1 R2
LD R2 y
/ R2 R1
例 3
+
iiiiiiiiWWWWWWWW
+
sssss MMMM /
qqqq OOOO
v 5 w 3
例 4
+
iiiiiiiiWWWWWWWW
+
sssss MMMM /
qqqq NNNN
x y w *
sssss MMMM
v 5
LD R1 v
* R1 5
LD R2 w
/ R2 3
+ R1 R2
LD R1 v
* R1 5
LD R2 w
/ R2 R1
LD R1 x
* R1 y
+ R1 R2
23
コンパイラ 解説資料 (山田)
グラフ彩色によるレジスタ割り当て
中間コードに現れる各変数のレジスタへの割り当ては,変数の干渉を表すグラフで決定できる.
●中間コード
diff = m - n
sq = n * n
val = m * sq
変数の定義 □ と最後の使用 ○
●変数の生存期間と干渉
変数の生存期間 · · · 変数が定義されてから (再定義されすに)最後に使われるまで変数の干渉 · · · 生存期間の重なり
diff = m - n
sq = n * n
val = m * sq
m n d s v
●レジスタ干渉グラフ
2変数の生存期間が重なり,同一レジスタを使えない
⇔ 頂点間に辺(2頂点が隣接)
n
m
v
s d
●グラフ彩色によるレジスタ割り当て
レジスタを k個使えば,全変数を割り当てられる
⇔干渉グラフの頂点を k色使って塗り,隣接頂点が必ず違う色にできる(干渉グラフが k彩色可能)
出る辺の数が k未満の頂点 (と付随する辺)を選んで消すことを繰り返し,全頂点を消せれば,グラフは k彩色可能
頂点と辺の削除を逆にたどって,頂点と辺を加えるごとに,隣接頂点で未使用の色番号の最小値を割り当てる
24
●グラフ彩色によるレジスタ割り当ての例
例題の中間コードの変数への,レジスタ 3個の割り当て
干渉グラフに対して,辺数が 3未満の頂点 (と付随する辺)の削除を反復
n
m
v
s d
m
v
s d
v
s d
v
s
v (空)−n
//
oo2
−m//
oo1
−d//
oo3
−s//
oo2
−v//
oo1
逆順で,頂点と辺の追加ごとに,隣接頂点で未使用の (最小)番号を割り当て
●参考文献
目的コード生成については,以下の文献が参考になる.
• 『プログラミング言語処理系』,佐々政孝,岩波書店,1980.8章 コード生成.• 『コンパイラ』,第 2版,A. V. エイホ,M. S. ラム,R. セシィ,J. D. ウルマン,サイエンス社,2009.8章 コード生成.
25