10分で終わるだろうと思っていたセッション内容も予想を反して1時間も喋ってしまい後片付けを急がせる結果になってしまい申し訳なかったです。
中々思う通りにはいきませんね。
メインセッションで出したプログラムはこことここの内容をもっと簡略化したものです。
ただ、僕個人の意見としては
「CPUがどうやって動くの?
とか
「CPUはこんな命令があるんだ」
ということを知っていれば良くて、必要性が無ければアセンブラを覚える必要は無いと思ってます。
そして、数名の方からご質問いただいた気持ち的なメインセッション。CとC++で同じサイズのコードが出た部分についての解説です。
以降使用しているコンパイラはgcc3.4.4でコンパイルオプションは-O2 -mh -mint32です。
前述とは裏腹にアセンブラのコードを出していますが、行数が多いってことは出力コードがデカくて遅い程度の認識で良いです。
最初にCのプログラムです。
| C (元のプログラム) | アセンブラ |
| typedef unsigned char byte; typedef unsigned short word; typedef unsigned long dword; #define PADDR 0xffffd1 #define PADR 0xffffd3 #define PBDDR 0xffffd4 #define PBDR 0xffffd6 int main(void) { byte data; volatile byte* paddr = (byte*) PADDR; volatile byte* padr = (byte*) PADR; volatile byte* pbddr = (byte*) PBDDR; volatile byte* pbdr = (byte*) PBDR; *padr = 0x00; *paddr = 0x00; *pbdr = 0x00; *pbddr = 0x0f; while(1) { *pbdr = *padr; }; return 0; } | .file "main.c" .h8300h .section .text .align 1 .global _main _main: mov.l er6,@-er7 mov.l er7,er6 sub.b r2l,r2l // 0を作ってる mov.b r2l,@16777171:8 // *padr = 0x00; mov.b r2l,@16777169:8 // *paddr=0x00; mov.b r2l,@16777174:8 // *pbdr = 0x00; mov.b #15,r2l mov.b r2l,@16777172:8 // *pbddr = 0x0f;
.L2: // while(1) { mov.b @16777171:8,r2l mov.b r2l,@16777174:8 // 上の行と合わせて*pbdr = *padr; bra .L2 // }
.end .ident "GCC: (GNU) 3.4.4" |
これが元のCのプログラムです。元のソースが短いこともありいいコード出してくれてますね。
素直にクラス化してみます。DDRは最初に設定したらその後の書き換えはする必要がないので、コンストラクタのパラメータにして、データレジスタへの書き込みは*オペレータをオーバーロードしてクラスオブジェクトがポートであるように扱えるようにしてみます。
| C++ | アセンブラ |
| // 型定義とマクロ割愛 class pio { private: volatile byte* ddr; volatile byte* dr; public: pio(dword ddr_val, dword dr_val, byte dir, byte init = 0) : ddr(reinterpret_cast<volatile byte*>(ddr_val)), dr(reinterpret_cast<volatile byte*>(dr_val)) { *dr = init; *ddr = dir; } volatile byte& operator *() { return *dr; } }; int main(void) { pio pa(PADDR, PADR, 0x00); pio pb(PBDDR, PBDR, 0x0f); while(1) { *pb = *pa; }; return 0; } | .file "main.cpp" .h8300h .section .text .align 1 .global _main _main: mov.l er6,@-er7 mov.l er7,er6 add.l #-16,er7 mov.l er6,er0 subs #4,er0 subs #4,er0 sub.b r3l,r3l mov.l #16777169,er2 mov.l er2,@er0 adds #2,er2 mov.l er2,@(-4,er6) mov.b r3l,@16777171:8 mov.l @er0,er2 mov.b r3l,@er2 subs #4,er0 subs #4,er0 mov.l #16777172,er2 mov.l er2,@er0 adds #2,er2 mov.l er2,@(-12,er6) mov.b r3l,@16777174:8 mov.l @er0,er3 mov.b #15,r2l mov.b r2l,@er3 .L4: mov.l @(-4,er6),er2 mov.b @er2,r3l mov.l @(-12,er6),er2 mov.b r3l,@er2 bra .L4 .end .ident "GCC: (GNU) 3.4.4" |
コンストラクタ・オペレータの部分がインライン展開されているところはいいのですが、DR/DDRのアドレスをクラスメンバで持っており、それらのオブジェクトの初期化コード、及びDRのアクセスをする際にクラスメンバであるアドレスを一旦er2に入れてからアクセスしていることで長いコードになっています。
但し、このコードの場合最適化によってはCと同等のコードを出す可能性はあります。
テンプレートを使ってCと同等なコードを出すように細工してみました。
(中さんから「何回書き直しました?」と聞かれたとき「鋭い!」と思った)
| C++ | アセンブラ |
| // 型定義とマクロ割愛 struct input { enum { VAL = 0 }; }; struct output { enum { VAL = 1 }; }; template<dword DDR, dword DR, typename d0 = input, typename d1 = input, typename d2 = input, typename d3 = input, typename d4 = input, typename d5 = input, typename d6 = input, typename d7 = input> class pio { private: enum { DIR = d7::VAL << 7 | d6::VAL << 6 | d5::VAL << 5 | d4::VAL << 4 | d3::VAL << 3 | d2::VAL << 2 | d1::VAL << 1 | d0::VAL }; public: pio(byte init = 0) { *reinterpret_cast<volatile byte*>(DR) = init; *reinterpret_cast<volatile byte*>(DDR) = static_cast<byte>(DIR); } volatile byte& operator *() { return *reinterpret_cast<volatile byte*>(DR); } }; int main(void) { pio<PADDR, PADR> pa; pio<PBDDR, PBDR, output, output, output, output> pb; while(1) { *pb = *pa; }; return 0; } | .file "main.cpp" .h8300h .section .text .align 1 .global _main _main: mov.l er6,@-er7 mov.l er7,er6 sub.b r2l,r2l mov.b r2l,@16777171:8 mov.b r2l,@16777169:8 mov.b r2l,@16777174:8 mov.b #15,r2l mov.b r2l,@16777172:8 .L4: mov.b @16777171:8,r2l mov.b r2l,@16777174:8 bra .L4 .end .ident "GCC: (GNU) 3.4.4" |
C++のコードは長くなってしまいましたが、利点はDDR初期設定値をinput/outputという型で指定できるので初期値がわかりやすくなること。
書いていて気付いたんですが、出力ビットのビット位置を指定するようにした方が良かったですね。
普段C++でtemplateを使っている方なら既に気付かれることと思いますが、見ての通りマクロに毛が生えた程度のコードです。
例えば
*pb=*pa;
の部分は
*reinterpret_cast<volatile byte*>(PBDR) = *reinterpret_cast<volatile byte*>(PADR);
と同じです。
つまり、
*((volatile byte*) PBDR) = *((volatile byte*) PADR);
と同じですね。
さて、元となっているCのソースでは変数宣言
volatile byte* padr = (byte*) PADR;
volatile byte* pbdr = (byte*) PBDR;
をしているにも関わらず
*pbdr = *padr;
部分がレジスタやローカルフレームによる変数領域を使用せず
mov.b @16777171:8,r2l
mov.b r2l,@16777174:8
と最適化されて出力されています。これはCで書けば
*((byte*) PBDR) = *((byte*) PADR);
になるわけですね。これは先ほどテンプレートを展開したコードと同等です。
C++のtemplate版の方は上記のとおり、変数を使用せずアドレスを指す整数値をポインタへキャストしてアクセスしていますから。
このような理由でCとC++のコードのコンパイル結果が同じものになっています。
もし、Cのソースの方のコンパイル結果が
mov.l #16777171,er2
mov.l #16777174,er3
.L2:
mov.b @er2,@er3
bra .L2
となっていたら同等な結果は得られなかったかもしれません。
# templateにはtemplateなりの問題もありますが、少々姑息と思いながらも今までC++は遅くなる、大きくなるからダメと言われてずーっとC++を使えなかったから、そのはけ口として出してしまいました^^;
そして、どうしても言えなかったことですが、-O2付けないとtemplate版でもC++の方がデカくなりますw
そして、パワポ資料はここ