ぐだぐだ低レベルプログラミング(22) GD32VF103、サイクルカウンタ実測例

Joseph Halfmoon

前回、GD32VF103のRISC-Vコアのサイクルカウンタを動かせるようになったので、今回は短いコードについて測定してみて感触を確かめたいと思います。「たかが」サイクルカウンタと言っても「高等な」マシンだと、いろいろあったりするので。シンプルなシステムなので素直に使えるとよいなあ。

※「ぐだぐだ低レベルプログラミング」投稿順indexはこちら

まずは、測定項目ですが、以下のようにいたしました。

  1. NOP10個
  2. NOP20個
  3. 10万回ループ
  4. 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のところを読む必要を感じました)で、もすこしばらけるかと予想していたのですが、安定してます。

CycleConter resultsNOP10回とNOP20回の結果をみると、1NOP=1サイクル=1命令と「あるべき姿」じゃないかと思います。末尾の1は、サイクル/命令カウンタをキャプチャする前後の命令のうち、多分前の方がカウントされているためかと考えると辻褄があいます。

次の10万回ループをみると、命令数20万1回というのは予想どおり。実行サイクル数の30万と5サイクルからは、1ループ=2命令=3サイクルで処理、ということが読み取れます。また、ループの最初か最後で4サイクルかかっている(ループ部分の一種分岐予測的な処理のためかと)ことも予想できます。

同じループの中でコールした場合、1回のコールで6命令余分に実行されるので、命令実行回数は予想どおり。サイクル数は、6命令なのだけれど行ってこいのコール、リターンがあって10サイクル。ここも順当でしょうか。

まったくもって、素直で分かり易い実装に見えます。

ぐだぐだ低レベルプログラミング(21) GD32VF103のサイクルカウンタ辺の実装 へ戻る

ぐだぐだ低レベルプログラミング(23) GD32VF103、メモリアクセスを測る へ戻る