[PIC]PIC16F84Aのコンフィグレーションビットを指定する

PICでは、マイコンの内部動作を規定するために、コンフィグレーション用のデータが存在します。

PIC16F84Aマイコンの場合、このデータはプログラムメモリの0x2007番地に存在します。0x2007番地は、ユーザプログラムメモリの範囲外なので、通常のアセンブラ命令では値のセットを行うことができません。

それでは、この値をどうやって指定するのかという疑問が出てくるのですが、MPLABのアセンブラでは__CONFIG命令でこの値を設定することができます。


ここでは PIC16F84Aのコンフィグレーションビットの内容について確認してみます。


まず、アセンブラプログラム上での指定方法ですが、以下のような記述になります。

LIST      P=16F84A
INCLUDE   P16F84A.INC
__CONFIG  _HS_OSC & _WDT_OFF & _PWRTE_ON & _CP_OFF



3行目の__CONFIGがコンフィグレーションビットの定義で、指定したい項目を&で列挙して指定します。

何の値が指定できるかはインクルードしているP16F84A.INCに記載されています。

;==========================================================================
;
;       Configuration Bits
;
;   NAME            Address
;   CONFIG            2007h
;
;==========================================================================
 
; The following is an assignment of address values for all of the
; configuration registers for the purpose of table reads
_CONFIG          EQU  H'2007'
 
;----- CONFIG Options --------------------------------------------------
_FOSC_LP             EQU  H'3FFC'    ; LP oscillator
_LP_OSC              EQU  H'3FFC'    ; LP oscillator
_FOSC_XT             EQU  H'3FFD'    ; XT oscillator
_XT_OSC              EQU  H'3FFD'    ; XT oscillator
_FOSC_HS             EQU  H'3FFE'    ; HS oscillator
_HS_OSC              EQU  H'3FFE'    ; HS oscillator
_FOSC_EXTRC          EQU  H'3FFF'    ; RC oscillator
_RC_OSC              EQU  H'3FFF'    ; RC oscillator
 
_WDTE_OFF            EQU  H'3FFB'    ; WDT disabled
_WDT_OFF             EQU  H'3FFB'    ; WDT disabled
_WDTE_ON             EQU  H'3FFF'    ; WDT enabled
_WDT_ON              EQU  H'3FFF'    ; WDT enabled
 
_PWRTE_ON            EQU  H'3FF7'    ; Power-up Timer is enabled
_PWRTE_OFF           EQU  H'3FFF'    ; Power-up Timer is disabled
 
_CP_ON               EQU  H'000F'    ; All program memory is code protected
_CP_OFF              EQU  H'3FFF'    ; Code protection disabled



このファイルは、インストール先をデフォルトで指定した場合は、C:\Program Files\Microchip\MPASM Suite\の下あたりにあります。

大まかに、オシレータの指定、WatchDogタイマー、パワーアップタイマー、コードプロテクションの指定が可能な事が分かります。
各項目の詳細はMicrochipが提供しているデータシートを見る事で確認できます。
(下記PDFのp21あたりに記載されています。)
PIC16F84A Data Sheet




コンフィグレーションビットで指定できる内容ですが、PICの種類が変わるとこの指定項目や方法は微妙に変わる可能性があります。

たとえば、PIC16F88では、コンフィグを指定可能なメモリがが2つあり、それぞれ_CONFIG1, _CONFIG2と呼ばれています。
このため以下のような記述で、どちら側のメモリを操作するのか指定する必要があります。

__CONFIG _CONFIG1, _HS_OSC &...
__CONFIG _CONFIG2, _IESO_OSC &...



この辺は、やはり該当チップのincファイルをチェックすると記載されています

;==========================================================================
;       Configuration Bits
;   NAME            Address
;   CONFIG1           2007h
;   CONFIG2           2008h
;==========================================================================
; The following is an assignment of address values for all of the
; configuration registers for the purpose of table reads
_CONFIG1         EQU  H'2007'
_CONFIG2         EQU  H'2008'
...


[PIC]MPLABで作成されたhexファイルのフォーマットを解析する

MPLABで開発を行い、コンパイル(orアセンブル)を行うと*.hexファイルが作成されます。
PICのマイコンへは、hexファイルを元に書き込みが行われます。

このhexですが、エディタで開いてみると普通のテキストファイルである事が分かります。
前回の動作確認で作ったasmファイルを確認してみると、以下の様な内容になりました。

作成したPICのアセンブラコード

; LEDを点滅させる
; (点滅が速過ぎるので、実機で実行すると目視では確認できませんが...)
	LIST    P=16F84
	INCLUDE P16F84A.INC
	ORG 0
 
	; ポートBのRB0ピンを出力にする
	BSF   STATUS, RP0
	CLRF  TRISA
	CLRF  TRISB
	MOVLW B'00000001'
	MOVWF PORTB
	BCF   STATUS, RP0
 
LOOP
	; ポートBを全ビットOFFにする
	MOVLW B'00000000'
	MOVWF H'05'
 
	; ポートBの再下位ビットをONにする
	MOVLW B'00000001'
	MOVWF H'05'
 
	goto LOOP
	END



作成されたhexファイル

:020000040000FA
:100000008316850186010130860083120030850049
:0600100001308500062806
:04001600003400347E
:00000001FF




MPLABで見たプログラム領域の16進ダンプ(メニューバーよりView->Program Memory)


ダンプを命令単位で逆アセンブルした結果



hexファイルの中身ですがMPLABの独自フォーマットというわけではなく、インテルHEXフォーマットという良く使われる汎用の書式になっており、フラッシュメモリ上のどの位置に何のデータを書くべきかが指定されています。

フォーマットはかなりシンプルで、以下の仕様になっています。




Intel hex format仕様

ファイルはテキスト形式で、16進文字列で書き込むべきデータと、メモリ上のどこに書き込むかの絶対アドレスが記述されています。

ファイルは改行を区切り文字とした、複数のレコードで構成されています。


レコードフォーマット

各レコードは可変長の構成をとり、以下の6つのフィールドで構成されるレコードフォーマットとなっています。

フィールド名             データ長(byte)
-----------------------  --------------
スタートコード           1
データ長                 2
アドレスオフセット値     4
レコード種別             2
データ                   可変長
チェックサム             2



各フィールドの意味

各フィールドの意味は以下の通りです。

スタートコード

固定で”:”が入ります。

データ長

2桁の16進文字列で、5番目のフィールドであるデータ部の長さを指定します。
例えば”10″だった場合は、データ部が16byteである事と意味します。
データは16進文字列で表記されており1文字が4bit分の情報を持つので、データ長が”10″だった時は、エディタで見ると32文字書かれている形になります。

アドレスオフセット値

ROMやフラッシュメモリに書き込む際の開始アドレスです。
16進で4桁ということは16bitなので、ここでは最大64キロバイトまでのオフセットを指定可能です。
この項目は、必ずビッグエンディアンで記載されるので、値が”0100″だった時、オフセットは10進表記で”256″となります。

レコード種別

2桁で”00″~”05″の値が指定されます。
各値の意味は後述します。

データ

実際に書き込むべきデータです。
バイトオーダーは、実際に書き込むべきROMに依存します。
(リトルエンディアンのときもあれば、ビッグエンディアンの場合もあります)

ちなみに、MPLABではリトルエンディアンなので、エディタで確認すると順序が逆になっているように見えます。
(詳細はこちらも後で確認します)

チェックサム

前述したフィールドのうち、”データ長、アドレスオフセット値、レコード種別、データ”部の各バイトを合計した後、2の補数をとった値です。
合計値が0xFFを超える場合は、下位2byteのみを採用します。

例として、対象のデータが”020000002030″だった場合、02 + 00+ 00 + 00 + 20 + 30 = “52”となり、2の補数を取ると”AD”になります。




レコード種別

レコード種別のフィールドには”00″~”05″が入り、それぞれ以下の意味を持っています。

00

データフィールドです。


01

ファイルの最後を意味します。
最後のレコードは必ずレコード種別が01になります。
このレコードは、データ長が”00″で、データフィールドはブランクになります


02

拡張セグメントアドレスを意味するレコードです。
データフィールドにはセグメントのアドレスが、ビッグエンディアンで入ります。
値をセットする場所が16bitの範囲を超える場合にこのレコードを使用し、以降のデータは”ここで指定されたアドレスを16倍した値”+”00レコードで指定されたアドレスオフセット値”の場所をオフセット値とみなします。

ファイル中にこのレコードがない場合は、セグメントは0x0000であるとみなします。


03

開始セグメントアドレスを意味します。
80×86プロセッサ用の命令で、プログラムをどこから実行するかを指定します。

PICマイコンには関係有りません。


04

拡張リニアアドレスを意味します。
32ビットアドレス空間をフルにアクセスするために作成されたレコードです。
このレコードでは、アドレス指定のうち上位16ビット分を指定します。
以降のデータは、”ここで指定されたアドレスを16ビット左シフトした値”+”00レコードで指定されたアドレスオフセット値”をオフセット値とみなします。

PICマイコンでは、32bitもアドレス空間が無いので関係有りません。
(出力ファイルを見ると0x0000が指定されているようです)


05

開始リニアアドレスを意味します。
レコード種別=”03″のデータに対して、プログラムの開始位置を32ビットアドレス空間をフルにアクセスするために作成されたレコードです。

こちらもPICマイコンには関係有りません。




MPLABで作成されたhexファイルを確認してみる


先ほど提示したhexファイル(MPLABにて作成されたモノ)を再掲します。
このファイルを、説明したファイルフォーマットにしたがって解析していきます。

:020000040000FA
:100000008316850186010130860083120030850049
:0600100001308500062806
:04001600003400347E
:00000001FF



このままだと分かり辛いので、各フィールドをスペースで区切っています。
データフィールドは可変長ですが、PICの命令は16bit単位で1命令記載されているので、4文字単位で区切ってみます。

: 02 0000 04 0000 FA
: 10 0000 00 8316 8501 8601 0130 8600 8312 0030 8500 49
: 06 0010 00 0130 8500 0628 06
: 04 0016 00 0034 0034 7E
: 00 0000 01 FF



1行目はオフセット指定で、データは”0000″なので気にしなくてよいです。

2行目からが命令が入っている部分になります。
レコード長が”10″なので、データ長は”8316 8501 8601 0130 8600 8312 0030 8500″の16byteになります。最初のデータに注目すると”8316″となっていますが、PICはリトルエンディアンなので、機械語は”1683″となります。

MPLABで確認すると、最初の命令は確かに”1683″になっています。



以降のデータも同様に確認していくと、hexファイルのデータを読み解く事が出来るかと思います。


参考文献:
http://en.wikipedia.org/wiki/Intel_HEX
http://www.interlog.com/~speff/usefulinfo/Hexfrmt.pdf


4873112885
Binary Hacks ―ハッカー秘伝のテクニック100選

[MPLAB]作ったプログラムのデバッグ実行が出来ない場合の対処法(Debugger->Run)

MPLABでは、IDEなので当然ですが、作成したプログラムをPC上でデバッグ実行する事が出来ます。

メニューバーのDebuggerを選択すると、以下のようにF9を押す事で、デバッグ実行が可能です。



なのですが、MPLABをインストールした直後では、なぜかPC上での実行が出来ない状態になっています。
Debuggerメニューを見ても項目が2つしかありません…



というわけで、MPLABでデバッグ実行を行うための設定方法ですが、以下の作業をすればOKです。
Debugger->Select ToolにあるMPLAB SIMを選択し、チェックを入れます。


以下のようになっていればOKです。
これで、メニューバーのDebuggerに項目が増えます。



デバッグ実行は、メニューバーのほかにアイコンをクリックしても行えます。



また、これらの機能は頻繁に使うので、最低限以下のショートカットキーだけでも覚えておくと
デバッグ作業が捗ります。

F10: メイク
F9 : 実行
F7 : ステップ実行(Step Into:サブルーチンの中に入る)
F8 : ステップ実行(Step Over:サブルーチンの中には入らない)
F2 : 選択行にブレークポイントをセット



PICマイコンの基礎

[PIC]MPLABでLED点滅プログラムを作成する

前回、PIC開発用のIDEであるMPLABのインストールが出来たので、今回はソフト作成の流れを確認しつつ、LEDを点滅させるプログラムをアセンブラで作成します。

プログラムは、実際にPICに書き込まなくてもPC上でデバッグ実行できるので、今回の作業内容ではPICのICはまだ必要ありません。

また、途中でも説明していますが、今回作成のプログラムではLED点滅のプログラムを作っていますが、実は一点問題があります。それは、動作が非常に速いので実際にPIC上で動作させると速すぎ、点滅している事が目視では確認できないことです。

