chibicc compiler を6800向けに改造する (10) プロファイラ
Dhrystone や Whetstone が動くようになったのだけど、Z80のコンパイラと比べると遅いのである。
Z80は1命令数あたりの実行ステート数は多いが、4MHzで動く。今ならもっと高速で動作するチップもあるが、1980年ごろは4MHz(あるいはNTSCに合わせて3.58MHz)であった。
6800は、無印が1MHz、68A00が1.5MHz、68B00が2.0MHzで動く。ベーシックマスターはさらに遅くて、754.56KHz(12.07296MHzを16分周)である。
Z80のADD A,n が7ステート、ADD A,(HL) も7ステート、ADD HL,ss が11ステートである。
6800のADDA #n は2サイクル、ADDA n,Xは5サイクルなので、クロック数の差を考えるとやや遅めぐらい。
ただし、16bit加算命令がMC6800にはないので、chibicc 6800のように intが16bitである処理系を作ると、さらに遅くなる。
Dhrystoneベンチマークの癖
実際にDhrystoneを走らせると、想像以上に差が大きい。Z80 4MHzとMC6800 1MHzだと 3-4倍差、MC68B00 2MHz換算としても 1.5-2倍遅い。
実はDhrystoneベンチマークには、特定の操作が多く使われる癖がある。検索して調べると、文字列操作が大きな比重を占めるらしい。
Z80には文字列処理に適したブロック転送・比較・変更命令があるので、ここで差がついていると思われる。
本当にそうなのか? 測定せよ!
てっとりばやくプロファイリングしてみる
以前、ベーシックマスターのエミュレーター bm2を改造して、プロファイラを作ったことがある。
chibicc コンパイラのテストは Fuzix-Compiler-Kit 付属の emu6800 を使っているので、これを改造してプロファイラを作ろうとして vi を立ち上げてから気がついた。
emu6800 -d オプションで、実行トレースが取れるから、これを処理すればいいんじゃね?
実行トレースは下記のように表示されるので、これをリダイレクトしてファイルに保存し、一番左のアドレスをカウントすれば簡単なプロファイラになる。
0100 : ----I- 00|00 0000 0000 | STS 3845 [0000] 0103 : --Z-I- 00|00 0000 0000 | LDS #EFFF 0106 : ---NI- 00|00 0000 EFFF | LDX #145A 0109 : ----I- 00|00 145A EFFF | BEQ 0122 [00] 010B : ----I- 00|00 145A EFFF | LDX #3845 010E : ----I- 00|00 3845 EFFF | LDAB #45 0110 : ----I- 00|45 3845 EFFF | LDAA #38 0112 : ----I- 38|45 3845 EFFF | ADDB #5A 0114 : -V-NI- 38|9F 3845 EFFF | ADCA #14 0116 : ----I- 4C|9F 3845 EFFF | STAB 03 [00]
perlでサクッと書いてみた
#!/usr/bin/perl use strict; use warnings; my %count; while (<>) { if (/^([0-9A-Fa-f]{4})\b/) { $count{uc $1}++; } } foreach my $hex (sort keys %count) { printf "%s\t%d\n", $hex, $count{$hex}; }
実行すると以下のような情報が得られる。$011B番地からカウント数が増えているのは、BSSの初期化のループである。
$ emu6800 -d 6800 dhry.bin dhry.map &> prof.txt $ ./prof.pl < prof.txt 0100 1 0103 1 0106 1 0109 1 010B 1 010E 1 0110 1 0112 1 0114 1 0116 1 0118 1 011A 1 011B 5210 011D 5210 011E 5210 0120 5210
sort -k2 -nr すると、時間を要しているアドレスがわかる。 テストは2万回実行されるので、最頻ループは2万の倍数(80万回)回っているようだ。
$ ./prof.pl < prof.txt |sort -k2 -nr |head -20 307F 800000 307E 800000 3078 800000 3075 800000 3074 800000 3073 800000 2013 380000 2011 380000 200F 380000
これだけだと、どのルーチンで時間を喰っているのかが わかりにくいので、mapファイルの情報を追加する。
#!/usr/bin/perl use strict; use warnings; # Check if file2 name is provided as a command-line argument die "Usage: $0 file2.txt\n" unless @ARGV == 1; my $file2 = $ARGV[0]; # Read file2 and build a hash: hex number => last field my %hex_to_info; open my $fh2, '<', $file2 or die "Cannot open $file2: $!"; while (<$fh2>) { chomp; next if /^\s*$/; # Skip empty lines my @fields = split /\s+/, $_; # Split by spaces or tabs my $hex = $fields[0]; my $info = $fields[-1]; # Get the last field $hex_to_info{$hex} = $info; } close $fh2; # Read file1 from standard input, merge with file2 info if exists, and print result while (<STDIN>) { chomp; next if /^\s*$/; # Skip empty lines my @fields = split /\s+/, $_; # Split by spaces or tabs my $hex = $fields[0]; print join("\t", @fields); if (exists $hex_to_info{$hex}) { print "\t$hex_to_info{$hex}"; } print "\n"; }
./prof.pl <prof.txt |./pm.pl dhry.map >prof_map.txt
80万回の部分を調べてみる。8bit✖️8bitのルーチンが10万回呼ばれている。この中に8bitのループがあるので、80万回実行されているわけだ。
2E60 212 __lt16u 2E62 212 2E63 212 2E65 212 306A 100000 __mul8x8 306C 100000 306E 100000 306F 100000 3070 100000 3073 800000 3074 800000 3075 800000 3078 800000 307A 160000 307C 160000 307E 800000 307F 800000 3081 100000 3082 20000 __div16x16 3083 20000
他に多いのは以下の部分。
1FD2 20000 __copy_struct2 (ループ部分で38万回回る) 2DF4 320032 __strcpy_start (strcpyのループ部分)
ソースコード上のどこが時間を食っている?
__mul8x8は、サイズ50✖️50のint配列へのアクセスが多数あるためであった。
int Arr_2_Glob [50] [50];
配列アドレスの計算のために、✖️100の乗算が多数呼ばれている。
ldab 1,x ldaa 0,x pshb psha ldab #<100 clra jsr __mul16x16 ins ins
__copy_struct2 は構造体の代入である。
昔は構造体の代入は利用できない処理系も多かったので、マクロになっている。
#ifdef NOSTRUCTASSIGN #define structassign(d, s) memcpy(&(d), &(s), sizeof(d)) #else #define structassign(d, s) d = s #endif
これがdhry_1.cのループ内で2回呼ばれている。繰り返しがあるので、合計2万回。コピーのための内部ループがあるので、かなり時間がかかる。
structassign (*Ptr_Val_Par->Ptr_Comp, *Ptr_Glob);
dhrystoneを高速化するには?
100倍の乗算、structのコピー、strcpyのメインを高速化すれば良さそうである。
しかし、copystructもstrcpyは既に2バイトずつコピーするように書いてあって、高速化の余地はあまりなさそう。
ということで、100倍を頑張ってみた。100倍専用のサブルーチンを作って、100倍のときだけこれを呼ぶ。
; Arr_2_Par_Ref [Int_Loc] [Int_Index] = Int_Loc; ldab 1,x ldaa 0,x jsr __mul100
これで、194,780,122サイクル、0.0584 DMIPS (1MHz換算)。2MHzのMC68B00なら0.117 DMIPS。
最初にDhrystoneが動くようになった時点で、0.039 DMIPS。その後の改良で 0.0486 DMIPSなので、ずいぶん速くなった。
Z80最速のSDCC Linux版が 0.201 DMIPS (4MHz換算)なので、まだまだだが、 HITEC-C 0.1278 DMIPSの背中は見えてきた感じである。
ディスカッション
コメント一覧
まだ、コメントがありません