6: Activation Records

(関数の実行時データ構造の一般論)p.124

例(Tiger言語の関数宣言サンプル)の説明

 

Higher-order functions

(関数の実行時データを格納するのにスタックを用いる際に問題となる機構:高階関数)p.125p.126

例(プログラム例 6.1

左はMLのコード、右はCの擬似コード(Cでは入れ子の関数宣言ができない。)

このように(外側の関数の変数を内側の変数が使用できるような)入れ子になった関数宣言の仕組みと関数が値として返値になったり変数に代入できるような仕組みの組み合わせにおいて局所変数は関数の実行を越えた寿命を持つ。

 

 

入れ子定義可能

入れ子定義不可

関数を値として取り扱える

ML, Scheme

C, C++

関数は値にならない

Pascal, Tiger, Fortran90

FORTRAN77, Java(←メソッド)

6.1: Stack frames

(一般的なスタックフレームの概観)p.126p.128

抽象的なpushpopを備えただけのデータ構造では不充分な理由

大きな配列としてのスタックの概観

 

典型的なスタック・フレーム内配置(図6.2の説明)

The frame pointer

(フレーム・ポインタの説明)p.128

設計例

何故フレーム・ポインタが必要なのか?

(フレーム内の全ての局所変数、引数、他にアクセスするのに単にスタック・ポインタからのオフセットによることにしない理由)

Registers

(レジスタの退避について)p.128p.129

レジスタを利用する理由

 

レジスタを安全に利用するための退避

退避責任の分担

番号1623のレジスタ

「呼び出し先退避(callee-saved)」で保護されると想定。

その他のレジスタ

「呼び出し元退避(caller-saved)」で保護はされないと想定。

Parameter Passing

(引数の渡し方について)p129p.131

実際にどのようにメモリとのやり取りを削減するか?

 

これには同時に適用可能な4つの回答がある。

    1. 関数を呼び出さない「末端(leaf…葉)手続き」と称する関数がある。末端手続きの割合に着目すると(楽観的な)見積もりとして平均的な関数呼び出しではそれ以上全く関数が呼び出されないか少なくとも2つの関数を呼び出すので、関数呼び出し関係から「木」を描くと末端でない内部の節点より末端の節点が多い。つまり最も多いのは末端手続きなのである。末端手続きは渡された引数をメモリに退避する必要がない。実際、しばしば全くスタック・フレームを必要としない。これは重要な節約である。
    2. 全プログラムの全ての関数を一度に解析して「手続きを跨いだレジスタ割り当て(interprocedural register allocation)」を行う最適化コンパイラがある。この場合、レジスタに保持する局所変数と受取る引数について異なった手続きには異なったレジスタを割り当てる。故に例えばf(x)xr_1に受取るが、h(z)を呼び出す際にはzr_7で渡す。
    3. fが末端の関数でなくてさえ関数hの呼び出しに際して引数xの利用は多分終了している(技術的にはhが呼ばれた時点でxは「死んだ変数(dead variable)」である。)。この場合、退避することなしにr_1は上書きできる。
    4. 「レジスタ・ウィンドウ(register windows)」という仕組みを備えているアーキテクチャがある。レジスタ・ウィンドウは関数が実行されるたびにメモリとのやり取りなしに新規のレジスタを一組提供する。

受取った引数をフレーム内のどこに書き出せばよいか?

C言語における問題

 

参照渡しについて

Return Address

(戻り番地について)p.131

Frame-resident variables

(値がスタック・フレーム内に置かれる場合のまとめ)p.131p.132

値がスタック・フレームに書き出される条件

    1. その変数は参照渡しされ、メモリ上にアドレスを持つ必要がある。(あるいはその変数はC言語で&演算子によってアドレスを取られる。)
    2. その変数はそれが定義されている関数の中で入れ子に定義されている関数からアクセスされる。(とは言え、「手続きを跨いだレジスタ割り当て(interprocedural register allocation)」が行われているなら、レジスタに置かれていても入れ子になった関数はその変数にアクセスできるので構わない。)
    3. その変数の内容は一つのレジスタに置くには大きすぎる。(とはいえ、効率のため大きな値を幾つかのレジスタに展開するコンパイラもある。)
    4. その変数は配列にあり内容を取り出すには配列演算が必要である。
    5. その変数を保持しているレジスタが特別な目的(既に述べた引数渡しのためなど)のために必要になった。(もっともコンパイラはメモリに退避せずに他のレジスタに置こうとするだろう。)
    6. 局所変数や一時的な値が多すぎて全てをレジスタに収め切れなくなり、それらのうちの幾つかをスタック・フレームに「こぼし(spilled)」た。

 

Static links

(ブロック構造の実現)p.132p.134

プログラム例6.3の説明

write関数は外側の変数outputを参照し、indent関数は外側の変数noutputを参照する。このようにするためには関数indentは(変数isのために)自身のフレームにアクセスできるだけでなく(変数nのために)関数showのフレームと(変数outputのために)関数prettyprintのフレームにもアクセスできなければならない。

ブロック構造を実現する幾つかの方法

    1. 関数fが呼ばれたらその関数宣言を囲むように定義されている関数のフレームへのポインタを渡すことができる。このポインタを「スタティック・リンクstatic link)」と称する。
    2. 最近に実行を開始した手続きから「関数宣言が静的に入れ子になった深さ(static nesting depth)」がiであるような関数のフレームへのポインタがi番目に収められた配列が維持できる。この配列を「ディスプレイdisplay)」と称する。
    3. 関数gが関数fを呼んでいるとき、関数fから実際にアクセスされる関数gの各変数を追加の引数として渡す。これを「ラムダ・リフティングlambda lifting)」と称する。

 

