前回、HiFive1のプログラムをC/C++で開発するための環境を構築した。
次に、HiFive1のCPUコアの基本性能を測定してみよう。まずは、通常のプログラムを動作させる前に、同一の命令を何度も実行して、命令のスループットおよびレイテンシを測定してみる。
命令のスループットおよびレイテンシを測定するベンチマークプログラム
命令のスループットは、同一の命令を何度も実行して、消費サイクル数を測定する。命令間の依存関係は存在しないように記述しておく。 これにより、プログラムの依存関係のストールを無視して、命令発行能力を測定することができる。
for(int i=0; i < 65536; i++) { func (&dst, src0, src1); func (&dst, src0, src1); func (&dst, src0, src1); func (&dst, src0, src1); func (&dst, src0, src1); func (&dst, src0, src1); func (&dst, src0, src1); func (&dst, src0, src1); }
一方の命令レイテンシは、同一の命令を何度も実行するが、各命令には依存関係が存在する。 これにより、ある命令を実行してから、次の同一命令を実行するのに必要なサイクル数を測定できる。
for(int i=0; i < 65536; i++) { func (&dst, src0, src1); func (&dst, dst, src1); func (&dst, dst, src1); func (&dst, dst, src1); func (&dst, dst, src1); func (&dst, dst, src1); func (&dst, dst, src1); func (&dst, dst, src1); }
これを、各命令について実装してみる。まずは、add
, sub
, mul
, div
について実装してみた。
以下のような共通関数を用意し、測定対象を関数ポインタとして渡す。以下は、1ループにつき8命令、
uint32_t inst_latency(void (*func)(int*,int,int), int init_src0, int init_src1) { uint32_t start_time, end_time, wrap_time; int dst, src0 = init_src0, src1 = init_src1; rdmcycle (&start_time); for(int i=0; i < 65535; i++) { func (&dst, src0, src1); func (&dst, dst, src1); func (&dst, dst, src1); func (&dst, dst, src1); func (&dst, dst, src1); func (&dst, dst, src1); func (&dst, dst, src1); func (&dst, dst, src1); } rdmcycle (&end_time); // printint(0, start_time, 10, 1); uart_print("\r\n"); // printint(0, end_time, 10, 1); uart_print("\r\n"); // printint(0, wrap_time, 10, 1); uart_print("\r\n"); return end_time - start_time; }
測定対象の関数は以下のようになる。
inline void add_inst(int *dst, int src0, int src1) { asm volatile ("add %0,%1,%2" :"=r"(*dst) :"r"(src0),"r"(src1) : ); } inline void sub_inst(int *dst, int src0, int src1) { asm volatile ("sub %0,%1,%2" :"=r"(*dst) :"r"(src0),"r"(src1) : ); } inline void mult_inst(int *dst, int src0, int src1) { asm volatile ("mul %0,%1,%2" :"=r"(*dst) :"r"(src0),"r"(src1) : ); } inline void div_inst(int *dst, int src0, int src1) { asm volatile ("div %0,%1,%2" :"=r"(*dst) :"r"(src0),"r"(src1) : ); }
以下のように、関数をコールしては測定結果を出力した。
void add_bench () { uint32_t ret; ret = inst_latency (add_inst, 1, 1); printint(0, ret, 10, 1); uart_print("\r\n"); ret = inst_throughput (add_inst, 1, 1); printint(0, ret, 10, 1); uart_print("\r\n"); ret = inst_latency (sub_inst, 1, 1); printint(0, ret, 10, 1); uart_print("\r\n"); ret = inst_throughput (sub_inst, 1, 1); printint(0, ret, 10, 1); uart_print("\r\n"); ret = inst_latency (mult_inst, 1, 1); printint(0, ret, 10, 1); uart_print("\r\n"); ret = inst_throughput (mult_inst, 1, 1); printint(0, ret, 10, 1); uart_print("\r\n"); ret = inst_latency (div_inst, 0x7fffffff, 1); printint(0, ret, 10, 1); uart_print("\r\n"); ret = inst_throughput (div_inst, 0x7fffffff, 1); printint(0, ret, 10, 1); uart_print("\r\n"); }
測定結果は以下のようになった。単純な整数演算系は、1サイクル1命令実行することができる。 一方で、乗算命令は実行に7サイクル程度必要であり、除算命令は36命令程度必要である。 ただし、除算命令はオペランドの値に従ってレイテンシが変わるようなので、これよりも小さな場合もあるし、大きな場合もあるかもしれない。 とりあえずここでは、被除数=0x7fffffff, 除数=0x01で測定している。
Latency(Cycle) | Throughput(Cycle) | Latency(CPI) | Throughput(IPC) | |
---|---|---|---|---|
add | 568291 | 560092 | 1.08 | 0.93 |
sub | 560094 | 560094 | 1.06 | 0.93 |
mul | 3632046 | 3689406 | 6.93 | 0.142 |
div | 18836174 | 18893504 | 35.92 | 0.027 |