[パタヘネ:読書メモ]2章 命令:コンピュータの言葉 その4

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以降だけでも解いておくと良いかもしれない。

4822284786
コンピュータの構成と設計(上)

関連記事

コメントを残す

メールアドレスが公開されることはありません。