さて、Mops/Forthでは無視することのできない、スタックの説明を試みます。
スタックという枠組み
スタックというのは、メモリーの使い方の一種です。後入れ先出し(Last In First Out: LIFO)記憶ともいわれます。伸び縮みしますから、まず予め余裕を持って必要な分だけメモリー領域を確保しておきます。ここでは、スタック領域と呼んでおきましょう。
スタック領域は、1セルを1項目としたリストと考えます。リストというのはアレイみたいなものですが、全体の大きさが不定で、順番で値を特定しないってことですかね(厳密な概念の違いは知りません)。つまり、均等幅のアイテム(項目)をどんどん付け足して行ったり、取り除いて行ったりできるわけです。
スタックに特徴的なのは、アイテムの追加と取り出し方の規則です。これは、机の上に平積みされた本のような感じです。新しい本(アイテム)は、どんどん上に積んで行きます。取り出すときは、上から取って行きます。つまり、最近積んだものから順に使うのがスタックの特徴です。
具体的なやり方としては、二つのポインタを保持しておきます。つまり、スタックの"底"(Bottom of stack)を指しているポインタと、一番最近積み込まれたアイテムの天井(Top of stack)を指しているポインタです。
|
スタック |
Top of Stack → |
トップアイテム |
|
第二アイテム |
|
... |
|
... |
Bottom of Stack→ |
底アイテム |
この二つのポインタの差をセルバイト幅で割れば、現在いくつのアイテムがあるのかが算出できるわけです。値の出し入れも簡単で、新しい値を積み込むには、top of stackのポインタの場所に1セルずつ格納して行けばよく、同時に、top of stackのポインタを積み込んだアイテム個数分のセルバイトだけ、bottom of stackのポインタから遠ざけておけば良いわけです。Mopsでは整数の1セルバイトというのは4バイトですが、小数の方は2倍の8バイトが1セルバイトになります。逆に値を取り出すときには、top of stackのポイントするところからスタックの下方に向けて必要なアイテムを取り出せば良いわけで、その後には、もちろん、top of stackのポインタを取り出したアイテム個数×セルバイト分top of stackのポインタをbottom of stackポインタに近づけておけばいいのです。まあ、いってしまえば、実質top of stackのポインタの操作だけでリストが管理できちゃうわけです。
初めに触れたように、スタックは、そのために使うメモリー領域を予め確保しておきます。どんどんアイテムを詰め込んで行って、予め確保した領域がいっぱいいっぱいになってしまうと、もう新しいアイテムを追加しようとしてもできません。この状態をスタックオーバーフロー(溢れ出しという感じですかね)といいます。逆に、スタックがもう空っぽなのにアイテムを取り出そうとしても、もう取り出すことはできません。この状態は、スタックアンダーフローといいます。Mopsをいじっていて失敗すると、よくスタックアンダーフローエラーには出くわします(^^;;)。オーバーフローしちゃうひとは、つわものですね(^^;;;)。
データスタック
このような機構によって、値を必要に応じて上に積んで行き、使うときには上から使うというMopsの
データスタックの仕組みが実現されているのです。データスタックは、Mopsのプログラミング中にも、プログラムが実際に走っているときも、常にそこに存在しています。そういう意味では、別のワード間を媒介する、一種の大域変数としての機能を持たせることもできます。しかし、他方で、使った値は廃棄するという独特の処理法によって、スタックは、局所変数としての性質も強く持つことになるのです。特に、Mopsのワードは、スタックに値を残すことによって、何個でも出力を持つことができます。C言語の関数のように高々一つしか値を返すことができないというような制限はありません。そして、スタックに返された値を直接次のワードの引数にすることができるわけで、名前付き変数の個数を節約できます。言語によっては、変数であることを示すための符牒を利用して、変数の宣言を省くことができるようにしているものがありますが、C言語系では、利用する変数は全て型とともに宣言しておかなければならず、変数宣言の長いリストが見られることもしばしばです。これが省略できるというだけでも、プログラム自体をかなり短くできます。
出力2 ←ワード1の出力
出力1 ←┘
↓
入力1=出力2 →(入力) ワード2
出力1
↓
出力1(ワード2) ← ワード2
出力1(ワード1)
もちろん、弱点もあります。変数に名前がないということは、プログラマが常に、データスタックに置かれている値を意識していないといけないということを意味します。また、スタックアイテムは上から順に使われますが、いつも初めから適切な順番でスタックに値を積み込めるとは限りません。それに、スタックアイテムは一回使われれば廃棄されてしまいますが、同じ値を2度3度と使わなければならないこともあります。そういった場合のために、スタックアイテムをコピーしたり、順番を入れ替えたりするためのワードが準備されています。しかし、名前のない変数を、コピーし、順番を入れ替え...としている間に、現時点でのスタックの状態はどうなっているのかを見失ってしまう危険が大きいのです。そのため、スタックにはあまり値を貯めないように、一つのワードの働きを細かく細かく区切っていこうという指向が出てきます。これ自体は良いことなのですが、他の言語のライブラリ関数を呼ぶ場合には、こういった指向ではつくられていませんから、どうしてもたくさんのパラメターをデータスタックに積んで順番を入れ替えて、という操作が必要になる場合があります。Mopsではその補助手段として、非常に実行効率のいい名前付き局所変数を準備しています。
なお、Mopsのデフォルト(初期状態)で利用できるデータスタックは、整数データスタックのみとなっています。通常の整数やアドレスなどが格納できますが、小数については別のスタックが準備されています。小数のためのスタックもまたワードの入力や出力の受け渡しに用いられるという意味では、データスタックに属します。
リターンスタック
Mops内部でスタックの使われているのはデータスタックだけではありません。もう一つ、リターンスタックというものがあります。このスタックは、ワードの呼び出しが入れ子になっている場合、つまり、ワードが別のワードを呼び出し、それがさらに別のワードを呼び出し.... と重畳的な呼び出しがある場合に、呼び出されたワードが担当する処理が終わったら戻るべきコードのアドレス(リターンアドレス)を保管しておくために使われます。このタイプのスタックは、ほとんど、言語を問わず使われています。C言語でもスタックが使われているのですが、それはこの局面で働いています。理由は、単純で効率が良いからです。ただし、Mopsではプログラミングで直接リターンスタックを操作することができますが、C言語ではそれはできません。Cコンパイラの専権事項となっており、コンパイラが全て自動的に調整してくれます。もちろん、Mopsでもリターンアドレス(戻る地点)の格納や取り出しは自動でやってくれます。リターンスタックを臨時のメモリーとして利用できるようになっていて、直接さわることもできるということです。
リターンスタックの原型
|
ジャンプ |
|
ジャンプ |
|
|
ワード0 |
→ |
ワード1 |
→ |
ワード2 |
....... |
|
|
|
← |
リターンアドレス2 |
|
|
← |
リターンアドレス1 |
|
リターンアドレス1 |
|
次のワードにジャンプするとき、終わったら戻ってくるべき箇所のアドレスをリターンスタックに積みます(リターンアドレス)。ジャンプ先のワードの処理が終わったら、リターンスタックのトップのアドレスの値を使って戻れば、ちょうど直前に呼び出されたところに戻れるという仕組み。青色がリターンスタックのアイテム。
もっとも、Mopsのリターンスタックは、純粋にスタックとして利用されているわけではありません。特定のワード内でのみ有効な
テンポラリオブジェクトは、リターンスタックの領域を利用しています。また、局所変数に使うレジスタの値を一時的に保管しておくのにも使います。細かい話ですが、ともかく、リターンスタックは、単純に次に戻るべきコードのアドレスを積み重ねて行くようにはなっていないので、Mopsが自動的に積み込んだ値を、プログラマが勝手に抜き出したり、引っ掻き回したりするのは危険です。せいぜい、コピーして調べるにとどめるべきです(デバッグ目的: ちなみに、リターンアドレスだけのときでも、リターンスタックは8バイト区切りになっているようです)。ただ、一つのワード定義内で使い切るなら、一時的メモリーとして利用することは可能です。Forth系では、データスタックアイテムが多くなりすぎて操作に困ったときに、リターンスタックに一時退避させるという使い方が典型的です。
ちなみに、C言語のスタックも、リターンアドレスの保管以外に、関数のパラメターや局所変数の一時保存など、色々な目的に使われています。ほとんど、データスタック兼用といえます。局所変数や、引数変数も、このスタック領域の一部に名前を付けたものです。C言語のスタックは全自動管理、かつ通常アンタッチャブルなので普通意識することはありません。Mopsと逆に、スタックアンダーフローというのはまず起こりませんが、スタックオーバーフローは起こりえます。メモリーがそもそも足りない(のに"贅沢な"プログラミングをした)という場合です。
浮動小数点数スタック--補遺
小数は詳しくいうと浮動小数点数(floating point number)といいます。Mopsでは小数は、独立の浮動小数点数スタック上に格納されます。つまり、三つ目のスタックです。これは、PowerMopsでは、ファイル"zfloating point"をロードすると顕在化し、利用可能になります。"zfloating point"は、浮動小数点数をサポートするための様々なユーティリティーが定義されています。68k Mopsでは"floating point"というファイルです。ファイルロードの方法は、Mopsのウィンドウ上に
// zfloating point
と書いて、enterキーを押します。Mopsではワードの実行にはすべて、enterキーを使います。普通のForth環境ではreturnキーを使っているものが多いですが、Mopsではreturnキーでは改行するだけです。
整数スタックと、小数スタックとは全く独立しています。整数と小数は直接には互いに交渉を持ち得ません。スタック操作に関するワードも、スタック毎に別々に定義されています。例えば、演算子(+や-など)もスタック毎です。自動型変換のようなものはありません。異種の数値間で計算するには、数値をスタック間で移動させるワードで、明示的に移動しなければなりません。整数を同じ値の小数にして浮動小数点数スタックに移動させるワードは"S>F"です。逆に、小数の小数点以下を切り捨てて整数にし、整数スタックに移動するワードは"F>S"です。
スタックの機構としては、整数データスタックと同じです。ただ、PowerPCの浮動小数点数は8バイトを要する形式をもちいているので、スタックセルは8バイト、と整数スタックの2倍の大きさがあります。
最終更新:2019年01月02日 17:13