プログラム例6.3に基づいた説明

21: prettyprintshowを呼び出しprettyprint自身のフレーム・ポインタをshowのスタティック・リンクとして渡す。

10 : showはスタティック・リンク(prettyprintのフレームのアドレス)を自身のフレームに格納する。

15 : showindentを呼び出しshow自身のフレーム・ポインタをindentのスタティック・リンクとして渡す。

17 : showshowを呼び出し(自身のフレーム・ポインタではなく)自身の持っているスタティック・リンクを渡す。

12 : indentshowのフレームにある変数nを利用する。その際indentのスタティック・リンク(showのフレームを指している。)からの適当なオフセットを利用する。

13 : indentwriteを呼び出し(indent自身のでもなくshowのでもなく)prettyprintのフレーム・ポインタをwriteのスタティック・リンクとして渡す。このポインタは、自身のスタティック・リンク(showのフレームを指している)を手繰ってそこのスタティック・リンク(prettyprintのフレームを指している)として得る。

14 : indentprettyprintのフレームにある変数outputを利用する。その際indentのスタティック・リンク(showのフレームを指している。)を手繰ってそこのスタティック・リンク(prettyprintのフレームを指している)からの適当なオフセットを利用する。

 

 

6.2: Frames in the Tiger compiler

Tigerコンパイラにおけるスタックフレームの抽象化)p.134p.136

抽象化スタック・フレームFRAMEシグネチャの利用方法

抽象化スタック・フレームFRAMEシグネチャの仕様

    1. 関数の内側で仮引数はどのように見える(レジスタ内あるいはフレーム内のどこか)か?
    2. 「見え方のズレ」を実装するためにどんな命令が生成されねばならないか?

Representation of frame descriptions

(抽象的なフレームでの仮引数の取り扱い)p.136p.137

Frameモジュールはframe型の表現に関する秘密をFrame型の利用者から守らねばならないが、内部では以下のようなデータ構造を保持している。

6.4に表されたnewFrameの動作の説明

function m(x: int, y: int)=(h(y, y); h(x, x))

m内でxが「仮引数レジスタ1」にあり、yが「仮引数レジスタ1」を介してhに渡される場合に問題が生じる。

Local variables

(抽象的なフレームでの局所変数の扱い)p.137p.138

幾つかの局所変数はフレーム内に保持され、それ以外はレジスタに保持される。

抽象化スタック・フレームFRAMEシグネチャの仕様(続き)

Calculating escapes

FindEscape:抜け出しを調べる関数)p.138p.139

抜け出しを調べるFindEscape関数構造の動作

Temporaries and labels

(抽象化された一時領域とラベル)p.139p.140

抽象化されたラベルと一時領域Tempシグネチャの仕様

Two layers of abstraction

(フレーム周りのTigerコンパイラの構成)p.140p.142

  1. Tigerコンパイラの意味解析フェーズにおいて各関数宣言についてtransDec関数が呼ばれるとtransDecTranslate.newLevel関数(引数は関数が定義されている入れ子レベルlevellabel型の関数名name、引数の「抜け出し」を示すbool値のリストformalsからなるレコード)を呼び出して新たな「入れ子レベル(nesting level)」を作る。
  2. この関数はFrame.newFrame関数を呼んで新たなフレームを作る。
  3. この時Semantは関数のため環境に保持されるFunEntryデータ構造内にこの入れ子レベルを保持する。
  4. この後、関数呼び出しに出会ったらこの入れ子レベルをTranslateに返す。FunEntryには関数のコードの開始位置を示すlabelも格納される。
  5. 入れ子レベルlevSemantが局所変数の宣言を処理するとき、Translate.allocLocal関数にlevを渡して呼び出し、その返値である関数にbool値で「抜け出し」の有無を渡してこのレベルでの変数を生成する。生成結果の最終的な返値は抽象データ型Translate.accessである。(これはスタティック・リンクについて知っているが故にFrame.accessと同じ型ではない。)
  6. Semantは変数のため環境に保持されるVarEntryデータ構造内にこのTranslate.accessを保持する。
  7. 抽象データ型Translate.accessは変数のlevelFrame.accessの対で実装できる。だから、Translate.allocLocalFrame.allocLocalを呼び出して得たFrame.accessと共に変数のあるlevelも覚えておく。
  8. この後でその変数が式中で使用されたときSemantはこのTranslate.accessTranslateに返してTranslateが変数にアクセスするコードを生成できるようにする。(異なったレベルにある変数にアクセスするときはTranslate.access内のlevel情報を使ってスタティック・リンクを辿る。)

