chibicc compiler を6800向けに改造する (10) プロファイラ

2025/06/07BASICMASTER, 昔のパソコン

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の背中は見えてきた感じである。