Cコンパイラ最適化のベンチマークテスト

[しまねソフト研究開発センター](https://www.s-itoc.jp)（略称 ITOC）にいます、東です。

先日来、STマイクロエレクトロニクス製のマイコンボード、「Nucleo-F401RE」と、開発環境「STM32CubeIDE」を使って、mruby/c の移植などを行ってきました。
その活動の中で、Cコンパイラの最適化については触れてきませんでしたので、今回簡単なベンチマークテストを実施するとともに、結果を公開します。

# 使用機器・環境

STM32マイコン評価ボード　STM32 Nucleo-F401RE
(https://www.st.com/ja/evaluation-tools/nucleo-f401re.html)

開発環境　STM32CubeIDE v1.15.1
(https://www.st.com/ja/development-tools/stm32cubeide.html)

Cコンパイラ
arm-none-eabi-gcc.exe (GNU Tools for STM32 12.3.rel1.20240306-1730) 12.3.1 20230626

マイコン　STM32F401RET6
(https://www.stmcu.jp/stm32/stm32f4/stm32f401/12226/)

  * ARM Cortex-M4
  * 84MHz
  * FLASH 512KB
  * RAM 96KB

![20240907-Nucleo-64_STM32F401.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/addeae10-44bc-6ffc-e817-10b4759ed902.jpeg)



# ベンチマークプログラム

mruby/c でハノイの塔の解法プログラムを記述し、それを実行します。ただし結果の表示は、速度に影響してしまうのでコメントアウトしています。
意図的に、配列、ハッシュ、文字列など、まんべんなく使うように記述しています。
mruby/c VM は、2024/09/07現在の master を使います。

```ruby
# hanoi tower benchmark

puts "Hanoi benchmark"

def make_hanoi_data( num )
  data = { :A=>[], :B=>[], :C=>[] }
  num.times {
    data[:A] << num
    num -= 1
  }
  return data
end

def solve_hanoi_recursive( n, data, ta, tb, tc )
  if n == 1
    data[tc] << data[ta].pop()
    puts_hanoi_data( "move #{ta}:#{data[tc][-1]} >#{tc}", data )
  else
    solve_hanoi_recursive( n-1, data, ta, tc, tb )
    solve_hanoi_recursive( 1,   data, ta, tb, tc )
    solve_hanoi_recursive( n-1, data, tb, ta, tc )
  end

  return data
end

def puts_hanoi_data( s, data )
  n = data[:A].size + data[:B].size + data[:C].size
  fmt = "%s=%-#{n*2}s "

  s << "   "
  data.each {|k,v|
    s << sprintf( fmt, k, v.join(",") )
  }
#  puts s
end

def hanoi( num )
  data = make_hanoi_data( num )
  puts_hanoi_data( "start      ", data )
  result = solve_hanoi_recursive( num, data, :A, :B, :C )
end

t1 = VM.tick
10.times do
  hanoi(8)
end
t2 = VM.tick

p [t1, t2, t2-t1]
```



# 実行結果

Release モードで、NDEBUG プリプロセッサ定数を定義した状態でコンパイルしています。

```
-O0 -DNDEBUG
 5785ms
   text	   data	    bss	    dec	    hex	filename
 111324	   1832	  34784	 147940	  241e4	mrubyc-tuto-stm32.elf

-O1 -DNDEBUG
 2392ms
   text	   data	    bss	    dec	    hex	filename
  86532	    924	  34788	 122244	  1dd84	mrubyc-tuto-stm32.elf

-O2 -DNDEBUG
 2303ms
   text	   data	    bss	    dec	    hex	filename
  85972	    924	  34792	 121688	  1db58	mrubyc-tuto-stm32.elf

-O3 -DNDEBUG
 2277ms
   text	   data	    bss	    dec	    hex	filename
  94604	    924	  34792	 130320	  1fd10	mrubyc-tuto-stm32.elf

-Os -DNDEBUG
 2770ms
   text	   data	    bss	    dec	    hex	filename
  74284	    924	  34788	 109996	  1adac	mrubyc-tuto-stm32.elf

-Ofast -DNDEBUG
 2276ms
   text	   data	    bss	    dec	    hex	filename
  94356	    924	  34792	 130072	  1fc18	mrubyc-tuto-stm32.elf
```

## 実行速度比較

| 最適化 | 実行時間(ms) | -O0比 (倍) |
|:-:|-:|-:|
| -O0 | 5785 | 1.00 |
| -O1 | 2392 | 2.42 |
| -O2 | 2303 | 2.51 |
| -O3 | 2277 | 2.54 |
| -Os | 2770 | 2.09 |
| -Ofast | 2276 | 2.54 |

![20240907-bench1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/b4da60b8-6cc3-35d9-68d2-0ec7ead310fa.png)

 * 実行速度は、最適化をかければ、（-O0でなければ）、どのレベルでも2倍以上の速度に向上する
 * -O3と-Ofast がほぼ同スコアで最速だが、その他の最適化オプションの場合と大差は無い


## オブジェクトサイズ (.text) 比較

| 最適化 | サイズ(KB) | -O0比 (%) |
|:-:|-:|-:|
| -O0 | 108.7 | 100.0 |
| -O1 | 84.5 | 77.7 |
| -O2 | 84.0 | 77.2 |
| -O3 | 92.4 | 85.0 |
| -Os | 72.5 | 66.7 |
| -Ofast | 92.1 | 84.8 |

![20240907-bench2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504019/1e60055d-8ab6-89ab-e78b-dc4c734382c7.png)

 * サイズは、-Osが最小になり、3割以上縮小する
 * 実行速度ベンチマークの場合ほどの大差は無い


## 考察

コンパイラの最適化の結果は、速度に関してはめざましい効果がありました。一方、サイズに関しては2〜3割の縮小にとどまりましたが、それでも十分といえるものでした。
速度とサイズのバランスを考えると、-O2　か　-Os　あたりが最もバランスが良いように思われます。
なお、コンパイラは、Thumb命令セットを生成しています。
-Os は、挙動から想像するに、かなり深い最適化をするようなので、volatile 修飾子などを丁寧に書いておかないと、動かなくなる場合がありそうです。


# おまけ

PIC32MX170 (MIPS32) でも、同じベンチマークをうごかしてみました。

| MPU | Core | clock(MHz) | 最適化 | 実行時間(ms) |
|-|-|-:|:-:|-:|
|PIC32MX170F256B| MIPS | 40 | -O2   | 4437 |
|STM32F401RET6　| ARM Cortex M4 | 84 | -O2   | 2303 |

コアの種類は違いますが、ほぼクロック周波数の違いが実行速度の違いで、クロックあたりの速度はMIPSの方が少し優秀といった結果でした。


# おわりに

今回は、Cコンパイラの最適化オプションを変えて、ベンチマークテストをやってみました。
プログラムは、mruby/c の VMを使って、mrubyプログラムを動かすものでしたが、測定結果は、C言語プログラムを作る時の汎用的な結果として参考になるものだと思います。