Managing static links

(スタティック・リンクの受け渡し)p.142p.143

 

Translate.newLevel(level_g, f, [false, false])

 

のように呼び出せる。ここでxyも「抜け出し」ていないものとする。

 

Frame.newFrame(label, [true, false, false])

 

と呼び出す。

Keeping track of levels

Semant内でのlevel値の管理)p.143

PROGRAM: Frames

(この章に関連するサンプルソースについて)p.143p.144

10/01現在http://www.cs.princeton.edu/~appel/modern/ml/chap6/のディレクトリは空だった。)

意味解析モジュールSemant (semant.sml):局所変数に場所を割り当て、入れ子の深さを追跡する。簡単にするためには全ての引数が「抜け出し」ていると仮定する。

変換モジュールTranslate (translate.sml)

抽象フレーム配置FRAMEに対する具象フレーム配置モジュールSparcFrame (sparcframe.sml)Sparcアーキテクチャ用フレーム配置を表している。機種依存性はこの中に閉じ込めること。簡単にするためには全ての引数が「抜け出し」ていると仮定する。NewFrameではescapeとしていつもtrueを渡す。(MIPSSparcなど)RISC計算機で動かすには最初のk個の引数をレジスタに保持し、残りをスタックに置く。簡単にするためにはkより少ない数の引数だけを扱う。ちなみにMipsFrameMIPSアーキテクチャ用フレーム配置を表している。

選択:「抜け出し」検出関数モジュールFindEscape:これが全ての変数の「抜け出し」フィールドを適切に設定するよう実装する。

選択:関数がk個以上の関数を扱えるようにする。サポートファイルは$TIGER/Chap6にある。

一時領域とラベルのためのサポート・モジュールTemp (temp.sigtemp.sml)

FURTHER READING:

(この章の参考文献)p.144

[McCarthy1960] Lispで単一の連続的なスタック上に変数と戻り番地を積んだ。

[Naur et al. 1963] Algolで単一の連続的なスタック上に変数と戻り番地を積み、入れ子関数宣言を含むブロック構造を導入した。

[Leonard 1987] 殆どのプログラムが変数をメモリに置き「抜け出し」について心配していなかった19601970年代。その終り1978年にプログラム・カウンタ、フレーム・ポインタ、引数へのポインタ、引数の数と全ての引数、そして呼び出し先退避レジスタを示すマスクをスタックに積む手続き呼出し命令を備えるVAX計算機を作った。

[Patterson 1985] RISC革命。

[Chitin 1982] RISC革命時、デフォルトで引数や局所変数をレジスタに置き、レジスタ割り当て器が溢れて必要が起こるまでメモリからの読みこみやメモリへの書きこみを行わないようにしてメモリとのやり取りを減らす手続き呼び出し規約のアイディアをもたらした。

[Tanenbaum 1978] 殆どの手続きは5つ以上の引数と5つ以上の局所変数を持たないことを明らかにした。

[Chow et al. 1986][Hopkins 1986] 上記の結果を発展させて以下のように最適化した呼び出し規約を設計した。最初の4つの引数をレジスタで、稀なそれ以上の引数をスタックで渡すこと。局所変数についてコンパイラが呼び出し元退避と呼び出し先退避のレジスタを使い分けること。末端手続きが呼び出し元退避のレジスタで演算し自身のスタック・フレームが不要なことさえあること。戻り番地さえスタックに積まなくていい場合があること。

EXERCISES:

(この章の練習問題)p.144p.147

6.1:

省略

6.2:

省略

6.3:

スタックに置かねばならぬもの。

その他は極力レジスタに置く。abはアーキテクチャによってはレジスタにも置く。

6.4:

省略

6.5:

 

6.6:

 

6.7:

a

フレームの参照関係は indent -> show -> prettyprintであるので読みこむ命令3個とオフセットの足し算1個。

b

Displayから必要なフレームへのポインタを取り出してアクセスするだけだからレジスタにディスプレイへのポインタがあった場合、読みこむ命令2個とオフセットの足し算1個。

c

読みこむ命令 d_2 - d_1 + 1 個とオフセットの足し算1個。

d

深さの差に関わらず、レジスタにディスプレイへのポインタがあった場合、読みこむ命令2個とオフセットの足し算1個。

e

自分より一つ深いところの関数を呼ぶ場合は、書き出し命令1個。戻るときには何もしない。

自分自身を呼ぶときには読みこみ命令1個と書き出し命令1個。

自分より浅いところの関数を呼ぶ場合は読みこみ命令d_2 - d_1個と書き出し命令1個。戻るときは何もしない。

f

呼ばれた関数がD(i)をスタックフレームに保存し、フレーム・ポインタをD(i)に書きこみ、

終了後D(i)に書き戻すのに読みこみ2つと書き出し3つ。

D(i)のアドレス計算とスタック・フレーム内退避場所のアドレス計算のために足し算が二つ。後者が不用ならば足し算は一つ。