PC上でのデバッグ確認では支障が無いので、ここではPC上での操作方法と動作確認方法を主に確認してください(その辺のところはこの後に順次、説明・解決していく予定です…)。


それでは、今回の作業手順です。

まずは、MPLABを起動し、Project->Project Wizardを選択します。


プロジェクトの開始ダイアログが表示されるので、次へをクリックします。



実行に使用するPICの型番を選択します。今回はPIC16F84Aを選択します。
※PIC16F84Aは入手が容易で、価格も安いので学習目的には最適です。



開発言語の選択画面です。
今回はアセンブラでの動作確認を行いたいのでMPASM Toolsuiteを選択します。



プロジェクトファイルの保存先を指定します。
(今回はC:\home\project\testフォルダの下に、test01という名前のプロジェクトを作成します)



次に、既存の作成済みファイルをプロジェクトに追加するための画面が表示されます。
今回は、まっさらな状態からプログラムを作成するので、そのまま次へをクリックします。



最終確認画面です。そのまま完了をクリックします。



test01.mcwというプロジェクトのワークスペースが表示されている事を確認します。拡張子のmcwはおそらくmicrochip workspaceの略かと思います。
また、ここでワークスペースの中にプロジェクト”test01.mcp”が登録されている事が確認できます。





次に、動作確認用のプログラムを作成します。
ツールバーの新規作成アイコンをクリックすると、Untitledの新規ファイルが作成されます。



新規ファイルに,アセンブラで以下のプログラムを書きます。
(とりあえず今のところは、内容を気にせず丸々コピーしてみてください)
PICアセンブラ入門

; LEDを点滅させる
; (点滅が速過ぎるので、実機で実行すると目視では確認できませんが...)
    LIST    P=16F84
    INCLUDE P16F84A.INC
    ORG 0
 
    ; ポートBのRB0ピンを出力にする
    BSF   STATUS, RP0
    CLRF  TRISA
    CLRF  TRISB
    MOVLW B'00000001'
    MOVWF PORTB
    BCF   STATUS, RP0
 
LOOP
    ; ポートBを全ビットOFFにする
    MOVLW B'00000000'
    MOVWF H'05'
 
    ; ポートBの再下位ビットをONにする
    MOVLW B'00000001'
    MOVWF H'05'
 
    goto LOOP
    END




プログラムを作成したら、File->Save Asをクリックして、ファイルを保存します。
今回、ファイル名はtest.asmにしました。


ファイル保存後、File->Add New File to Projectをクリックし、先ほど保存したファイルをプロジェクトに登録します。





メニューよりProject->Build Allをクリックしてコンパイルを行います。



Outputウィンドウに、”BUILD SUCCEEDED”と表示される事を確認します。



メニューよりDebugger->Step Intoをクリックして、プログラムを実行します。
(DebuggerメニューにStep Intoは、こちらの作業を行ってください)



プログラムのウィンドウに緑の矢印が表示され、今実行している行が表示されます。
F7キーを押すたびに1ステップづつ、プログラムが進みます。


また、メニューよりView->File Registersをクリックすると、レジスタ一覧のウィンドウが表示されます。





F7キーをどんどん押していき、下のほうに行くとレジスタのPORTAの値が1⇔0と交互に変わることが確認できます。

 ↓ ↑ F7を押していくと値が交互に変わります



また、メニューバーの三角が2つあるアイコン(animateアイコン)をクリックすると、F7を押さなくてもどんどん進んでいってくれます。



以上で、プロジェクトの作成から、プログラムの作成、実行までの流れの確認は完了です。

自作ソーラー発電用のバッテリーを大容量に変更

前回作成した自作ソーラー発電システムですが、バッテリーが弱っていて十分に蓄電できていなかったので、今回新しいものに買い換えました。


今回購入したバッテリーは、以下の商品です。
バッテリー自体かなり重く、amazonが最安(12,500円)だったのネットで購入しました。
(画像をクリックすると、最新の価格が確認できます)
B003JETJI8
130F51(PRN) GSユアサバッテリー

他にもう少し安いもの(-500円ぐらい)も有ったのですが、マイナーブランドだったり、送料別の価格になっていて送料を含めると高くついてしまうものだったので、購入する際は注意が必要です。


で、届いた商品はこちらです。
想像したより結構大きめでした。



上のふたを開けた状態です。大きさが分かりやすいようにペンを置いています。


購入時の電圧チェック。12.7Vありました。


バッテリー以外にマニュアルも付属しています。


マニュアルはどうでも良い内容ですが、最後の一覧表は便利です。
今回買った130F51は、5時間で使い切る場合に96Aの容量があることが分かります。



今回購入したものはサイズの規格が”F”となり、今まで使っていた”B”とは異なるので端子サイズが変わってしまいます。
なので、ターミナルも別途購入します。


今回は近くのホームセンターで購入してしまいましたが、こっちもamazonで購入した方が安かったです。
B001VNTVS2
B657 バッテリーターミナル (蝶ネジ) 10Ф


配線は前回と同様、簡単に接続できました。丁度バッテリの上にチャージコントローラが載るサイズです。
バッテリーを交換して1週間ほど稼動させてますが、充電不足になることも無くなり、快適に使用できています。



だれでもできるベランダ太陽光発電


B003JETJI8
130F51(PRN) GSユアサバッテリー

[gcc]long long intの値をprintfで表示させる

long long intで64bit整数値を格納した際に、その値をprintfで表示させる方法です。
普通に%dや%xで表示させようとしても、下位32bit分しか見てくれないので正しい値を表示させる事が出来ません

プログラム

#include <stdio.h>
 
int main() {
    long long int a = (long long int)1 << 63;
 
    // NG: intとして表示
    printf( "case1: %d\n", a );
 
    // NG: 16進で表示
    printf( "case2: %x\n", a );
}



実行結果

case1: 0
case2: 0




このような場合は、%lldや%llxを使用すると、64bit分見てくれます。

#include <stdio.h>
 
int main() {
    long long int a = (long long int)1 << 63;
 
    // long long intとして表示 (印字対象データの最上位ビットが立っているので負数とみなされる)
    printf( "case3: %lld\n", a );
 
    // unsigned long long intとして表示
    printf( "case4: %llu\n", a );
 
    // 16進数で表示
    printf( "case5: 0x%llx\n", a );
}



実行結果

case3: -9223372036854775808
case4: 9223372036854775808
case5: 0x8000000000000000



実例で学ぶGCCの本格的活用法 高機能コンパイラのオプションを一つ一つていねいに解説
コンパイラ―原理・技法・ツール

PIC用IDEのMPLABをインストールする

PIC向けの統合開発環境であるMPLABのインストールを行います。
MPLABをインストールする事で、Cのコンパイラとアセンブラを,統合開発環境にて使用出来ます。

キホンからはじめるPICマイコンC言語をフリーのコンパイラで使う


まず、下記のサイトにアクセスします。


MPLAB Integrated Development Environment



ページの下のほうにスクロールするとダウンロードリンクがあるので、MPLAB IDEをクリックしダウンロードします。


ダウンロードしたzipファイルです。


zipファイルを展開すると、以下のようなファイルが出てくるので、setup.exeを実行します。



しばらく待つとセットアップ画面が表示されるので、Nextをクリックします。


ライセンスを確認後、”I accept…”を選択してNextをクリックします。


Completeを選択します。


インストール先のフォルダ選択です。Nextをクリックします。


同時にインストールするApplication Maestroと,MPLABのライセンス確認です。
ライセンスを確認し、Nextをクリックします。




※Application Maestroについては、以下のページに詳細が載っています。
Application Maestro Software



最終確認のダイアログです。
Nextをクリックします。


Completeだと、以下のコンポーネントがインストールされるようです。

Destination Directory:
C:\Program Files\Microchip\
 
Setup type selected: Complete
Components selected:
	Serial Memory Devices
	8-bit MCUs and KeeLoq devices.
	16-bit MCUs and DSCs
	32 bit MCUs
	REAL ICE CMD
	ICD 3 Cmd
	PK3CMD
	Procmd
	PM3Cmd
	Visual Procmd
	MPASM Suite
	ASM30 Suite
	MPLAB C32 Suite
	MPLAB IDE
	MPLAB PM3
	PICSTART Plus
	PRO MATE II
	MPLAB ICD 2
	MPLAB ICD 3
	MPLAB SIM
	AN 851 FLASH Bootloader
	PICkit
	PICkit2
	PICkit3
	MPLAB ICE 2000
	MPLAB ICE 4000
	MPLAB REAL ICE
	KEELOQ
	MPLAB PIC32 Starter Kit
	MPLAB Serial Memory Products Starter Kit
	MPLAB dsPIC Starter Kit
	Application Maestro
	CCS PCB C Compiler, Microchip MPLAB IDE Edition
	Target Application I/O Display
	Data Monitor and Control Interface
	AN908 ACIM Tuning Interface
	Real Time OS Viewer
	MATLAB
	PCLint
	SMPS GUI
	LCD Designer
	dsPIC Filter Designer
	mTouch GUI
	dsPIC Works Plug-in




インストールが完了すると、再起動の確認が行われます。
そのままFinishをクリックするとWindowsの再起動が行われます。


再起動後、デスクトップにプログラムのアイコンが表示されます。
また、同時にオンラインドキュメントの画面が開きます。


オンラインドキュメントは、インストール先フォルダにあるhtmlファイルを表示しているだけのようです。


また、このウィンドウは一旦閉じても、スタートメニューより再度表示させる事が出来ます。



デスクトップのアイコンをダブルクリックし、MPLAB IDEが開く事を確認します。



以上でMPLABのインストールは完了です。


4274209024
キホンからはじめるPICマイコンC言語をフリーのコンパイラで使う

[パタヘネ:読書メモ]第4章 プロセッサ その4

4.8 制御ハザード


制御ハザードというのは、分岐命令に起因するハザードの事を意味する。

これがなぜ発生するかというと、分岐条件が成立するかの判定はEXステージで比較演算を行い、その後のMEMステージまで決まらないから。一方で、分岐命令がMEMステージに来るころには、直後3命令分に対してそれぞれIM,ID,EX処理が行われている必要があるので、その分のクロックサイクルが無駄になるリスクがある。

これに関しては、その性質上完全に防ぐ事は出来ないので、どうすれば”よりましか”という考え方になる。


単純な分岐の予測


分岐結果が決まるまで後続の命令実行を止めるというのは、その待ち時間が完全に無駄になる。

なので、結果を捨てるのを承知で、分岐成立/不成立のどちらかを実行してしまった方がまだ”まし”になる。

そうすると、分岐が成立するかどうかを見込みで決める必要があるけど、不成立と予想しておくのが簡単(普通にPCをインクリメントしていくだけでよいので)。

この戦略をとった場合、見込みが外れたときの処理回路が追加で必要になる。
この回路は簡単で、IM,ID,EXステージのパイプラインレジスタを全クリアしてnop命令に上書きしてしまえばよい。nop命令に上書きは、前節でパイプラインがストールしたときにバブルとしてnopを入れる方法を既に考えているので、これと同じ事になる

※見込みの決め方は、以前確認したように、若いアドレスにジャンプする場合に限り、分岐成立とみなすという考え方もある。この戦略は、ループ処理で若いアドレスに分岐する事が多くなる事に起因する。


分岐予測ミス発生時のリカバリを早める


2つめの戦略は、仮に分岐予測が外れた場合でも、リカバリを早くすることを考える。

今までの話だと、IM,ID,EXステージのパイプラインレジスタのクリアが必要だったので、3クロックサイクル分の無駄が発生していた。これは、分岐が成立するかがMEMステージまで分からなかった事に起因している。

分岐の条件判定自体は、2つのレジスタ値が同じか等の簡単な比較演算なので、この演算処理だけを考慮するのなら高機能なALUを使うまでも無く、もっとシンプルな論理回路で対応する事も可能となる(比較がしたいだけなら、レジスタ値の各ビットのxorをとった結果に対してandを取って、0だったら同じ(分岐成立)、1だったら異なることが分かる)。
また、比較に使用する情報はIDステージでレジスタから取得できている。

というわけで、IDステージに分岐条件チェック用のスペシャル回路を追加してあげれば、仮に分岐予測が外れてもIFステージの破棄だけで住むようになるので、無駄となるのが3サイクル->1サイクルに減らす事が出来る。


ただ、この設計方針の転換は結構でかいので、以下のような配慮が必要となってくる。

1.今までALUでフォワーディングしてたのに加えて、ここでの演算結果のフォワーディングまで考えて
回路を組む必要がある(MUXの入力が増える)
 
2.比較に必要なレジスタはIDステージで取得できると書いたけど、これには若干のうそがある。
  というのは、その値は直前の命令のEXステージが完了しないと手に入らない場合があるから。
  なので、パイプラインストール処理の回路との整合性も配慮しておく必要がある。
 
 
