2.10 32ビットの即値およびアドレスに対するMIPSのアドレシング方式
MIPSの命令長は32bitなので、32bit値をレジスタにセットしたり、32bitアドレス空間の任意の場所にジャンプさせるには工夫が必要。理由は、32bitの中にopcodeやオペランドを指定する場所が必要なので、値の指定には16bitとかしか使えないから。
というわけで、レジスタへの値セットと、ジャンプ命令で指定するアドレスの指定方法を考える。
レジスタに,32bitの値をセットする
まず、値のセット方法について。これは、lui命令(load upper immediate) + ori命令(or immediate)を使用し、2回に分けてセットすればよい。
luiは、指定された16bit即値を、レジスタの上半分にセットする命令。
lui $s0, 0xffff # 17~32bit目に値をセット ori $s0, $s0, 0xff00 # or命令で下位ビット側に値をセット # $so = 0xffffff00になる。 |
機械語的には2命令必要だが、アセンブラの表記上は32bit分の定数値セットを、1命令で表記できるようにされている場合がある。
(アセンブラのプログラムが自動で変換を行う。このような命令を擬似命令と呼ぶ)
任意のアドレスにジャンプさせる
まず前提として…
汎用のジャンプ命令(j命令)ではジャンプ先アドレスに26bit、bneなどの条件付ジャンプでは16bitしか指定できない。そうすると、この命令では大きな番地(0xffffffffとか)にジャンプさせる事が出来ない。
また良い話として、ジャンプしたいときというのはループ制御がほとんどなので、ジャンプしたい先のアドレスは、今の場所の近くにある事が多い。ただ、ジャンプには他に関数呼び出しもあって(jal),こっちは残念ながら遠くのアドレスに飛ぶ事も多い。
あと、そもそもの話としてMIPSの命令長は4byteなので、1byte単位のジャンプに意味が無い。なので、指定値x4をジャンプ先アドレスとみなせば、より遠くまで飛ぶ事が出来る。
MIPSのジャンプ命令はpcの下位28bitのみしか指定できず、上位4ビットは変更されない(但し、jump register命令を使えば回避可能)。
調べてみると、MicrosoftのMIPSアセンブラには、以下のエラーメッセージがある。
Byte address of jump target must fit in 28 bits (ジャンプ先のバイト アドレスは 28 ビットに収める必要があります。) |
MIPS エラー メッセージ C2526 ~ C2610より
※”28″という制約は、後述の”擬似直接アドレッシング”を参照のこと。
上記のような制限等を踏まえたうえで、MIPSのアドレス指定では以下の方法を用意している。
このようなアドレス指定方法のことをアドレッシングモードという。
MIPSで行う事が出来るアドレッシングモードの一覧は以下の通り
即値アドレッシング 指定された値を、ジャンプ先アドレスとみなす j命令など レジスタアドレッシング 指定されたレジスタに入っている値を、ジャンプ先アドレスとみなす jr命令 ベース相対アドレッシング(又はディスプレースメントアドレッシング) まず、指定されたレジスタ値 + 即値を求める。 求めた値の番地にあるメモリ上の値を、ジャンプ先アドレスとみなす。 PC相対アドレッシング 現在のPCの値 + 4 + 指定された即値を求める。 求めた値の番地にあるメモリ上の値を、ジャンプ先アドレスとみなす。 ※4足すのは、"ハードウェア的に"pcが既に次命令の場所を指すようになってるから 擬似直接アドレッシング 命令で指定された26bitを2bitシフトさせる。(4byte単位の指定) PCの上位4bitと、シフト値(28bit)を連結させた値を求める。 求めた値の番地にあるメモリ上の値を、ジャンプ先アドレスとみなす。 ※命令側で28bitしか指定できないのは、命令部が6bit必要だから。 |
というのが、原則だが64bitのCPUの場合は、そこからさらに拡張された方式がある。
2.11 並列処理と命令:同期
マルチスレッドのプログラムなんかを書く場合、各スレッド間で資源(メモリなど)のアクセスに同期が必要な場合がある。
同期というのは、複数の人(PG)が同時にアクセスするのではなく、順番に処理をしていく事。
これが出来てないと競合状態(race condition)に陥る。
同期処理はソフトウェアの仕事だけど、それを実現するためにハードの機能(ロック、アンロック処理等)を利用する。
これを利用する事で、相互排除(mutual exclusion, mutex)の状況を作る事が出来る。
相互排除というのは”電車の単線区間に1台の車両しか入れない状況”とかが、よく例として挙げられるもので、”特定の時点において、ある資源(この場合は単線区間)に1つだけの装置(電車)が占有できる”という事を意味する。
これを実現するためには、ハードウェア的に保証されているアトミックな操作が必要となる。
同期を取るための代表的なものに、atomic exchange(atomic swapともいう)というものがあり、メモリとレジスタの値を交換する動作を指す。これはソフト屋からすると簡単そうに思えるけど、”マルチCPU環境や、割り込み処理、マルチタスクのコンテキストスイッチ”等、考慮すべき事象が多いのでハードウェア的には意外と実装が難しい。
それでは、どうするかというと、MIPSでは、2つペアになっている命令を利用する。1つ目の命令で行った結果が成功したかを、2つ目の命令の結果でチェックする事で、ロックが取れたかを確認する流れになる。
ll命令(load linked) 指定されたメモリアドレスの内容を返す sc命令(store conditional) 指定されたメモリアドレスに値を書き込むが、前回のll命令以後に値が変更されていない ときのみ書き込みが成功する。 |
Load-Link/Store-Conditionalについては、Wikipediaが詳しい
具体的には、以下の命令でメモリとレジスタのアトミックなswapが行える。
try: add $t0, $zero, $s4 # セットしたい値($s4)を$t0にセット ll $t1, 0($s1) # $s1のアドレスにあるメモリ値を$t1にコピー sc $t0, 0($s1) # $s1に$t0を書き込もうとする、値が変わってなかったら書込が成功し$t0=1になる beq $t0, $zero, try # $t0が0だったら、失敗したのでやり直し add $s4, $zero, $t1 # メモリから取得した値を、レジスタにコピー |
例では、アドレス$s1のメモリ値と、$s4レジスタの値を書き換えている。
また、上記例ではatomic swap処理の説明だが、これを応用してatomic comprare and swap(cas)や、atomic fetch and incrementも実装できる。
コンペア・アンド・スワップ – Wikipedia
Fetch-and-add – Wikipedia
2.12 プログラムの翻訳と起動
C言語で書かれたソースがコンピュータで実行されるためには、以下のステップを取る
コンパイル→アセンブル→リンク→ロード→実行 |
アセンブリ言語では、機械語に存在しない命令を,使用(記述)出来る処理系もある(Cでいうところの関数マクロみたいなもの)。
例えばレジスタのコピー命令”move $t0, $t1″というものは存在しないが、可読性向上の為に記述できるようにしている。
これは、実際はにアセンブラが上記のコードを”add, $t0, $zero, $t1″に読み替えた上で、機械語に置き換える。このような命令を擬似命令(pseudo instraction)と呼ぶ。
他の例としては、機械語上は即値のセットが16bit単位でしか出来ないが、アセンブラでは32bit分の指定が出来る様になっている等の擬似命令があったりする。
この手の擬似命令読み替えにワークのレジスタが欲しい場合があるので、MIPSはそれ用に$atレジスタを予約している。
擬似命令を使うとコードが読みやすくなるが、時にパフォーマンス低下のリスクもあるので、本当にチューニングが必要な場合は、どれが擬似命令かということまで意識する必要がある。
アセンブル処理のアウトプットは、オブジェクトファイルになる。
UNIXで使用されているELF形式のオブジェクトファイルは以下の構成をとる
(…この部分、要確認。ちょっと違うような気もする)
プログラムヘッダ 各セクションヘッダのサイズと位置を表す セクションヘッダ テキストセグメント(.text) 機械語の命令コードが入っている 静的データセグメント(.bss) グローバル変数など、プログラムの実行中に割り当てられるデータが入っている リロケーション情報(relocation infomation) 絶対アドレスに依存する情報(命令・データ)の情報 シンボル表 extern宣言された変数など、未定義のラベルを保持する デバッグ情報 各命令がソースコード上の何行目に相当する可など、デバッグ用の情報 |
※ELFファイルは、readelf -Sで情報をダンプすることが出来る。
リンカの処理では、extern宣言された、別objに定義されている未定義のラベル解決等を行う。
また、メモリにロードされたときの絶対アドレスを決定し、実行ファイルを作成する。
実行ファイルのフォーマットは通常はobjと同じだが、未解決のラベルなどが含まれてない点が異なる。
2.13 Cプログラムの包括的な例題解説
swapとsort関数のC言語と、アセンブラへの変換の説明。
(省略)
処理速度高速化の手法として関数のインライン化があるあるが、呼び出される箇所が多いとコードサイズが増えるデメリットがある。
サイズが増えると、キャッシュミス率が増え、パフォーマンスが返って落ちるるリスクがある。
2.14 配列とポインタの対比
配列操作を行う上で、配列版とポインタ版の比較
例:
同じことを行う、下記2つの処理をアセンブルした結果を比較してみる。
case1
clear1( int array[], int size ) { int i; for ( i = 0; i < size; i++ ) { array[i] = 0; } } |
case2
clear2( int *array, int size ) { int *p; for ( p = &array[0]; p < &array[size]; p++ ) { *p = 0; } } |
case1のアセンブラコード
move $t0, $zero # i = 0 loop1: sll $t1, $t0, 2 # t1 = i*4 // arrayは1要素4byteなので、4倍する add $t2, $a0, $t1 # t2 = &array[0] + t1 sw $zero, 0($t2) # array[i] = 0 // ループ内の処理 addi $t0, $t0, 1 # i++; slt $t3, $t0, $a1 # t3 = (i<a1)?1:0; bne $t3, $zero, loop1 # if ( t3 == 0 ) { goto loop1; } |
case2のコード
move $t0, $a0 # t0 = &array[0]; sll $t1, $a1, 2 # t1 = size * 4 add $t2, $a0, $t1 # t2 = &array[0]; loop2: sw $zero, 0($t0) # *t0 = 0; // ループ内の処理 addi $t0, $t0, 4 # t0+=4; // 処理対象のアドレスを加算 slt $t3, $t0, $t2 # t3 = (t0<t2)?1:0; bne $t3, $zero, loop2 # if ( t3 == 0 ){ goto loop2; } |
case1の方がループ内の命令数が多い事が確認できる。
(sllによる掛け算が毎回走っている)これは、毎回a0で示されるベースアドレスからのオフセットを再計算する必要がある為。
但し、最近のコンパイラは最適化処理が賢いので、より見やすい配列方式で書く場合も多い。
2.15 高度な話題:CのコンパイルおよびインタープリタによるJavaの実行(◎CDコンテンツ)
省略2.16 実例:ARMの命令
特にコメント無し2.17 実例:x86の命令
80386のレジスタ回りの汎用性の無さとかは、激しく分かり辛い(知ってたけど)最近はもう少しましになったっぽい…?
x64は、互換モードや、レガシーモードなどの後方互換性を考慮したものがある。
レガシーモードの中には、さらにリアルモード、仮想x86モード、プロテクトモードとかあるので、見る気がうせる。
x64の3つの動作モードを知る (1/2) – ITmedia エンタープライズ
2.18 誤信と落とし穴
1章に引き続き、良くある勘違いの説明。誤信:命令を強力にすれば性能が改善される。
シンプルな命令を素早く実行した方が、効率が良い場合もある(?) |
落とし穴:最高の性能を得るためにアセンブラでコーディングする
最近のCPUはパイプラインやキャッシュがあるが、コンパイラ任せにしたほうがその辺の最適化が上手いので、 下手にアセンブラで書くよりCで書いた方が良いことも多い。 |
誤信:商業的理由によりバイナリ互換をとる場合、CPUの命令セットは変化しない
下位互換は取られるかもしれないが、新しい命令はどんどん追加されていく。 便利さの拡大という意味もあるし、競合他社が互換品を作りづらくなるという側面もある。 |
落とし穴:バイトアドレッシング方式をとるCPUでは、ワードのアドレスは1づつ増えるとは限らない
MIPSでは命令長が32bitなので、アドレスで指定した値 * 4単位でジャンプする。 確かに勘違いしそう... |
落とし穴:自動変数のポインタを、スコープが抜けた後で使ってはいけない
Cプログラマにとっては常識レベル?? |
2.19 おわりに
4つの格言が紹介されている
単純性は規則性につながる 小さければ小さいほど、高速になる 一般的な場合を高速化する 優れた設計には妥協が必要 |
基本的にはsimple is bestな考え方。
MIPS贔屓な内容かもしれないが、x86の命令セットを見た後なら納得できる。
また、ハードウェア設計に限らず、アプリの設計にも言える考え方。
2.20 歴史展望と参考文献(◎CDコンテンツ)
省略2.21 演習問題
最後まで読みきってしまいたいので、スキップする。後で余裕が有れば、2.12以降だけでも解いておくと良いかもしれない。

コンピュータの構成と設計(上)
関連記事
コメントを残す