前回、GD32VF103のRISC-Vコアのサイクルカウンタを動かせるようになったので、今回は短いコードについて測定してみて感触を確かめたいと思います。「たかが」サイクルカウンタと言っても「高等な」マシンだと、いろいろあったりするので。シンプルなシステムなので素直に使えるとよいなあ。
※「ぐだぐだ低レベルプログラミング」投稿順indexはこちら
まずは、測定項目ですが、以下のようにいたしました。
- NOP10個
- NOP20個
- 10万回ループ
- 10万回ループの中でサブルーチン呼び出し
この4つの同じコードについて、サイクルカウンタと、リタイヤ済命令数カウンタを読み取ってみようという形です。Cで書いた計測のトップ部分はこんな感じ。最初のwrite_csrこそ、カウンタを「活かす」ためのおまじないです。
write_csr(0x320, 0); //mcountinhibit cntDiff = cycNop10(); printf("cycNop10 : %08x\n",(unsigned int)cntDiff); cntDiff = cycNop20(); printf("cycNop20 : %08x\n",(unsigned int)cntDiff); cntDiff = instNop10(); printf("instNop10 : %08x\n",(unsigned int)cntDiff); cntDiff = instNop20(); printf("instNop20 : %08x\n",(unsigned int)cntDiff); cntDiff = cycLoop100000(); printf("cycLoop100000 : %08x\n",(unsigned int)cntDiff); cntDiff = instLoop100000(); printf("instLoop100000 : %08x\n",(unsigned int)cntDiff); cntDiff = cycCall100000(); printf("cycCall100000 : %08x\n",(unsigned int)cntDiff); cntDiff = instCall100000(); printf("instCall100000 : %08x\n",(unsigned int)cntDiff);
被測定関数そのものは、アセンブラで記述したですが、Cからそれらを呼び出すために参照するヘッダは以下のようです。
#ifndef TEST_PFC_H #define TEST_PFC_H #include <stdint.h> uint32_t cycNop10(void); uint32_t cycNop20(void); uint32_t instNop10(void); uint32_t instNop20(void); uint32_t cycLoop100000(void); uint32_t instLoop100000(void); uint32_t cycCall100000(void); uint32_t instCall100000(void); #endif /* TEST_PFC_H */
さて、アセンブラ関数の定義の最初の部分です。先ほど述べた4つの処理について、サイクルを測るものは cyc、命令数を測るものはinstという名でグローバル参照できるようにしてあります。どの関数も、形は同じで、結局リターンアドレスのセーブ以外使わないのですが、16バイト分ローカル領域を確保しています。
.section .text .align 2 .globl cycNop10, cycNop20, instNop10, instNop20, cycLoop100000, instLoop100000, cycCall100000, instCall100000 cycNop10: addi sp,sp,-16 sw ra, 12(sp) rdcycle t0 nop nop nop nop nop nop nop nop nop nop rdcycle a0 sub a0, a0, t0 lw ra, 12(sp) addi sp,sp,16 ret
cycとinstの違いは、コードの中で、
rdcycle
を呼ぶか、
rdinstret
を呼ぶかだけです。次は10万回の空ループです。gasをお使いの人は 1:というラベルに御馴染みかと思いますが、そうでない方に注釈しておくと、ローカルラベルです。後ろに飛ぶときは、ジャンプ命令の飛び先を1b (backのbだと思う、前に飛ぶときはfだから)などと書くと、後方の 1: が飛び先となります。
cycLoop100000: addi sp,sp,-16 sw ra, 12(sp) li s1, 100000 rdcycle s0 1: addi s1, s1, -1 bgt s1, zero, 1b rdcycle a0 sub a0, a0, s0 lw ra, 12(sp) addi sp,sp,16 ret
その次は、10万回ループの中でサブルーチンをさらに呼び出しています。呼び出されたサブルーチンは形だけで、直ぐに戻ってきます。
instCall100000: addi sp,sp,-16 sw ra, 12(sp) li s1, 100000 rdinstret s0 1: jal ra, callTarget addi s1, s1, -1 bgt s1, zero, 1b rdinstret a0 sub a0, a0, s0 lw ra, 12(sp) addi sp,sp,16 ret callTarget: addi sp,sp,-16 sw ra, 12(sp) lw ra, 12(sp) addi sp,sp,16 ret
測定結果を下にまとめました。各3回ずつ測った平均値なのですが、全て3回の測定値は一致していました。なにか、命令フェッチの違い(コードはFlashROMに置かれており、実行速度は120MHzです。Flashからフェッチしたら数サイクルはかかるじゃない、と思ったのですが、そんな素振りはほとんどなく。あとで、例によって読んでいなかったデータシートのFlashのところを読む必要を感じました)で、もすこしばらけるかと予想していたのですが、安定してます。
NOP10回とNOP20回の結果をみると、1NOP=1サイクル=1命令と「あるべき姿」じゃないかと思います。末尾の1は、サイクル/命令カウンタをキャプチャする前後の命令のうち、多分前の方がカウントされているためかと考えると辻褄があいます。
次の10万回ループをみると、命令数20万1回というのは予想どおり。実行サイクル数の30万と5サイクルからは、1ループ=2命令=3サイクルで処理、ということが読み取れます。また、ループの最初か最後で4サイクルかかっている(ループ部分の一種分岐予測的な処理のためかと)ことも予想できます。
同じループの中でコールした場合、1回のコールで6命令余分に実行されるので、命令実行回数は予想どおり。サイクル数は、6命令なのだけれど行ってこいのコール、リターンがあって10サイクル。ここも順当でしょうか。
まったくもって、素直で分かり易い実装に見えます。