3.上記2.以外に、lwなどのメモリロード系命令の結果値を使用する場合は、2クロックのストールが発生する。
  なので、こっちの配慮も必要。



というわけで、回路設計的には結構面倒になるけど、条件付分岐は頻発するし、この改善によって2クロックサイクルも無駄が省けるので、手間を掛けてまで対応する価値はある。



複雑な分岐の予測


今検討しているCPUはパイプラインが5段しかないので,前述の改善で1クロックサイクルの無駄だけで済んだけど、さらにチューニングしてパイプライン段数を増やす事になった場合は、分岐ミスのダメージが増えるリスクがある。
なので、分岐予測はもう少し考え直すだけの余地がある。

最初に単純な分岐予測として、”常に分岐が成立しないと予測する”という事を行ったけど、ここでは、もう少しがんばって分岐予測の精度を高めてみる。

ここで、C言語などのプログラムを考えてみると、分岐命令が入るのはforやwhileなどの繰り返し文に起因するものが多い。繰り返し文は、たいていの場合では複数回ループが実行されるので、各分岐命令に対して前回分岐命令の成立/非成立かを覚えておいて、次も同じ側が来ると予測できればかなり精度は高まる。


上記の手法をを、動的分岐予測(dynamic branch prediction)と呼ぶ。

動的分岐予測を実装するためには、最近実行された分岐命令に対して、以前の分岐結果を覚えておけばよい。これをもしC言語で実装するなら、ハッシュなどを使えば簡単だけど、今回は電子回路として実装しなければならないので、”シンプル、かつ、そこそこ妥当なもの”で妥協する必要がある。


というわけで考えられたのが,分岐予測バッファ(branch prediction buffer)になる。これは、分岐履歴テーブル(branch history table)とも呼ばれる。

これは、分岐命令の場所(アドレス)の下位nビットをキー(index)にして、前回の分岐結果を1bitを保存しておけるメモリで、大きさはn^2(bit)分必要となるする。
(例:下位3bitをキーにするなら、テーブルの領域はは8bit分必要となる)

これは、アドレスの下位nビットをキーとするハッシュテーブルという扱いになっている。
ハッシュだと、普通はキーのコリジョン対策が必要となるけど、今回はそこまで対応しない。

なぜかというと、利用目的がしょせん分岐予測なので、キー衝突時に本来とは異なる情報を引っ張ってきたせいで分岐予測がミスっても、そのデメリットはCPUサイクルを無駄遣いするだけ。それに対して、コリジョン対策まで論理回路で実装するとH/W設計ががとても複雑になってしまうことが理由となっている。


これで、かなり精度が上がるけど、以下のような良くある多重ループの場合、内側のループでの予測ミスが、最初と最後の2回発生してしまう。

for ( int i = 0; i < 10; i++ ) {
    for ( int j = 0; j < 10; j++ ) {
        // do something.
    }
}



これは、1回目のループの最後に分岐履歴テーブルへ分岐不成立と覚えさせたものをベースに、2回目の内側ループの初回分岐予測を行ってしまう事に起因する。
この手の処理は非常にありがちなので、対策すべきものになる。対処法としては、分岐履歴テーブルに直近1回分ではなく2回分を覚えさせておき、2回連続はずした時だけ分岐の予測を変えるように変更する。



さらに、前回分岐先のプログラムカウンタ値までキャッシュし分岐先アドレスの予測まですると、さらに高速化できる可能性がある(EXステージでの演算サイクルを省略できる可能性がある)。

その他にも応用編として、相関予測方式(局所分岐予測)や、トーナメント分岐予測方式(結合分岐予測)などもあるけど、そのあたりの詳細はWikipediaで確認してください。
分岐予測 – Wikipedia



また、条件分岐命令自体を減らす方法として、CPUの命令セットに条件付move命令を追加するという方法もある。これは、条件式が成立するときのみレジスタのコピーを行うという、良くある処理を1命令で行うもので、命令自体に分岐処理的要素を含んでいるので分岐予測自体を行う必要が無い。

これについては、MIPSでは以下の命令が用意されている。

movn $8, $11, $4     # if ( $4 != 0 ) { $8 = $11 }   // move if not zero





4.9 例外


次は、イレギュラー処理を考える。
CPUからすると、イレギュラーには以下の2つが存在する。

例外:exception   (0除算、命令異常)
割込:interrupt   (CPU外部信号線の状態変化等など)



例外と割込の呼び分けは、CPUのアーキテクチャによって異なる場合があるので、注意が必要(上記の分類はMIPSの場合)


例外の対処法は状況によって異なるので、回路設計上も複雑になりがちだし、設計方法を誤るとここが原因で性能が出なくなる危険もある。

例外がおきると、CPUは以下の処理を行う

ベクタ割り込みを使わない場合
    例外の発生元アドレスをEPCレジスタに保存する(PCはインクリメントされているので、
    厳密にはEPC=PC-4を行う)
    例外理由をCauseレジスタに保存する
 
ベクタ割り込みを使う場合
    各エラーが発生したときに実行すべき例外処理ルーチンのアドレスを事前に登録しておき、
    例外発生時は該当箇所にジャンプする。


この辺はVB6のON ERROR GOTO文に仕組みがちょっと似ている。


パイプライン化されているCPUにおいて、例外処理は制御ハザード+分岐命令と同様の処理を行う。
なので、IF,ID用のパイプラインレジスタをクリアしnop命令に差し替えた上で、PCを適切な値に変更する。

また、例外発生時は、例外処理を行った上で対象の処理を正常に行う必要がある場合が多い。これは、該当命令を再度IFステージから実行しなおせばよい(退避されたEPCの値を,PCに戻せば実装できる)

例外の発生は、5つあるステージのどこでも発生する可能性があるし、同時に複数の例外が起きる場合がある。1クロックサイクル内で複数の例外が発生する場合、MIPSでは例外の優先順にしたがって処理が行われる。

昔のCPUでは、HWのしくみにより、例外の発生元命令(PCの値)が正確にわからないものもあった。これを不正確な割り込み(imprecise interrupt)と呼び、このようなアーキテクチャの場合は、ソフトウェア側での配慮が必要になる。

また、I/O割り込みに関しては、特定命令には紐付かないので別種の考慮が必要になるが、これは別の章(下巻)で検討する。


4.10 並列処理と高度な命令レベル並列性

並列処理について、さらに深く学びたい場合は、コンピュータ・アーキテクチャが詳しい。

並列化を増やすには、2つの方法がある。

命令レベル並列性: パイプラインのステージ数を増やして、クロックサイクルをあげる
複数命令発行    : ALUなどの回路を増やす事で、1度に複数命令を実行できるようにする


後者の場合は、どの命令が同時に実行できるかのコントロールやハザード処理などが難しくなる。


命令の投機実行


CPUやコンパイラの機能として、後続の命令が、現在の命令に依存しない事を見込んで、先に後続の命令を実行してしまう機能のこと。

見込みが外れたときのリカバリや、見込み実行した命令で例外が発生したときなど、考慮すべき点は多い…


静的な複数命令の同時実行


CPU的に同時実行可能な命令郡を用意し(H/W回路として同時実行可能な組み合わせを作る)、コンパイラの配慮でが該当する組み合わせを作るよう努力する事で、1クロックサイクルで複数の命令を同時実行させる。

一部のMIPSの実装では、2命令単位で同時に実行を行う事を試みる。
命令のフェッチは64bit単位で行われ、同時実行できない組み合わせの場合は、一方をnopにさせる事で対応する。

動的な複数命令の同時実行


複数命令を動的に発行させる事が出来るCPUのことを、スーパースカラプロセッサと呼ぶ。

仕組みとしては、動的パイプラインスケジューリング(dynamic pipeline scheduling)を用いる事が多く、これは命令の実行順序をCPU側で自動で組み替えてしまう。組み換えはもちろん実行結果が変わらない範囲において行われる。
例えば、”メモリ操作系の命令実行は遅いので、それを待っている間に次の命令を処理していってしまう”などがある。



ここから先の話題は、ちょっと難しいので斜め読みで終わらしておく…(なのでメモは省略)
キーワードだけメモしときますので、興味がある方は本書を確認してください。

リザベーション ステーション
リオーダバッファ
アウトオブオーダー実行
インオーダー確定
命令レベル並列性



電源効率とパイプライン処理


投機実行等によって複数の命令を1クロックで同時実行させる事はある程度可能になるけど、一方で回路が非常に複雑になってしまうというデメリットがある。
そうすると、クロックあたりの性能を向上させる事ができても、能力あたりの消費電力が増えてしまう。(消費電力は駆動するトランジスタ数に依存するので)

電力が増えると熱の発生が増え、熱の発生は半導体の寿命に影響するため、回路の複雑化による性能向上には限界がある。


なので、最近はパイプライン処理を複雑化させるのではなく、シンプルなCPUを複数用意(マルチコア化)させることで性能を上げ、消費電力と性能のバランスを取っている。

例えばPendtium4ではパイプライン段数は31もあったけど、その後のIntel Coreでは14段に減っており、消費電力(TDP)も2割以上削減できている。


4.11 実例:AMD Opteron X4 (Barcelona)のパイプライン


AMDではx86の命令セットを内部的にRISC風の命令セットに置き換えて、CPUで実行しており、これをRISCオペレーションと呼んでいる(IntelのCPUも同様の仕組みがあって、マイクロオペレーションと呼んでいる)。

また、x86のアーキテクチャ上は16個のレジスタを持っているが、これを内部的には72個の物理レジスタに割り振りなおしている。

これはJavaで行われている、”VM上の定義された命令セットでバイナリを作成し、実行時は実CPU上の命令セットに翻訳している”機能をハードウェア的に行っているようなものっぽい。


4.12 高度な話題:パイプラインの記述およびモデリング用のハードウエア設計言語を使用したディジタル設計の概要とパイプライン処理の追加図解

省略

4.13 誤信と落とし穴



誤信:パイプライン処理は容易である。

本書をここまで読んできた時点で、その誤解は既に無いです…

パイプラインの設計思想は、設計テクノロジに関係なく実現できる

設計テクノロジによって、CPUに搭載可能なトランジスタの数(大きさに依存)や、回路駆動の遅延が変わってくる。トランジスタの性能が変わるとメモリアクセスとの速度差が顕著になってくる。

その結果、回路的に複雑な事(投機実行やスーパースカラなど)を行っても、パフォーマンス的に割が合う様になってくるので、パイプラインの設計思想は設計テクノロジに依存する。

落とし穴:命令セットの設計がパイプラインに負の効果を与える事を検討し忘れる

同じレベルのテクノロジで作成されたCPUであっても、命令セットによって性能比が倍以上変わる事例もあるらしい。


4.14 おわりに

パイプライン化を行っても、1命令あたりの実行時間(レイテンシ)は速くならない(スループットは向上する)。

パイプラインは、CPIを小さくする事を目的にしている。

暫くの間パイプライン化による性能向上を目指していたが、最近では電力・発熱がボトルネックになっている。なので、複雑なCPUを1つ作るのではなく、シンプルなCPUを複数祖結合する事で性能向上を目指している。

CPUの性能向上だけを目指しても、主記憶がボトルネックになってくるのでコンピュータ全体の性能向上には限界がある。(Amdahlの法則)


4.15 歴史展望と参考文献(◎CDコンテンツ)

省略

4.16 演習問題

省略

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

[パタヘネ:読書メモ]第4章 プロセッサ その3

4.6 データパスのパイプライン化と制御


前の節でも書いたように、MIPSでパイプライン化を検討する際は、以下のステージ分けを行う

IF: 命令のフェッチステージ
ID: 命令のデコードステージ
EX: 実行ステージ
MEM:メモリアクセスステージ
WB: 書き戻しステージ(write back to register)



パイプライン化されたCPU環境で、命令を実行する際、注意が必要なのは2点。

1.WBステージによって、レジスタの値が書き換わってしまう(EXステージの元ネタが変わってしまう)
2.次のPCの値が、EXステージが完了しないと分からない場合(分岐命令)がある



前者はデータハザード、後者は制御ハザードのリスクがあるので考慮が必要となってくる。


パイプライン化を行うと、各ステージ間でデータの受け渡しが必要となる。
この受け渡し用のバッファとして、”パイプライン・レジスタ”をCPUに追加する。

パイプラインレジスタの名前は、以下のように受け渡しされるステージ名をつけている。

IF/ID
ID/EX
EX/MEM
MEM/WB



一番最後の処理であるWBステージは、本来のレジスタファイル(or メモリ or プログラムカウンタ)を直接書き換えればよいので、後続のパイプラインレジスタは不要となる。

