パタヘネ2章末までです。今回もひたすらMIPS命令セットの仕様を学んでいきます。具体例や簡単な話は飛ばしていきます。
2.9
コンピュータでは文字列も数字で表される。ASCIIでは8bitで1文字を表す。また、8bit全部0の文字は文字列の終端を表す。MIPSではlb,sbが1文字をレジスタに読み込んだり書き込んだりするために利用できる。ここまでに学んだ算術命令、ジャンプ命令、lb,sbを使うとstrcpyを実装できる。
JavaではASCIIではなく16bitで1文字を表すUnicodeを使用している。
2.10
命令が32bitのため、32bitの定数を表現するためには工夫が必要で、そのためにlui命令がある。luiは16bit即値を指定したレジスタの上位16bitに入れる。その後下16bitを設定すれば、レジスタに32bitの定数を入れることができる。
また、分岐命令のアドレスの表現の方法に関して、jは32bit中26bitがジャンプ先のアドレスを指定するため用意されており、十分遠くまで飛ぶことができる。一方条件分岐ではレジスタの分ビットを消費するため、ジャンプ先指定は16bit分しか割り当てられていない。この16bitが絶対アドレスを意味すると解釈するなら、プログラム全体が16bitで収まるサイズでなければならず現実的ではない。そこで、現在位置を示すプログラムカウンタからの相対アドレスを意味すると解釈する。このアドレスの表現の仕方をPC相対アドレシングという。また、MIPSでは命令長が4byteで統一されているので、ジャンプ先は4byteの倍数になるはずである。そのため、命令のアドレス部がbyte単位のアドレスを表すと解釈するのではなく、word単位のアドレスを表すと解釈することにする。こうすることで2bit分得をする。
PC相対アドレシングの他に、即値アドレシング、レジスタアドレシング、ベース相対アドレシング、PC相対アドレシング、疑似直接アドレシングがMIPSでは用意されている。これらアドレシング形式をアドレッシングモードという。
機械語とアセンブリ言語は基本的に対応関係が有るので、機械語からアセンブリ言語に戻すこともできる。
2.11
複数のタスクが並列動作されているとき、リソース使用でタスク同士が競合状態とならないよう排他制御が必要になることがある。排他制御は不可分なメモリ読み出しと変更を行うことで実現される。ソフトウェア側で使用できる同期プリミティブを構成するためatomic exchangeという操作がよく利用される。これはメモリ上のあるアドレスを読み込み、内容が0であれば、レジスタ中の1をそのアドレスに設定する。読み込んだ内容が1なら、ロックが他のタスクに取得されており、ロック取得失敗となる。これらはアトミックに行われるので、読み込んだ後別タスクが同じアドレスに書き込んだりすることを考えなくて良い。ただ、ハードウェア的にこの操作を本当に不可分に実行するのは難しい。そこで、読み込みと書き込みの間に、別処理が割り込んだかどうかを判定することで代替する。読み込み命令と書き込み命令を2つ1組で実行し、2個の命令の間で他のプロセッサが別命令を処理したかどうかを判定する。別プロセッサが間に動いていなかったなら、不可分に2個の命令が行われたとみなせる。MIPSではこれを実現するためload linkedとstore conditionalが用意されている。load linkedはメモリの初期値が返される。次に実行されるstore conditionalは、load linked後別プロセッサが当該アドレスの内容を変更していたら、指定されたレジスタを当該アドレスに書き込んだ後、そのレジスタの値を0とする。別プロセッサが変更していなければ、当該レジスタをアドレスに書き込み、そのレジスタの値を1とする。
2.12
C言語のプログラムは、ソースコードをアセンブリ言語に翻訳するコンパイラ、アセンブリ言語のソースコードを機械語に翻訳するアセンブラ、モジュールごとにアセンブルされたオブジェクトファイルをつなげ、シンボルのアドレスを解決するリンカーによって実行ファイルとなる。実行ファイルはローダによってメモリ上に展開・実行される。
アセンブラでは、ハードウェアによって提供されていない疑似命令を提供する事がある。疑似命令は実在する命令へと展開される。また、32bitの定数をロードできるよう命令を展開したりする。これらはプログラマへの補助機能であるが、本来の役割はオブジェクトファイルを作成することである。オブジェクトファイルは、
- オブジェクトファイルヘッダー ファイルの残りサイズと位置を保持
- テキストセグメント 機械語コード
- 静的データ・セグメント プログラム実行中常に割り当てられるデータ
- リロケーション情報 プログラムをメモリにロードしたときの絶対アドレスに依存する命令後及びデータ語
- シンボル・テーブル 未解決の残りのラベル
- デバッグ情報
の6セクションからなる。
リンカは各コード・データモジュールをメモリ中に置く。次にデータ・命令ラベルのアドレスを判定し、内部・外部参照先を解決する。アセンブラが各モジュール単位でしかバイナリの構造を把握しておらず、したがってモジュール間の相対位置がわからない。そのためリンカが各モジュールを主記憶上のどこに配置するかを決定し、絶対アドレスによる参照に、実際のロケーションを反映させる(再配置)。
ローダは実行時引数をスタックに積み、スタックポインタに空きロケーションを作った後、レジスタを初期化し、プログラムの開始ルーチンにジャンプする。ローダは動的リンクライブラリの参照を解決する役割も担っている。以前は最初に全部の外部ルーチンの参照を更新していた。しかし、今ではルーチン使用時にリンクされるようになっている。まず、プログラムは最初のルーチン呼び出し時にダミールーチンを呼び出す。ダミールーチンは何を呼び出したいかのIDをレジスタに入れ、動的リンク・ローダにジャンプする。動的リンクローダは当該ルーチンを見つけ、ダミールーチンのジャンプ先を当該ルーチンへ置き換える。こうすることで2度目以降はダミールーチンから直接ライブラリルーチンに飛ぶことができる。
Javaではソースコードはバイトコードという、元のソースコード構造を大部分保った中間言語に変換される。以前はバイトコードをインタプリタが読み込んで実行していたが、実行速度改善の為現在はJITコンパイラで実行時にコンパイルされるようになっている。更にコンパイルされた部分は保存され次回実行時に利用される。そのため実行のたびに動作が高速になる。
2.13~2.15
C言語のMIPS命令セットへのコンパイル結果具体例。
2.16~2.19
ARMv7はMIPSから見ると不合理な命令が多かったがv8でMIPSと類似するようになった。RISC-VもMIPSと類似している。x86は互換性維持のため無秩序に命令を増やしていき、非常に複雑となっている。また、命令長も複数存在し、ややこしい。
2.20
C言語でPythonの行列積プログラムを改善すると、コンパイル最適化レベル0でも77倍、最高レベルの最適化を行うと212倍高速化できる。
2.21
メモリ間データ転送を繰り返すとき、x86のプリフィックスによる繰り返し機能より、データをレジスタに読み込み、次にメモリに内容をストアすることを繰り返すほうが高速になる。以上より、命令を強力にしても性能改善に寄与しない事がある。
現在のコンパイラは賢いため、手でアセンブリ言語のプログラムを組んでもC言語で書いたプログラムに勝てない事が多い。また、アセンブリ言語を直接書くと保守も難しい。
バイナリ互換性は商業的に重要なので、成功を収めた命令セットは変化しない、とおもいきやx86は互換性を保ちつつ平均で月1個命令が増えている。
バイト・アドレシング方式を用いるマシン上で、wordのアドレスは1byteずつ増える訳では無い(4byteずつふえる)。
自動変数へのポインタはスタック上の領域を指し示すので、呼び出し元手続きで使うと無意味な値が入っている。
コメント