また、パイプラインレジスタの値は計算途中の値を保存したワーク変数的な位置づけなので、例外やパイプラインハザードが発生して演算を最初から実行しなおす場合は破棄できるような性質のもの。一方で、レジスタファイルやPCは基本的に保持しておく必要がある(一部,保存しなくて良いレジスタも歩けど…)。

というわけで、各ステージは、それぞれ同時に、原則として1クロックサイクルにおいて、以下の処理を行う

1.前段のパイプラインレジスタからデータを読んで,
2.所定の操作を行い,
3.後段のパイプラインレジスタに書き込む.





プログラムカウンタは、IF(命令フェッチ)のステージでインクリメントされる。
この値は後続の処理で使用される可能性があるので、パイプラインレジスタにも保存される。
というわけで、EXなどの後ろのステージにとっては、今処理している命令のアドレスはPCに存在しない(インクリメントされて、もっと先に進んでいるので)。
なので、パイプラインレジスタに保存された、”当時のPC値”を元に処理を行う事になる。

同様にレジスタファイルの内容は、IDステージで取得され、原則として、その値のコピーがパイプラインレジスタを経由して順に渡されていく。ここで、”原則として”というのは、前節で説明した”フォワーディングなどが発生しない場合において”という意味。

コピーするのは、レジスタ値だけでなく、”レジスタ番号”もパイプラインレジスタ経由で渡す必要がある。
これは、データハザードが発生していないかの検出に使用する。
(今の命令で演算した結果値を、次の命令で使用してないかをチェックする必要がある。詳細は後述)


また、各ステージを単純に同時実行すると、他の問題も発生してくる。
レジスタファイルを考えると、1クロック内で、WBによるデータ書き込みと、IDによるデータ読み込みが同時に発生してしまう。単一のリソース(レジスタファイル)を同時に2つの目的で使用することは出来ないので、結果として構造ハザードが発生する。この問題は次節で考える事にする。


CPU内の各構成ユニット(ALU,レジスタ,データメモリetc)は、その動作をコントロールするための制御線が必要だった。
パイプライン化に伴い、この制御線のOn/Offの管理も注意が必要になる。
制御線の情報は2番目のIDステージには全て決まるので、ここで決定した上で、制御線の情報をパイプラインレジスタに登録しておくことで対処できる。その後、後続の各ステージでは、自分が必要とする制御線の情報を使用する。後続のステージで必要な情報はそのまま次のパイプラインレジスタにスルーさせておく。



4.7 データ・ハザード:フォワーディングとストール

前節でパイプライン化の基本形が出来上がったので、次はハザード処理のイレギュラーパターンを考える。


構造ハザード


まずは、前節で判明している構造ハザードを考える。
再掲すると、構造ハザードは以下の問題だった。

また、各ステージを単純に同時実行すると、他の問題も発生してくる。
レジスタファイルを考えると、1クロック内で、WBによるデータ書き込みと、
IDによるデータ読み込みが同時に発生してしまう。
 
単一のリソース(レジスタファイル)を同時に2つの目的で使用することは出来ないので、
結果として構造ハザードが発生する。



これは、ハードウェア回路的に解消させる事が出来る(らしい…)
レジスタの回路だけちょっと工夫を行い、1クロックの前半でWBステージによるデータの書き込みを行い、後半でIDステージのデータ読み込みを行わせる。(クロックによる同期処理をさらに細分化させる)

※これはおそらく、他のステージ処理に比べ、レジスタの読み書きの方が回路的に遅延が少ないからとれる戦略なんだと思う。


データハザード その1


次にデータ・ハザードを考えてみる。データ・ハザードは下記の状況で発生する。

WBステージで書き込んだレジスタ値を、既にパイプライン実行中のIDステージが必要とする



これに対して今までは、”回路を工夫してフォワーディング(レジスタを経由せずに結果値を受け取る)という仕組みを利用する。”とだけ言及し、詳細の検討を後回しにしていた。
ここでは、フォワーディングの仕組みをもう少し具体的に考えてみる。



フォワーディングが必要な状況の中でEXステージについて厳密に考えてみる。
(EXステージ以外の場合も同じ考え方なので、そっちの方は省略する)
問題が起きるのは、後続で求めた値を前段が計算元として使用する場合なので、以下の何れか条件が満たされた場合になる。

// EXステージで必要とする元ねたを、前段のEXステージで更新した
EX/MEM.RegRd = ID/EX.RegRs
EX/MEM.RegRd = ID/EX.RegRt
 
// EXステージで必要とする元ねたを、前々段のEXステージで更新した
MEM/WB.RegRd = ID/EX.RegRs 
MEM/WB.RegRd = ID/EX.RegRt



条件は、パイプラインレジスタに登録された処理対象レジスタ番号を比較する事で判定できる。


なので、上記条件が満たされたかを比較し、条件に合致すればALUの値をMUX経由で取得するような回路を追加すればよい。この回路の事を”フォワーディングユニット”と呼ぶ事にする。

フォワーディングユニットの設置場所はEXステージになる。これは、命令の実行時にデータを先送りすべきかが決定できるから。(なので、イメージとしては後続の処理がALUからデータを引っ張るというより、EXステージがMEM,WBステージに値をPUSHする型を想像すると分かりやすい)


データハザード その2

上記で検討したデータハザード以外にも、問題が残っている。
それは、lw命令でメモリからレジスタに読み込んだ値を、直後の命令で使用する場合に発生する。

この場合、lw命令がMEMステージを実行中に次の命令はパイプライン上でEXステージを行っているが、EXステージで使用するALUの入力値はlw命令のMEMステージがおわらないと、どうがんばっても取得できない。なので、このような場合は後続の命令のEXステージを、絶対に1クロック分待たせる必要がある。(バブルの挿入)


バブルの挿入は2つの作業を行えばよい。

1つ目:
パイプラインレジスタを書き込み禁止にする作業になる。
これによって前サイクルの命令が再実行される事になる(演算の元ねたが書き換わらない)
 
2つ目:
待たせたEXステージの出力を、nop命令に強制的に置き換える。
これによって後続のステージを一回休ませる事が可能で、結果的に"バブル"が入る事になる。



これらの作業は、想像できるように電子回路的に実現可能な仕組みになる。



バブルの挿入によるハザードの解消はCPU側による防御機構だけど、当然コンパイラ側もこのような状況が起きないように命令の並び替えを可能な範囲で調整する必要がある。
調整が不十分な場合、計算結果自体はおかしくならないがパフォーマンスが落ちるという問題が発生する。

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

[パタヘネ:読書メモ]第4章 プロセッサ その2

4.4 単純な実現方式


ここでは、まずシンプルな命令だけを実行できるCPUを作る
4.1でも書いたけど、サポートする命令を再掲しておく。

メモリ参照命令  : lw(load word), sw(store word)
算術論理演算命令: add, sub, and or, slt
ジャンプ命令    : beq, j



演算を行うためにはALUが必要だけど、今回使用するALUは、以下の演算を行う事が出来るものとする。
Cプログラマからするとアセンブリ命令は非常に限られている(=表現力が乏しい)けど、アセンブリからみるとALU回路が行える計算処理は非常に限られている。

ALU制御コード 機能                演算内容
------------- ------------------  --------------------
0000          AND                 F = A & B
0001          OR                  F = A | B
0010          ADD                 F = A + B
0110          SUB                 F = A - B
0111          SLT(set less than)  F = ( A < B ) ? 1:0
1100          NOR                 F = ~ ( A | B )


参考:CS281 MIPS ALU

上記表の”ALU制御入力”というのは、機械語のopcodeとは関係なくALU装置の信号線の値を指している。信号の値はMIPSの仕様を流用している(のかな…?詳細は不明)

アセンブリの命令は、最終的にALUの上記各機能のどれかを使用して実現される。

CPUの命令を実行するためには、アセンブリ命令からALU制御信号を生成する必要がある。
この為に、制御ユニットという回路が必要。どんな回路が必要か考えるために、アセンブリ命令とALU制御信号の対応表を作ってみる。

この辺は、MIPSの命令コード表を見ながら考えると分かりやすい

アセンブリ  opcode   ALUOp 機能コード ALU機能  ALU制御コード
lw          lw       00    XXXXXX     add      0010
sw          sw       00    XXXXXX     add      0010
beq         beq      01    XXXXXX     subtract 0110
add         R形式    10    100000     add      0010
sub         R形式    10    100010     subtract 0110
and         R形式    10    100100     and      0000
or          R形式    10    100101     or       0001
slt         R形式    10    101010     slt      0111



opcodeはMIPS命令の先頭6bitで、機能コードは末尾6bitを指す。

ALUOpというのは、どのパターンの制御を行うかの指定で、以下の意味を持つ…
(この部分余り分かってない。あとで再確認する。)

00 加算
01 減算
1X 命令どおりの演算



beqのALUOpは01なので減算処理になるけど、その理由は2つの値を引いた結果が0になるかを判定しているから。
(等しければ、結果としてALUのゼロ判定フラグがアクティブになる)


命令によっては、演算を行った結果をレジスタに保存する。
R形式や、I形式の複数命令フォーマットがあるので、保存先レジスタを指定しているビット位置は異なる。

なので、レジスタファイルのユニットに対する入力は、MUXをかましてビット位置を指定できるようにしておく。


というわけでMIPSにおける”演算処理”は、ALUOpの決定と、ALUによる演算という2ステップに分けて考えられている。
これはなぜかというと、制御を複数のステップに分け、小さな制御ユニットを複数使用したほうがHW的に高速化しやすいから。


単一サイクルの設計(CPI=1)に基づいてデータパスを設計しても正しく動作させる事は出来るが、パフォーマンスが悪いので実際には行われない。なぜかというと、クロック周波数が最も時間が掛かる命令に引っ張られるから。(通常はsw,lwのメモリ操作系が最も遅くなる)



4.5 パイプライン処理の概要


パイプライン処理を行う事で、複数の処理を平行して行えるので、見かけ上の処理速度を向上させる事ができる。但し、1命令の実行に掛かる速度は変わらない。

パイプライン処理を有効に活用するためには、以下の条件が満たされていると良い

各ステージの実行時間が等しい事
ステージ数が程よく多い事



各ステージの実行時間が大きく異なると、最も遅いステージの実行時間に全体が引っ張られてしまう。
またステージ数が少ないと、あまりり並列化ができないので効率向上させにくい。一方でステージ数が多すぎると、パイプライン処理がストールしたときにダメージが大きくなるのでよろしくない。

MIPSでパイプライン化を検討する際は、以下のステージ分けを行う

IF: 命令のフェッチステージ
ID: 命令のデコードステージ
EX: 実行ステージ
MEM:メモリアクセスステージ
WB: 書き戻しステージ(write back to register)



MIPSの場合は命令長がすべて32bitだから命令のフェッチは高速化させやすい(回路が簡単になるので)、x86だと命令長が1~17byteの可変長なのでフェッチを高速化させるのが難しくなる。
なので、最近のIntelのアーキテクチャでは、x86命令を1つ以上のマイクロオペレーションに分解し、これをパイプライン化させるという手間を踏んでいる。


パイプラインのハザード


綺麗にパイプライン化できれば勿論実行効率は良くなるが、実際はうまくはいかない。
パイプライン処理が失敗する事をパイプラインハザードと呼び、以下のような要因がある。

構造ハザード
    回路設計的にパイプラインとして一緒に実行できる命令の組み合わせに制限がある。
    この制限に引っかかった場合は、パイプラインを止める必要が出てくる。
 
    ただし、コンパイラが賢ければ、構造ハザードはかなり防ぐ事ができる。
 
データハザード
    他のステップが完了するのを、別のステップが待つ必要がある場合に発生する。
    例えば、一個前の命令で操作したレジスタを次の命令で使用する場合、
    レジスタ値の取得をやり直す必要がでるので、パイプラインが止まる。
 
    ただ、この手の処理はよくあるパターンなので、構造ハザードと異なりコンパイラで
    対応する事は困難になる。
 
    なので、計算結果をレジスタからではなくALUの演算結果から直接持ってくるなどの
    バイパス処理(forwarding, またはbypassing)を行って対応する事が多い。
 
制御ハザード
    分岐命令の場合、分岐判定が決まるまで、次に実行する命令が分からない。
    なので、分岐命令の結果が決まらないと、次の命令をフェッチする事が出来ない。



データハザードに関しては、完全に防ぐ事は出来ない。
待ちが出る場合は、パイプラインの処理を一時停止させる処置を行い、これをパイプラインストールと呼ぶ。また、ここで発生する無駄な待ち(ストール)のことをバブルと呼ぶ場合もある。


制御ハザードに関しては、分岐命令の発生する頻度は結構高く、配慮が必要なので、一般的には分岐予測を行う。
簡単な方法としては、最適化しがいのある分岐はループ処理で、ループ処理中での分岐は低位アドレスに飛ぶことが多いので、低位アドレスへの条件付分岐がある場合は、ジャンプするとみなして処理を進めてしまうという手法がある。また、過去の分岐履歴の統計値を取っておいて判断材料にするという方法もあり、このような方法を取る事で最近では90%ぐらいは的中させる事ができる。

制御ハザードの対策には、前に説明した遅延分岐の処理を行う手法もあり、MIPSでは実際に採用されている。これは、分岐が発生しないとみなして分岐先の命令を仮実行してしまう。遅延分岐の処理はハードウェア的に実行されるので、機械語レベルで意識する必要は無い。



最初にも書いたけど、命令の実行をパイプライン化しても、ある1命令の実行に掛かる時間は短くならないことに注意.
(レイテンシは改善しない)

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

[パタヘネ:読書メモ]第4章 プロセッサ

4.1 はじめに

この章では、CPUの設計について考える。

だけど、いきなり本格的なCPU設計を考えると難しいので、まずはシンプルなものを設計する。
最初のバージョンで実行可能な命令は以下の9つだけをサポートする。

メモリ参照命令  : lw(load word), sw(store word)
算術論理演算命令: add, sub, and or, slt
ジャンプ命令    : beq, j



CPUの実装をしたいのだけど、CPUが最初にすることを考えると、以下の3つになる。

1.プログラムカウンタの値を取得し、メモリから命令をフェッチする
2.命令で指定されたレジスタの値を読み込む
3.ALUを使って演算を行う(ジャンプ命令以外)



各命令によって電子回路的に見た複雑さは異なるので、必要なクロック数は異なる。

CPUでは、演算処理を行うためにALUというユニットが必要となる。ALUの使用目的は命令によって異なる。

lw, sw等   : アドレスの算出
add, sub等 : 計算処理
beq等      : 値の比較



ある装置(レジスタ,ALU,pc等)には、複数の場所からデータがセットされる可能性がある。
回路上、それらの線を直結する事は出来ないのでマルチプレクサ(multiplexor)というものを使用する。
これは、制御線によってどのデータを出力に流すかを決めるデータセレクタとしての役割を持つ。


4.2 論理設計とクロック方式


MIPSを構成するユニットには、2つの種類に分けられる。

組み合わせ論理要素
    出力は、今の入力だけに依存する
 
状態論理要素
    出力は、前回の入力値に依存する



状態論理要素は、最低限入力が2つと出力を1つ持つ。
入力としてデータ値とクロック信号の2つが必要で、出力は前回書き込まれた値になる。

信号はもちろんH(1), L(0)の2種類だけど、それらをアサートされた(asserted)、ネゲートされた(deassert)と呼ぶ事もある
ここで、アサートされた状態=電圧が高い状態(bit=1)という意味ではないので注意が必要。アクティブロー回路の場合アサート状態がLOW(bit=0)となる。

CPUの命令はクロックを基準に動作する。
レジスタに対するReadとWriteは1クロックサイクル内で同時に行うことが出来る。


4.3 データパスの構築


データパスというのは、CPUを構成している論理要素で、MIPSのばあいは命令,データ用メモリ,レジスタ,ALU,加算器を指す。

ここでは、各要素を順番に組み立てていく事で最小限のCPUを構成していく



命令の順次取得サイクル


まずは、命令を順次実行する為に必要な3つの要素が必要。
(ブロック図は描くのが面倒なので省略)

命令メモリ
    実行すべき命令を管理する
    アドレスを指定して、命令を出力する
 
プログラムカウンタ(PC)
    今実行している命令の番地を管理する
    カウンタ値のセット、取得が出来る
 
加算器
    プログラムカウンタを加算する
    具体的には、今のPCの値を入力として、4加算した値を出力する。





取得した命令の演算


次に、これに演算処理部を追加する。

ALU
    命令には複数あるので、R形式命令(1or2つのレジスタの値を元に演算を行い結果を出力)だけを考える。
    R形式命令はarithmetic-logical instructionとも呼ばれ、演算はALU(arithmetic-logical unit)
    で行われる。
 
    ALUはALU命令番号、レジスタ値(複数)を入力とし、演算結果・ゼロ判定を出力する。
    ゼロ判定は演算結果が0だったときアクティブになるフラグで、分岐命令とかを行うときに使う。
 
    あと、本当はオーバーフロー制御とかもあるけど、説明が煩雑になるので、とりあえず無視して
    後で考える事にする。
 
レジスタファイル
    計算元のデータを管理するために、レジスタ値を管理する
    レジスタ番号を指定されると、レジスタ値を出力する。
 
    レジスタファイルの入力は、ちょっと多くて以下の5つになる
        読み出しレジスタ番号1
        読み出しレジスタ番号2
        書き込みレジスタ番号
        書き込みデータ
        書き込みを行うかFlg(制御線)
    出力は、読み出しデータ1,2の2つとなる
 
    というわけで、1クロックでレジスタは2つのレジスタ値を同時に読み出せる。
 
    レジスタ番号は5bit(レジスタ数が32個なので)で、データは32bit(CPUが32bitの場合)となる。




メモリのアクセス


ここまでで、レジスタだけを使用した演算処理が出来る様になった。
次は、メインメモリ(RAM)とのアクセスを考える。

CPUから考えると、メモリアクセスはlw,sw等のロード・ストア命令で実行される。
これらの命令は、アクセスするアドレスを求めるために演算が必要。
(例えば、レジスタに格納されたベースアドレスと、即値のオフセットを加算するなど)

また、即値は16bitだったりするので、32bitへの符号拡張機能も必要となる。
というわけで、追加で必要な装置は以下の3つ。

ALU
    アクセスを行うアドレスを求める装置
    これは、前述した演算処理用のALU回路をそのまま流用できる。
 
    だけど、計算の元データは、レジスタ値とは限らないので、
    命令メモリから直接データを取り込む信号線も必要。
 
 
マルチプレクサ(MUX)
    上記ALUの説明で"命令メモリから直接データを取り込む信号線も必要"と書いたけど、
    実際の回路上では、2つの信号線を直結する事は出来ない。
 
    なので、どっちからの入力をALUに入力させるかを決めるため装置としてMUXが必要。
 
    この装置の入力は複数のデータ値(レジスタ値や即値)と、どの入力を選択するかの制御信号で、
    出力は選択されたデータとなる。
 
データメモリユニット
    メモリの管理を行う
    入力として、読み出しアドレスと書き込みデータがある。
    他に、読み出し、書き込みを行うかの制御線が計2本ある。
 
    また、出力は読み出しデータになる。
 
        → 読み出しアドレスは、"読み書きアドレス"ではという気がするけど、そこが分からない?
           あと、Read/Writeの信号線が独立して存在するという事は、両方を同時に行えるという事?





分岐命令


次は分岐命令を考える。

分岐先アドレスを決定する際に、細かい事だけど以下の2点を気をつける必要がある。
(これは、CPUの制限ではなく、MIPSの命令セットアーキテクチャ上の制限)

・命令長は4byte単位なので、指定されたアドレスのおoffsetをそのまま使用できず、4倍する必要がある。
・分岐先アドレスを求める際、元にするpcは既に+4加算済みになっている(命令フェッチ時に加算してしまう)。



条件付分岐の場合は分岐が成立(branch taken)したか不成立(branch not taken)かの判定も必要

というわけで、厳密に考えると分岐命令では2つの事を行う必要がある。

分岐先アドレスの計算処理
分岐すべきかの条件判定処理



さらに、次に実行される命令が変わるということは、命令フェッチ部にも細工が必要となる。


実際のところは上記の説明よりもう少し複雑で、それは、MIPSでは遅延分岐という仕組みがあるから。
遅延分岐によって、条件分岐命令がある場合に、分岐成立の有無に関係なく分岐命令の次命令が必ず実行される事になる。
なぜそんな面倒な事をするかというと、パイプライン処理の都合(詳細は4.8で後述する)による。

ただ、beq命令は遅延がないので説明の通りになる。



データパスの作成


上記の構成要素で、最低限のCPU構成に必要な論理部品が整ったので、データパスを考える事にする。
ここで、データパスというのは、データが流れる経路。

シンプルなデータパスは、1クロックで演算処理を実行しようとする。
これによって、最短時間で処理を行えるが、一方で特定の回路は1回づつしか使えないという制約が生まれる。
もし有る回路を2回使いたい場合は、物理的に2個用意する必要がある。

但し、前述のALUのように、機能によってはMUXをかますとかする事で、同時使用せずにすむ場合もある。

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

[パタヘネ][C]シフトと加算だけで、掛け算を行う

パタヘネの”第3章:コンピュータにおける算術演算”にある、3.3乗算の補足説明です。



“乗算は、ハードウェア的には、前述した加算処理とシフト処理で実装できる”を確認するために、Cでコードを作成しました。

当然ながら、乗算命令が下記のような形でソフトウェア的に実装されているというわけではないです。

下記のような振る舞いを行う”電子回路が組まれる”という意味だけど、
回路的として説明すると分かり辛いので、説明としてコードとして記述しただけです。

#include <stdio.h>
 
main() {
    int a = 11;
    int b = 12;
//  int c = multi( a, b );
    int c = multi_by_shift( a, b );
    printf( "c=%d\n", c );
}
 
//-------------------------------------------------
// 普通に掛け算を行う
//-------------------------------------------------
int multi( int a, int b ) {
    return a * b;
}
 
//-------------------------------------------------
// 加算とシフト命令だけで、掛け算を行う
//-------------------------------------------------
int multi_by_shift( int multiplicand, int multiplier ) {
 
    // 値の初期化
    int product      = 0; // 積
 
    //--------------------------------------------
    // multiplicandを全ビット処理するまで繰り返し
    //--------------------------------------------
    int loop = 0;
    for ( loop = 0; loop < 32; loop++ ) {
        // 最下位ビットを取り出す
        int t0 = multiplicand & 0x0001;
 
        // 最下位ビットが立っていたら、乗数を足しこむ
        int t1 = (t0 == 0) ? 0 : multiplier;
        product += t1;
 
        // for debug
        printf( "loop[%02d] 0x%02x 0x%08x -> %3d %3d\n", loop, t0, multiplier, t1, product );
 
        // 被乗数をシフト(次の処理対象ビットをずらす)
        multiplicand >>= 1;
 
        // 乗数をシフト(桁上げを行う)
        multiplier   <<= 1;
    }
 
    return product;
}




基本的な考えかた上記の通りだが、この形のままでCPU設計を行うと、掛け算にかかる処理時間が非常に長くなってしまう。
なので、通常はここからさらに最適化を行う。

方法の1つは、0~31bit分シフトされた値を一気に求めて、同時に2つづつ足しこみを行う。
そうするとビット数がnの場合の計算量が O(n)から O(log n)に減少する。
(一方でH/W的には、たくさんの加算器を用意する必要があるが、最近のトランジスタは十分に小さいので無視できるレベル)



また、下記はx86環境のアセンブリ(gccで生成したもの。本当はmipsのコードが欲しかったけど、環境がないのであきらめた。
multiは”imull”命令で乗算しているけど、_multi_by_shiftでは乗算命令がなくなっている。

_multi:
        pushl   %ebp
        movl    %esp, %ebp
        movl    8(%ebp), %eax
        imull   12(%ebp), %eax
        popl    %ebp
        ret
        .section .rdata,"dr"
        .align 4
 
 
_multi_by_shift
        pushl   %ebp
        movl    %esp, %ebp
        subl    $56, %esp
        movl    $0, -12(%ebp)
        movl    $0, -16(%ebp)
        movl    $0, -16(%ebp)
        jmp     L4
L7:
        movl    8(%ebp), %eax
        andl    $1, %eax
        movl    %eax, -20(%ebp)
        cmpl    $0, -20(%ebp)
        je      L5
        movl    12(%ebp), %eax
        jmp     L6
L5:
        movl    $0, %eax
L6:
        movl    %eax, -24(%ebp)
        movl    -24(%ebp), %eax
        addl    %eax, -12(%ebp)
        movl    -12(%ebp), %eax
        movl    %eax, 20(%esp)
        movl    -24(%ebp), %eax
        movl    %eax, 16(%esp)
        movl    12(%ebp), %eax
        movl    %eax, 12(%esp)
        movl    -20(%ebp), %eax
        movl    %eax, 8(%esp)
        movl    -16(%ebp), %eax
        movl    %eax, 4(%esp)
        movl    $LC1, (%esp)
        call    _printf
        sarl    8(%ebp)
        sall    12(%ebp)
        addl    $1, -16(%ebp)
L4:
        cmpl    $31, -16(%ebp)
        jle     L7
        movl    -12(%ebp), %eax
        leave
        ret

[パタヘネ:読書メモ]第3章 コンピュータにおける算術演算

3.1 はじめに

この章で取り扱う話題の確認

実数や小数のあわら仕方(内部表現)
32bitに収まらない、大きな数の管理方法
四則演算などの計算は、ハードウェア的にどうやって実装されているのか




3.2 加算と減算


2進の加算処理の基本 -> 情報処理試験レベルなので省略


演算処理では、オーバーフローの考慮が必要。
ソフトウェアの都合で、オーバーフローの検出を行いたい場合があるので、どうやって検出すればよいかを考えてみる。


加算において2つの値の符号が異なる場合は、絶対オーバーフローしない(減算では、符号が同じ時はオーバーフローしない)。

符号が同じ場合はオーバーフローのリスクがあるが、1bit分しかあふれない。
(加算だと、たとえINT_MAX + INT_MAXでも、最大で2倍にしかならないから)



ところで、最上位ビットは符号ビットになっている。
なので、オーバーフローするという事は、”符号ビットの破壊”と同じ意味を指している。



そこまで考えると、オーバーフローの検出は以下のロジックで対応できる事が分かる。

2つの正の数を足した結果、符号ビットがマイナスになった
2つの負の数を足した結果、符号ビットがプラス  になった
	-> このような場合はオーバーフロー!!




上記の場合は、符号付き整数値の話で、符号無しの場合は、オーバーフローは一般に無視される(=検出されない)。




上記の内容を纏めると…
add,addi,sub命令ではオーバーフローがおきると例外(割り込みとも言う)が発生するようになっている。逆にaddu,addiu,subuでは例外は発生しない。

例外が発生すると例外プログラムカウンタ(exception program counter)に例外発生元アドレスが保持され、mfc0命令で値が取得可能になってる。
なので、例外処理を行った上で、処理を続行する事も、ハードウェア的には可能。

また、例外処理用に$k0, $k1という特別なレジスタがある。これは、$atのようにコンパイラが通常使用してはならないレジスタで、例外の復帰処理で使用することが出来る。



また、マルチメディア系の命令(intelのMMXなど)では、オーバーフローしたときに、例外発生ではなくMAX値をセットさせる事も出来る。
これは、ボリューム処理や、クリッピング処理等を考えると、こっちの方が使い勝手が良い場合もある為。

3.3 乗算

まずは、用語の確認

a*b = c の場合...
 
a:被乗数(multiplicand)
b:乗数(multiplier)
c;積(product)




2進での計算方法は,簡単なので説明省略
足し算と異なり、結果値が大きくなりがち(32bit * 32bitの掛け算で、最大64bitになりうる)。

MIPSでは64bitの乗算結果を保存するために2つのレジスタを割り当てており、これらをHi, Loと呼ぶ。
結果が32ビットの場合、Loの値を取得するために、mflo(move from lo)命令を使用する。(上位側はmfhi)
アセンブラでは、この辺は擬似命令を使用することで、詳細を隠蔽している。

乗算は、ハードウェア的には、前述した加算処理とシフト処理で実装できる。
説明は、ちょっと長くなったので別記事で説明してます。
→ [パタヘネ][C]シフトと加算だけで、掛け算を行う


符号処理は、まず符号無しの乗算を行った上で、最後に符号判定を行えばよい。


3.4 除算


除算は、加減算、除算より頻度は低いが計算が面倒で、0除算をチェックする必要もある。

用語の確認

a / b = c あまり d
 
a:被除数(dividend)
b:除数(divisor)
c:商(quotient)
d:剰余(remainder)



計算処理
-> ちょっと複雑そうなので後回し


除算の高速処理としてSRT法、引き放し法、引き戻し法、ノンパフォーミング除算というものがある。
lookupテーブルを使用して、商の推測をするらしい(詳細は後で調べる)


除算の処理は、積の回路を流用できる。

3.5 浮動小数点演算

この節、斜め読みだけしてスキップしました…
余り興味が持てないところで、かつこの後の内容理解には影響しなさそうなので
余裕があったら後で読むかも(でも、たぶん読まない)


3.6 並列処理とコンピュータの算術演算:結合則

順番に処理を行うものを、高速化の為に並列処理化しても、普通結果は変わらないはず。
これを結合則という。

だが、浮動小数点の場合は近似値計算なので、並列化する事によって、微小な差異が出る可能性はありうる。

3.7 実例:x86における浮動小数点演算

省略

3.8 誤信と落とし穴

誤信:左シフトが2^nの掛け算を意味するように、右シフトは割り算を意味する。

signedの値を右シフトする場合は、符号ビットの関係で合わなくなる。
論理シフトではなく算術シフトを使っても合わない。




落とし穴:MIPSのaddiu命令は、16ビットの即値フィールドを符号拡張する

add immediate unsignedなので符号ビットは無視するかと思いきや、実は符号拡張を行う。
この命令は、オーバーフローで例外を出したくない場合に使用する
 
なぜ、そんな仕様になっているかというと、subiu命令というものが存在せず、
addiuは即値にマイナスの数も指定できるというところから来ている。




誤信:浮動小数点形式の演算精度を気にするのは理論数学者だけである。

Pentiumのバグの話。


詳細はこちら -> コンピュータアーキテクチャの話 (90) Pentiumの割り算器のバグ


3.9 おわりに


この章のまとめ

1.コンピュータの計算には、桁数が有限なので精度に制限がある

2.浮動小数点は近似値である事に注意する必要がある

3.計算の並列化により、計算誤差が出る可能性がある


3.10 歴史展望と参考文献(◎CDコンテンツ)

省略

3.11 演習問題

省略

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

cygwinでgcc開発メモ

cygwin環境の確認(win7, x64)

$ uname
CYGWIN_NT-6.1-WOW64



とりあえずhello world

$ cat test01.c
#include <stdio.h>
main() {
    printf( "hello world\n" );
}




コンパイルしてexeの出力と実行
(cygwinでは、出力ファイル名を省略するとa.outではなくa.exeになる)

$ gcc test01.c -o test01.exe
$ ./test01.exe
hello world




アセンブラのコードに変換する

$ gcc -S test01.c
$ cat test01.s
    .file   "test01.c"
    .def    ___main;        .scl    2;      .type   32;     .endef
    .section .rdata,"dr"
LC0:
    .ascii "hello world\0"
    .text
.globl _main
    .def    _main;  .scl    2;      .type   32;     .endef
_main:
    pushl   %ebp
    movl    %esp, %ebp
    andl    $-16, %esp
    subl    $16, %esp
    call    ___main
    movl    $LC0, (%esp)
    call    _puts
    leave
    ret
    .def    _puts;  .scl    2;      .type   32;     .endef




objの作成(出力ファイル名を指定しないとa.outになる)

as -o test01.o test01.s



リンク

gcc -o a.exe test01.o



実行

./a.exe
hello world




objファイルのダンプ

$ od a.out
0000000 000514 000003 000000 000000 000214 000000 000010 000000
0000020 000000 000405 072056 074145 000164 000000 000000 000000
0000040 000000 000000 000000 000000 000000 000000 000000 000000
0000060 000000 000000 000000 000000 000040 060060 062056 072141
0000100 000141 000000 000000 000000 000000 000000 000000 000000
0000120 000000 000000 000000 000000 000000 000000 000000 000000
0000140 000100 140060 061056 071563 000000 000000 000000 000000
0000160 000000 000000 000000 000000 000000 000000 000000 000000
0000200 000000 000000 000000 000000 000200 140060 063056 066151
0000220 000145 000000 000000 000000 177776 000000 000547 060546
0000240 062553 000000 000000 000000 000000 000000 000000 000000
0000260 072056 074145 000164 000000 000000 000000 000001 000000
0000300 000403 000000 000000 000000 000000 000000 000000 000000
0000320 000000 000000 062056 072141 000141 000000 000000 000000
0000340 000002 000000 000403 000000 000000 000000 000000 000000
0000360 000000 000000 000000 000000 061056 071563 000000 000000
0000400 000000 000000 000003 000000 000403 000000 000000 000000
0000420 000000 000000 000000 000000 000000 000000 000004 000000
0000440




ELFデータのダンプ
cygwinで作成されるバイナリはELF形式じゃないので怒られる

$ readelf -S a.out
readelf: Error: Not an ELF file - it has the wrong magic bytes at the start




依存しているライブラリの確認

$ ldd a.exe
    ntdll.dll => /cygdrive/c/Windows/SysWOW64/ntdll.dll (0x77510000)
    kernel32.dll => /cygdrive/c/Windows/syswow64/kernel32.dll (0x74c80000)
    KERNELBASE.dll => /cygdrive/c/Windows/syswow64/KERNELBASE.dll (0x75160000)
    cygwin1.dll => /usr/bin/cygwin1.dll (0x61000000)
    ??? => ??? (0x530000)


[パタヘネ:読書メモ]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
コンピュータの構成と設計(上)

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

2.8 コンピュータ・ハードウエア内での手続きのサポート


関数の呼び出し


まずは用語の確認

caller             関数の呼び元(親側)
callee             関数の呼び出され側(子供側)
プログラムカウンタ 現在実行中の命令がある場所(address)



関数呼び出しの手続きは以下の流れで行われる。

パラメータのセット  ($a0-$a3レジスタを使用)
手続きに処理を移す
手続きに必要なメモリを確保
手続きの実行
結果のセット        ($v0-$v1レジスタを使用)
制御を元に戻す      ($raにreturn addressが保存されてる)



関数呼び出しは、アセンブラ的にはjal(jamp and link)命令を使用する
この命令で$raにリターンアドレス(プログラムカウンタに4[byte]を加えた値)をセットしてくれる

呼び出された側で呼び元にreturnする時は、”jr $ra”命令を実行する
$a0-$a3に値をセットするのは、もちろんcallerの仕事



スタックの利用


引数として必要な情報が5つ以上ある場合や、戻りの情報が3つ以上ある時は、は$a0-$a3/$v0-$v1に収まらないので、スタック領域を使用してやり取りする。
スタックポインタは$spレジスタで管理しており、大きな番地のアドレスから小さい方に伸びていく。


例題:
以下の関数をアセンブルせよ(g,h,i,jはそれぞれ$a0-$a3とする)

int func( int g, int h, int i, int j ) {
    int f;
    f = ( g + h ) - ( i + j );
    return f;
}



回答例

func: addi $sp, $sp, -12   # 3つ分のスタックエリアを確保(低い番地に伸びるので負数になる)
      sw   $t1, 8($sp)     # 各レジスタ値をスタック領域にpushする
      sw   $t2, 4($sp)
      sw   $s0, 0($sp)
      add  $t0, $a0, $a1   # t0 = g + h;
      add  $t1, $a2, $a3   # t1 = i + j;
      add  $s0, $t0, $t1   # f  = t0 - t1;
      add  $v0, $s0, $zero # v0 = s0;
      lw   $t1, 8($sp)     # 退避したレジスタ値をpopする
      lw   $t2, 4($sp)     #
      lw   $s0, 0($sp)     #
      jr   $ra             # 呼び元にreturnする



自分が使うレジスタの退避と、スタックポインタのずらし作業はcalleeの仕事となる。
関数呼び出しを行うと、上記例のようにレジスタの退避(spilling)が手間になる。MIPSでは、以下のレジスタ以外は関数呼び出しによって破壊しても良い。
ワークで使う$t?レジスタとかまでは、毎回復元しない。

$s0-$s7
$gp
$sp
$fp
$ra
自分および、自分より親側の関数が保存したスタック情報



上記のレジスタを元に戻すのはハードの仕事ではなく、呼び出された関数の仕事


2段以上の関数呼び出し


上記の説明には一部うそがある。関数の最後に”jr $ra”で呼び元に戻っているが、関数呼び出しが3段にネストすると子関数->孫関数の呼び出し時に親関数のリターンアドレスが失われてしまう。
なので、本当は$raもスタックに逃がしておく必要がある。($a0-$a3レジスタも同様)


例題:
以下のコードをアセンブルせよ(階乗の再帰処理)
nは$a0とする

int fact( int n ) {
    if (  n < 1 ) {
        return( 1 );
    } else {
        return( n * fact( n-1 ) );
    }
}



回答例

fact: addi $sp, $sp,   -8       # まずは raとa0レジスタを退避(t0は退避する必要なし)
      sw   $ra, 4($sp)          # a0レジスタを退避するのは、自分が後で使うから
      sw   $a0, 0($sp)
 
      slti $t0, $a0,   1        # 引数が1未満?
      beq  $t0, $zero, L1       # false(1以上)だったらL1にジャンプ
 
                                # n < 1の処理
      addi $v0, $zero, 1        # v0 = 1;
      addi $sp, $sp,   8        # スタックの破棄(ra,a0を書き換えてないのでlwは不要)
      jr   $ra
 
 
                                # n >= 1の処理
L1    addi $a0, $a0, -1         # n -= 1
      jal  fact                 # fact( n ); ※戻り値は$v0に入る
 
      lw   $ra, 4($sp)          # レジスタの復元
      lw   $a0, 0($sp)
      addi $sp, $sp,   8        # スタックの破棄
 
      mul  $v0, $v0, $a0        # n * fact( n-1)
 
      jr   $ra                  # 呼び元に戻る



レジスタに定数をセットするには、以下の命令を使用する
$zeroの使いどころをまた1つ見つけた。

addi  $v0, $zero, 1



L1の処理で、a0レジスタを書き換えているので、掛け算しても (n-1) * fact(n-1) になるのでは???
と思ったけど、掛け算する前にlwで元に戻すからOKという仕組みだった。

レジスタの復元は常にjrの直前にしたほうが分かりやすいように感じるんだけど,なぜそうしないんだろう??
使用レジスタを節約したいからぐらいしか思いつかない。
十分な理由にはなるけど、復元処理を途中に挟み込むとコンパイラの実装が大変そう…



Cの変数をCPUで管理する時、型と記憶クラスの解釈が重要になる。
型はその変数が占めるメモリサイズに関係する。

また、記憶クラスはstatic変数とauto変数(=ローカル変数)がある。
staticはプログラムの実行中、ずっと生きてる必要があるけど、その辺の管理を楽にする為に$gp(grobal pointer)が存在している。



再度まとめると、関数呼び出しが行われた後、保持されるレジスタは以下の通り

$s0-$s7
$gp
$sp
$fp
$ra
自分および、自分より親側の関数が保存したスタック情報


$a0-$a3は、保存されない事に注意(呼び出し先側で破壊してもかまわない)

ここで”保持される”というのは、CPUが自動的に保持してくれるという意味ではなく、”呼び出された関数側(callee)で同じ状態に戻しておく義務がある。”という意味なので注意。




配列や構造体の受け渡し


配列や構造体を引数として渡す場合は、レジスタに入りきらないので、これらもスタックに積む必要がある。これらの情報の事をスタックフレーム(stack frame)と呼ぶ(またはactivation record, activation frame,procedure frameとも呼ばれる)。

関数のコールスタックは複数の手続きフレームで構成されていて、退避した$a0-$a3,$raレジスタなどの上に積まれる。そうすると、ある関数をコールした時に、追加されるスタックの大きさは可変になる。スタックの高さは記録されていないので、スタックの底の情報を取りたいときに$spの情報だけだとアクセスできない(offsetが分からないので)。

なので、そのような場合に備えて、スタックの底のアドレスを$fp(frame pointer)レジスタに保存する”ものがある”。 ここで、”ものがある”というのは、前述までの例のように、関数自身で使用するスタックが固定なら、$fpを使わなくても定数値でspの加減算すればやり繰りすることも出来る。

※たしか,javaのバイトコードはメソッド呼び出しの際に、使用するスタックの高さは固定だったはず。
※MIPS向けCコンパイラでも、実装によって$fpの使用有無は異なる。
また、関数の引数が5つ以上ある時は$a0-$a3に収まらないので、この場合はスタックフレームを積む前にスタックに載せ,$fp側からアクセスする。



メモリ空間のセグメント分け


メモリ空間は、その使用目的ごとにセグメント分けされている

text segment        プログラムのコードが入っている領域
 
static data segment static変数(プログラムの実行中、ずっと有効なグローバル情報)
                    定数
                    ヒープ情報
                    スタック情報




2.9 人との情報交換

文字を表現するにはascii,unicodeなどのコード体系を使う。

ascii文字を処理するときはword(32bit)単位ではなく、byte(8bit)単位で処理した方が都合が良い。また、unicodeの場合はhalf word(16bit)単位にアクセスできると便利。

なので、この為のロード命令がある.

lb  (load byte)
lbu (load byte unsigned)
sb  (store byte)
lh  (load half)
lhu (load half unsigned)
sh  (store half)


使い方はlw,swと同様。


例題:
strcpy関数を自作せよ
strcpy関数は、以下の実装とする。
(dest=$a0, src=$a1, i=$s0)

void strcpy( char *dest, char *src ) {
    int i = 0;
    while ( ( dest[i] = src[i] ) != 0x00 ) {
        i++;
    }
}




回答例:

strcpy:
    addi    $sp, $sp, -4    # s0を退避
    sw      $s0, 0($sp)
 
    addi    $s0, $zero, 0   # i = 0;
 
L1:
    addi    $t1, $a1, $s0   # t2 = src[i];
    lbu     $t2, 0($t1)
 
    addi    $t3, $a0, $s0   # t3 = &dest[i];
 
    sb      $t2, 0($t3)     # *t3 = t2;
 
    beq     $t2, 0, L2:     # if ( t2 == 0x00 ) goto L2;
 
    addi    $s0, $s0, 1     # i++;
    j       L1:             # goto L1;
 
L2:
    lw      $s0, 0($sp)     # s0を復元
    addi    $sp, $sp, -4
 
    jr      $ra             # return



strcpyは1byteづつ処理を行う。

strcpyは、ここからコールされる子関数がないので、リーフ処理になる。
この場合レジスタの退避・復元の省略が出来る。


また、MIPSではスタックの値は4byte境界でアライメントが整えられる。
なので1byteのcharをパラメータとして渡す場合でも4byte消費する。

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

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

2.4 符号付き数と符号なし数


1の補数と2の補数の話とか
情報処理試験とかで学べるレベルの内容なので飛ばし読み


2.5 コンピュータ内での命令の表現

MIPSのアセンブリ言語では32のレジスタがあり、例えば以下のような割り当てになっている

$t0-$t7は、レジスタ8-15
$s0-$s7は、レジスタ16-23





MIPSアセンブリと機械語の割り当てについて

例えば

add $t0, $s1, $s2


は、以下の機械語に対応付けられる(10進表記)

0,17,18,8,0,32



MIPSの機械語は全て32bitの構成になっている。
add命令の場合、32bitの機械語は6つのフィールドに分かれている。
0,32がadd命令を意味し、17,18,8は$s1,$s2,$t0のレジスタ番号、5個目の0はadd命令では使用しないフィールドとなっている。

各フィールドはそれぞれ6,5,5,5,5,6bitの計32bitで構成されているので、2進で書くと以下のコードになる
(下記の例は、分かりやすいように各フィールド間にスペースを入れている)

000000 10001 10010 01000 00000 10000



このようなフィールド分けの事を、命令形式と呼ぶ


add命令で見た上記6つのフィールドには、以下の名前が付いている
命令によって必要なパラメータは異なるので全て使われるとは限らない。未使用の場合は0をセットする。

op rs rt rd shamt funct
 
op:オペコード。命令の内容
rs:第1ソースオペランドのレジスタ。1つ目の引数。
rt:第2ソースオペランドのレジスタ。2つ目の引数。
rd:デスティネーションオペランドのレジスタ。結果値の保存先。
shamt:シフト量(shift amount)
funct:操作命令のバリエーションをあらわす。 機能コードとも呼ぶ(funcion code)





だだし、全ての命令が上記のフォーマットを取るわけではない。

例えば、前述したlw(load word:メモリからレジスタへのロード命令)なんかだと、メモリのアドレスを5bitや6bitで表しきれない。命令長を可変にすればよいんだけど、CPUハードの設計が難しくなる。

なので、MIPSでは命令の種類によって命令フォーマットを変えている。
lwの場合は、R形式(レジスタ用)、もしくはRフォーマットと呼ばれ、以下の形になる

op(6bit)
rs(5bit)
rt(5bit)
constatntまたはaddress(16bit)



このほかにもI形式(あるいはIフォーマット)というものもあり、これは即値・データ転送命令用で使用する
詳しくは、こちら:MIPS Instruction Coding

リンク先を見ると分かるけど、R形式の場合、opは常に0でfunctで命令の内容を指定している。
I形式のopは、000000, 00001x, 0100xx以外が指定され、ここで命令の内容が決まる。




命令形式が複数あると、当然ハードウェア設計が複雑になる。
だけど、各形式をなるべく同じにしておくとHW側の複雑さは押さえられる。上記の例だと、最初の3フィールドを同じにしている。

フォーマットは、各opコードごとに決まるので、解析側からすると、まず最初の6bitを見て後続の処理を振り分けるイメージ。これは、ファイルフォーマットや、通信プロトコルとかでも良く使われる手法なのでピンとくる。



ここで例題。以下の機械語がどうなるかを考える。

a[10] = h + a[4]



まずアセンブリにすると、以下の命令になる

lw  $t0, 14($t1)
add $t0, $t0, $s1
sw  $t0, 40($t1)



これを、10進の機械語に置き換える。
lwは25,swは43のオペコードを持つので…

op:25, rs:9,  rt:8, addr:14
op:0,  rs:17, rt:8, rd:8, shamt:0, funct:32
op:43, rs:9,  rt:8, addr:40


となる。ちなみに再掲すると、8番のレジスタが$t0で, 9が$t1, 17が$s1。
lw命令はデータのロード先をrtで指定している。

さらに2進に変換すると機械語になる。
(これはすぐに分かるし、書くのが面倒なので省略)



memo:本節の自己診断にある,問題の選択肢がおかしい?
s0,s1,s2ではなくt0,t1,t2が正しいはず。


2.6 論理演算

論理演算の命令説明

srl  : 論理右シフト(shift right logical)
sll  : 論理左シフト(shift left  logical)
and  : 論理積(2つのレジスタ値の)
andi : 論理積(即値との)


他に、or, ori, norなど

常識レベルなので飛ばし読み。


2.7 条件判定用の命令


どうでも良いけど、タイトル横に有るBurks, Goldstine and von Neumannの文章訳がおかしい?
“…マシンでの取り扱い上は0を正とみなす”は、”真とみなす。”では



分岐処理



分岐(branch)系の説明(I形式の命令)

まずはbeq。これはbranch if equalの略。

beq reg1, reg2, L1



これは、以下のコードと等価

if ( reg1 == reg2 ) {
    goto L1;
}



条件式を == では無く != にしたい場合は、bne(not equal)命令になる。



例題1:
以下のコードをアセンブルせよ (i=$s3, j=$s4, f=$s0, g=$s1, h=$s2とする)

    if ( i == j ) {
        f = g + h;
    } else {
        f = g - h;
    }



回答例

      beq  $s3, $s4, Else 
      add  $s0, $s1, $s2
Else: j    Exit
      sub  $s0, $s1, $s2
Exit:


ここでいきなりラベルが登場。
ラベルが機械語でどう実装されるかは未だ説明されてないけど、意味は分かる。


例題2:
以下のコードをアセンブルせよ(i=$s3, k=$s5, save=$s6 )

while ( save[ i ] == k ) {
    i += 1;
}



回答例

Loop: sll  $t1, $s3, 2     # wk1 = i << 2;      saveの要素は4byteなので,i*4を求める為に2bitシフト
      add  $t1, $t1, $s6   # wk1 = wk1 + save;
      lw   $t0, 0($t1)     # wk0 = &(char*)wk1;
      beq  $t0, $s5, Exit  # if ( wk0 != k ) goto Exit;
      addi $s3, $s3, 1     # i++;
      j   Loop
Exit:


2,3行目は、気持ち的に”lw $t0, $t1($s6)”と書きたいところだけど、オフセットにレジスタ値は使えないので一旦addする必要がある。で、addするとロードすべきアドレスが決まるのでlw命令自体のオフセットは0でよい。


末尾(or先頭)のみに分岐命令があるコードのカタマリを基本ブロック(basic block)と呼ぶ
コンパイル作業の最初の方に行われる1段階として、基本ブロックへの分解がある。



大小比較


slt(set less than)命令では、2つのレジスタの大小比較が出来る。

slt $t0, $s3, $s4



は、以下のコードと等価になる。

if ( $s3 < $4 ) {
    $t0 = 1;
} else {
    $t0 = 0;
}



定数と比較したい場合はsltiを使用する(iはimmediate)

slti $t0, $s3, 10


レジスタ値をunsignedとみなしたい場合は、sltu/sltiuを使用する。
これは、最上位ビットが立っているときの振る舞いがu無しと異なる。

定数0と比較したい場合は$zeroレジスタを使用する。らしい。

-> これだけ読むと$zeroの存在意義が分からない...
   slti $t0, $s3, 0でよいのでは?
   パフォーマンス上の問題とかあるのだろうか?
     -> と思ったら、すぐ下で$zeroの活用場所があった...
 
   また、調べてみると$zeroは結果値が不要なときのゴミ捨て場としての役割もあるらしい。
   unixの"/dev/null"的な役割





signedの値を、あえてunsigned版(slt/slti)で比較するワザがある。
負の数は、2の補数で表現すると最上位ビットに1が立つので、見かけ上大きな数字になる。
なので、unsignedで比較しておけば、配列のオフセットが負では無いかの境界チェックも同時に行える。

具体的には、以下の感じになる。

sltu $t0, $s1, $s2                # $s1はindex, $s2にはarrayのサイズが入っているとする
beq  $t0, $zero, IndexOutOfBounds # 配列境界エラー処理へジャンプさせる





case/switch文


switch分はbeq系の羅列でも実装できるけど、ジャンプテーブル(jump table/jump address table)という仕組みが用意されている

jr命令(jump register)では、指定したレジスタに書かれている番地にプログラムカウンタを変更させる事が出来る。また、MIPSのハード上は、パフォーマンス向上のために遅延分岐の機能が実装されているが、ソフト側はその事を意識しなくて良い。

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

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

2.1 はじめに


コンピュータの言葉を、命令セットと呼ぶ(機械語/アセンブラの事)
アセンブラは、CPUが変わっても大きく違わない(方言レベル)。

命令セットの設計方針

コンパイラを実装しやすくする
性能の最大化
消費電力の最小化
シンプルである事



本書では、C言語と、アセンブラ命令の退避を行う
javaのようなOO言語の場合は、2.15節(CDROM)で別記されている


2.2 コンピュータ・ハードウエアの演算


まず、アセンブラ表記法の説明(これはMIPSのアセンブリ表記)

add a, b, c


は、Cでいうところの a = b + cと等価。


addのパラメータは必ず3つで、#以降の文字はコメント。
アセンブラでは、パラメータの事をオペランドと呼ぶ

add a, a, b   #この処理は a += b;と等価



addは2つの足し算しか出来ないので、a = b + c + d + eをする為には3命令必要

add a, b, c
add a, a, d
add a, a, e



オペランドを可変個にするとソフトウェアを書くのは楽だが、全ての命令でオペランドを固定(例だと3つ)にしてしまうと、ハードの設計が簡単になる。





p72:オペランド、アセンブリ言語の説明

オペランド

省略



レジスタは32個

$zero   常に0
$at     アセンブラに予約されている
$v0-$v1 式の評価と結果の保存用
$a0-$a3 引数(arg)
$t0-$t7 テンポラリ(呼び元によって保存される?)
$s0-$s7 テンポラリ(呼ばれた関数で保存される?)
$t8-$t9 テンポラリ(呼び元によって保存される?)
$gp
$sp     スタックポインタ
$fp     フレームポインタ
$ra     リターンアドレス



詳細は,mips_instructions.pdf


例題:f=(g+h)-(i+j)をアセンブリでどうあらわすか?
模範解答は以下の通り.

add $t0, g,   h
add $t1, i,   j
sub f,   $t0, $t1



疑問:でも、上記の場合は以下の答えもありな気がする。
こっちの方がtレジスタを使わないから良いと思うんだけど、どうなんだろう…

add f, g, h
sub f, f, i
sub f, f, j





2.3 コンピュータ・ハードウエアのオペランド


MIPSのレジスタ長は32bit
レジスタの数が少ない理由は2点

高速なストレージは高価
レジスタ数を増やすと機械語のビット数が増える(命令長が伸びる)



レジスタ番号は前述の$s0といった書き方ではなく$0~$31という数字だけの表記も理論上は可能だが、MIPSでは意味を示すアルファベット+連番の表記法になっている
以後、$s?は変数、$t?はワーク変数の意味で使う事にする。

なので、前節のアセンブラは厳密には以下が正しい。

add $t0, $s1, $s2
add $t1, $s3, $s4
sub $s0, $t0, $t1




沢山の変数や、構造体を管理したい場合はどうするか? このような場合は、メモリに情報を保存する。
メモリ->レジスタのコピーはlw(load word)命令,その逆はsw(store word)命令を使用する



例題1:
変数gに$s1, hに$s2をマッピングし, a[0]のアドレスが$s3に有るとき、以下の処理をアセンブラでどう表記されるか?

int a[100];
g = h + a[8];



回答

lw  $t0, 32($s3)   # wk0 = a[8] memo:配列1要素が4byteなので8*4=32を指定
add $s1, $s2, $t0  # g = h + wk0
int a[100];
g = h + a[8];



lw命令では、第二オペランドの括弧内にベースアドレスを表記する。
また、上記例のように各変数とレジスタ、配列とメモリアドレスの割付けはコンパイラの仕事。

メモリマッピングは、MIPSの場合はビッグエンディアン(big-endian)を採用している。




例題2:
以下のコードをアセンブルするとどうなるか?

int a[100];
a[12] = h + a[8];



回答

lw  $t0, 32($s3)   # wk0   =  a[8]
add $t0, $s2, $t0  # wk0   += h
sw  $t0, 48($s3)   # a[10] =  wk0



変数がレジスタ数より多くなる場合、一部のデータはメモリに退避させる必要がある。これをスピルアウト(spillout)と呼ぶ
メモリからディスクへのスワップアウトと(ストレージが変わっただけで)同じ意味合い。


定数の足し算はaddi(add immediate)命令を使用する
例えば a += 10は、下記のように表記できる

addi $s0, $s0, 10



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

[パタヘネ:読書メモ]1章 コンピュータの抽象化とテクノロジ その2

今日で2日目。

1.8 誤信と落とし穴


パフォーマンスの改善について、よくある勘違いの例を説明している

その1: コンピュータのある点を改善しても、その改善度に応じて全体は改善しない。




Q:
100秒掛かるPGがありその80%が乗算だったとき、乗算処理を何パーセント高速化すると、性能は5倍になるか?
 
A:
どれだけ高速化しても、全体の性能を5倍にすることは出来ない。
本文中では数式を使って説明しているけど、乗算以外で20秒掛かっているのだから、計算するまでも無く明らか。


これをAmdahlの法則という。また、経済学でいう収穫逓減の法則。


その2: コンピュータの利用率(CPU負荷)が低ければ、消費電力は少ない


ベンチマークを取って実際に計測をしてみると、CPU負荷が10%程度でも消費電力はピーク時の60%までしか落ちないという結果だった。CPU負荷と消費電力は比例させるべきという考え方もある。(が実現されてないっぽい)

    ->アセンブラのnop命令は消費電力が低いイメージがあるけど、命令毎の消費電力は仕様として
       spec sheetに明記されているのだろうか...? 
 
       あと、"イメージがある"と書いておいてアレだけど、なぜ命令によって消費電力を変えら
       れるのか,実は分かってない。
       駆動させるトランジスタの数が少ないから?



とはいうものの、感覚的に本文に記載されているイメージを持っていたので問題なさそう。


その3: 性能の尺度に、性能方程式の一部を使用すること


これは前2つと違って、タイトルから意味が分かり辛い…

例として、MIPS値(時間当たりの実行命令数)が高いコンピュータが、実際のPGを動作する上で高性能とは限らないというのが挙げられている。理由は、1命令で実現可能な”機能”の水準がモノによって異なるから。

心理学でいうハロー効果みたいなもの(ちょっと違う?)
うまく説明し辛いけど、言わんとすることは良く分かるので良しとしておきます


1.9 おわりに


将来CPUの性能がどの程度向上するかの予想は困難。

APIを統一すれば、新世代のハードウェアを既存のソフトで動作させる事は容易だけど、新たなハードのアーキテクチャは発展しづらくなる。

    -> intelの386は仮想86モードを持っていたけど、今のcore iシリーズはどの程度まで
       下位互換があるのだろうか?



wikipediaによると、まだ互換性があるらしい!!

インテル自身は過去にIntel iAPX 432、Intel i860、IA-64といった革新的アーキテクチャを導入することにより、x86を置き換えようとしたが、ことごとく不成功に終わり、結果としてx86は後付けの拡張を続け今日までインテルの主力アーキテクチャとして延命している。
インテルは現在、2011年リリースのSandy Bridgeマイクロアーキテクチャから新たにAVX命令フォーマットを導入し、古いx86命令セットからの脱却を徐々に進め始めている。




性能の定量的な評価は難しいが、実行時間を指標値にするのは信頼できる値の1つ

実行時間 = 実行命令数 * 命令あたりのクロックサイクル数 * 動作周波数
         = 実行命令数 * クロックサイクル数 / 命令 * 秒数 / クロックサイクル



プログラム性能のキーになる要素

    コンパイラ
    シリコン
    アーキテクチャ(キャッシュ、並列化etc)




1.10 歴史展望と参考文献

CDを図書館で借りてこなかったので、後回し
(借りればよかった…)


1.11 演習問題

省略

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

[パタヘネ:読書メモ]1章 コンピュータの抽象化とテクノロジ

1.1:はじめに~1.3:コンピュータの内部


一般常識(情報処理試験レベル)なので、軽く飛ばし読み。
特にコメントすべき内容は無し。



1.4:性能


性能を測るための指標値は色々ある



CPI:1命令あたりの平均クロックサイクル数
(clock cycle pre instruction)
命令によって、消費クロック数は異なるが、平均値としての性能

命令セットが同じで、異なる実装の場合、性能比較する上での”目安”になる。
昔のx86 CPU “intel CPU” vs “amdの互換CPU”のような感じ。

CPIによる性能比較は、異なるアーキテクチャ間では意味が無い。
なぜなら、機械語の1命令で出来る事のレベルが異なるから。

また、CPIは1.0以下になる可能性もありうる。
なぜなら1サイクルあたり2つ以上の命令を実行できるアーキテクチャもあるから。

-> intelのHT/マルチコア的なこと? それともパイプライン処理的な意味?
   良く分からなかったので、気に留めつつ読み進める。





CPUの使用時間
CPUの使用時間(性能)は、典型的に以下の式で導き出せる
CPU使用時間 = 実行命令数 * CPI / クロック周波数

クロック周波数はスペックシートから分かる。

実行命令数を取るのは難しい。
取得方法の例
プロファイラを利用する
ハードウェアに用意されているカウンタを利用
シミュレータ上で実行して、計測する

CPIは、命令の頻度や、メモリシステム、プロセッサの構造に依存する。らしい。

-> 命令の頻度は分かるけど、残り2つがイメージできない...
-> 詳細は4,5章で後述するらしいので、一旦保留




1.5:電力の壁


CMOSの消費電力は、トランジスタの切り替え電力がメイン



消費電力は、以下の式で近似できる
消費電力 = 容量性負荷 * 電圧^2 * 切り替え周波数

切り替え周波数は、クロック周波数に関係する
容量性負荷 は、出力に接続するトランジスタ数(fanout),配線,トランジスタの静電容量に関係する

fanoutについては、以下のサイトが詳しい
ディジタル電子工学講座
ファンアウト – FPGA用語集



電圧は2乗されるので、ここを下げると効果がデカイ。
intelの80386は5Vだったけどcore 2では1.1Vしか必要ない。
5^2=25で, 1.1^2=1.21なので、ここだけで20分の1以下になっている。

ただし、電圧を下げすぎるとリーク電流が増えるので限界がある。



クロック数を上げるのも限界がある。
3Ghzのクロックだと、1クロックあたりに光が10cmしか進めないぐらいの速さなので、これ以上劇的には増やせない。



1.6 方向転換:単体プロセッサからマルチプロセッサへ


性能向上には以下の制約条件がある。

1.消費電力が増えると、発熱が増える
2.発熱が増えると、半導体の寿命が短くなる
3.廃熱を行うにしても限界がある(水冷等が限界?)。




というわけで、これ以上性能を上げるには、CPUの数(コア数etc)を増やし、並列化によって対応するしかなくなってくる。

-> 最近は言語側での、並列処理サポートも増えてきている。
   C#では、最近asyncキーワードで非同期処理が出来る。
   node.jsでは、基本のアーキテクチャ自体が非同期処理ベースになってる。




1.7: 実例:AMD Opteron X4の製造技術とベンチマーク・テスト



シリコンウェハーのサイズ、CPUチップのサイズ、歩留まりの考え方など
技術的な面には関係ないので、特に感想なし

本題と関係ないけど、歩留まりは英語でyieldらしい。
yieldと聞くとrubyの予約語のアレを連想するけど、英単語的には以下の意味があるらしい。

〈作物・製品などを〉産する.
〈利子・収益などを〉もたらす,生む.
〈結果などを〉引き起こす.





ベンチマーク
ベンチマークの内容によってCPIの変動幅が13倍もある
本の中の表では、0.75~10.00の幅だった

4822284786
コンピュータの構成と設計 第4版(上) ハードウエアとソフトウエアのインタフェース (Computer Organization and Design: The Hardware/Software Interface, Fourth Edition)