背景:C言語とMicroPythonでどれぐらいの性能差があるのかを把握したい
取り組み:C言語でwaitなしのLチカ(GPIOのL/H)をやってみる。そのためSDKを使って簡単なバイナリをビルドする。
結果:C言語だと、1パルスの周期は34.4ns(29MHz)、システムクロック150MHzなので、概算で5命令ぐらいでループが実装されている計算、ソースを確認すると、ループは4命令で収まっている。MicroPythonの場合、チューニングしても、周期は1.44us(694.4KHz)
詳細:
RaspberryPiの公式ドキュメントを参照、まだ調べていませんが、UF2形式でバイナリを作るとUSBケーブルで転送できるようだ。picotoolというのもあるらしい。
www.raspberrypi.com
github.com
VS Code等を使わず手動でビルドしたいのだが。。Claudeに素でビルドしたいと相談。まずは開発環境を入れろと。
sudo apt update && sudo apt upgrade -y sudo apt install -y \ cmake \ gcc-arm-none-eabi \ libnewlib-arm-none-eabi \ libstdc++-arm-none-eabi-newlib \ build-essential \ git \ python3
(過去にARMマイコン用にビルドしたのか、WSL上には上記ツール一式がすでに入っていたのでインストール作業は省略)更新だけしておく
sudo apt update ; sudo apt upgrade --yes
次にSDKを入れろと指示
cd ~/<some_path> git clone https://github.com/raspberrypi/pico-sdk.git cd pico-sdk git submodule update --init
SDKの場所を定義しておく
export PICO_SDK_PATH=~/<some_path>
次はサンプルをビルドしてみろと
git clone https://github.com/raspberrypi/pico-examples.git cd pico-examples mkdir build && cd build cmake .. make blink -j4
ビルド待っている間にサンプルコードを入手
RPi Pico 2Wのボードに実装されているLEDのブリンクサンプル(ボード上のLEDはGPIOに直結されていないので、外付けCHIP経由で制御必要)
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
int main() {
// init Wi-Fi chip (need for LED Blink)
if (cyw43_arch_init()) {
return -1; // initialize failed
}
while (true) {
cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, 1); // LED ON
sleep_ms(500);
cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, 0); // LED OFF
sleep_ms(500);
}
}picotoolをビルドするところで止まっているようなのだが。。
Downloading Picotool Using picotool from /home/xxx/lang/c/rp2350/samples/build/_deps/picotool/picotool TinyUSB available at /mnt/c/cygwin64/home/xxx/lang/c/rp2350/pico-sdk/lib/tinyusb/hw/bsp/rp2040; enabling build support for USB. BTstack available at /mnt/c/cygwin64/home/xxx/lang/c/rp2350/pico-sdk/lib/btstack cyw43-driver available at /mnt/c/cygwin64/home/xxx/lang/c/rp2350/pico-sdk/lib/cyw43-driver Pico W Bluetooth build support available. mbedtls available at /mnt/c/cygwin64/home/xxx/lang/c/rp2350/pico-sdk/lib/mbedtls lwIP available at /mnt/c/cygwin64/homexxx/lang/c/rp2350/pico-sdk/lib/lwip Pico W Wi-Fi build support available. Skipping cache_perfctr example which is unsupported on this platform Skipping ssi_dma example which is unsupported on this platform Skipping multicore_fifo_irqs example which is unsupported on this platform Skipping some Pico W examples as WIFI_SSID is not defined Adding 20 BTstack examples of type 'background' Skipping RTC examples as hardware_rtc is unavailable on this platform Skipping universal examples as PICO_RISCV_TOOLCHAIN_PATH and PICO_ARM_TOOLCHAIN_PATH are not defined Skipping TinyUSB dual examples, as TinyUSB hw/mcu/raspberry_pi/Pico-PIO-USB submodule unavailable Skipping FreeRTOS examples as FREERTOS_KERNEL_PATH not defined -- Configuring done (18.2s)
picotool使う予定ないのだが、毎回Picotoolのビルド?で詰まるので、picotoolだけを別途ビルドしてみる。以下の手順は”Getting started with Raspberry Pi”より
$ git clone https://github.com/raspberrypi/picotool.git $ cd picotool #You will also need to install libusb if it is not already installed, $ sudo apt install libusb-1.0-0-dev $ mkdir build $ cd build $ export PICO_SDK_PATH=~/<spme_path>/pico-sdk $ cmake ../ $ make
エラーなくビルドはできたので、/usr/local/bin/に置いておく
sudo make install
これでpicotoolで詰まるのは解消と期待して、buildを再開
cmake .. -DPICO_BOARD=pico2_w -DPICOTOOL_FETCH_FROM_GIT=OFF
使っているボードがpico2_wなので、その件も指定しておく
先ほどのメッセージも処理が止まっているのではなく、単に時間がかかっているだけの可能性が高い。しばらく放置してみる
何も表示されない状態が4分続いた後、configが再開された。時間がかかるのはSDK自体をビルドしようとしているからかも。。
かなり時間がかかったが、つづいて、make blinkでビルド。blink.uf2が生成された。このコードはLEDがGPIOに直結されているのを前提に実装されているとのことで、Pico2Wの基板上のLEDは光らないだろう。
$ ls -ltr total 3536 -rwxrwxrwx 1 someone someone 160165 May 5 10:38 Makefile* -rwxrwxrwx 1 someone someone 1133 May 5 10:38 cmake_install.cmake* drwxrwxrwx 1 someone someone 4096 May 5 11:16 CMakeFiles/ -rwxrwxrwx 1 someone someone 955660 May 5 11:37 blink.elf* -rwxrwxrwx 1 someone someone 475376 May 5 11:37 blink.elf.map* -rwxrwxrwx 1 someone someone 500064 May 5 11:37 blink.dis* -rwxrwxrwx 1 someone someone 728842 May 5 11:37 blink.hex* -rwxrwxrwx 1 someone someone 259100 May 5 11:37 blink.bin* -rwxrwxrwx 1 someone someone 519168 May 5 11:37 blink.uf2*
MicroPythonが起動できず文鎮になってしまっているPico2Wに対して、USBケーブル経由で焼いてみる。
ClaudeはLED点滅しないと言っていたが、実際はPico2Wのボードでも点滅した。blink.cのソース追いかけてないのでなぜ光るのか不明だが。。
ビルド環境はできたとして、、まずはwaitなしでGPIO1をひたすらLHするプログラムを作る
file: blink.c
#include "pico/stdlib.h"
#include "hardware/clocks.h"
#define OUT_PIN 1 // GPIO:1 for output
int main() {
// set systemclock to 150MHz
set_sys_clock_khz(150000, true);
// setup GPIO for output
gpio_init(OUT_PIN);
gpio_set_dir(OUT_PIN, GPIO_OUT);
while (true) {
gpio_put(OUT_PIN, 1); // H
gpio_put(OUT_PIN, 0); // L
}
}file: CMakeLists.txt
cmake_minimum_required(VERSION 3.13)
include($ENV{PICO_SDK_PATH}/pico_sdk_init.cmake)
project(blink C CXX ASM)
set(CMAKE_C_STANDARD 11)
pico_sdk_init()
add_executable(myblink blink.c)
target_link_libraries(myblink pico_stdlib hardware_clocks)
pico_add_extra_outputs(myblink)ビルド
mkdir build && cd build cmake .. -DPICO_BOARD=pico2_w -DPICOTOOL_FETCH_FROM_GIT=OFF make myblink
GPIO:1に対してLHが繰り返されるのでオシロで計測、一周期は34.4nsで周波数は29MHz

システムクロックが150MHzなので、RP2350はARMでRISCと考えて、1命令1クロックだとすると、150/29 = 5.1より、ループはマシン語では約5命令ぐらいで動いている。Cで実装した場合の最速なのではかなろうか。
Claudeよりソース見れますよと言われたので再確認、以下の通り
$ arm-none-eabi-objdump -d myblink.elf | grep -A 20 main 10000274 <main>: *略* 10000294: f000 fcd8 bl 10000c48 <set_sys_clock_pll> 10000298: 2001 movs r0, #1 1000029a: f000 f811 bl 100002c0 <gpio_init> 1000029e: 2301 movs r3, #1 100002a0: ec43 3044 mcrr 0, 4, r3, r3, cr4 100002a4: f04f 0100 mov.w r1, #0 100002a8: f04f 0201 mov.w r2, #1 100002ac: ec42 3040 mcrr 0, 4, r3, r2, cr0 100002b0: ec41 3040 mcrr 0, 4, r3, r1, cr0 100002b4: e7f8 b.n 100002a8 <main+0x34>
LHのループは以下のコード、試算上は約5命令だったが実際の命令数としては4命令である
100002a8: f04f 0201 mov.w r2, #1 100002ac: ec42 3040 mcrr 0, 4, r3, r2, cr0 100002b0: ec41 3040 mcrr 0, 4, r3, r1, cr0 100002b4: e7f8 b.n 100002a8 <main+0x34>
Claudeによるクロック計算は以下

MicroPythonによるLチカ
C言語でGPIOのL/Hを確認できたので、次はMicroPythonでどれぐらいの速度でL/Hできるかを確認する
GPIOのL/HをPythonで実装、REPLで貼り付けて実行すると、周期は21.6us , 46.3KHz
import machine
from machine import Pin
print(machine.freq())
pin1 = Pin(1, Pin.OUT)
while True:
pin1.high()
pin1.low()
関数にして最適化(@micropython.native デコレータ)を付けると、周期は4.1us , 243.9KHzまで改善
import machine
from machine import Pin
@micropython.native
def speed_test(pin):
while True:
pin.high()
pin.low()
print(machine.freq())
pin1 = Pin(1, Pin.OUT)
speed_test(pin1)
補足:native最適化なしの単なる関数化だけの場合、周期は5us、周波数は200KHz。REPL実行から関数化すると一気に高速化する。関数実行の場合、中間コードに変換されているから早いのかと思ったけど、REPLでも中間コードに変換して実行するらしい。変数参照方式の差によると説明を受けた。関数内で実行しない場合、変数はグローバルスコープに所属しており、グローバルスコープの参照には時間がかかると(ハッシュ計算->辞書テーブル検索->オブジェクト取得の3ステップらしい(AIによる説明))
viperの最適化を付けると、周期は1.44us , 694.4KHzまで改善。コードはSIOレジスタのGPIO XOR用レジスタに対して0x0002を書き続けて、XOR動作によりL/Hを反転させる
import machine
from machine import Pin
#SIO_BASE_REG = 0xd0000000
#SIO_GPIO_OUT_XOR_REG_OFFSET = 0x28
#SIO_GPIO_OUT_XOR_REG = SIO_BASE_REG + SIO_GPIO_OUT_XOR_REG_OFFSET
@micropython.viper
def _gp1_xor():
while True:
# 0xd0000028 : SIO_GPIO_OUT_XOR_REG
ptr32(0xd0000028)[0] = 0x00_02 # 0x00_02 means gpio:1
print(machine.freq())
pin1 = Pin(1, Pin.OUT)
_gp1_xor()
RP2350 Datasheetより
SIOレジスタについて


■考察
コンパイル型言語のC言語とインタプリタ型言語のPython を単純に比べるのはどうかとは思うが、どれだけの性能差があるのか把握は必要
| 条件 | C言語 | REPL | 関数化 | 関数化+native最適化 | 関数+viper最適化 |
|---|---|---|---|---|---|
| 周期 | 34.4ns | 21.6us | 5us | 4.1us | 1.44us |
| 処理時間比 | 1 | 628倍 | 145倍 | 119倍 | 42倍 |
※最後の関数+viperはXORレジスタに0x2を書き続けるという反則のレベルのチューニングなので*1・・普通に頑張って120倍程度まで下げられるということかと。今回は単純はGPIOのL/Hだけですが、条件分岐等が入ってくるとさらに差が広がると思われます。
■補足
REPLだと遅い理由は変数アクセスの性能差と言う説明を受けた件

■追記
最後のXORレジスタを叩く実装はちょっと姑息な高速化かと思って、まずはSET/CLRレジスタにそれぞれ値を書くことでH/Lするように実装した。下記プログラムの場合、1周期は1.44usで周波数は694.4KHz
import machine
from machine import Pin
#SIO_BASE_REG = 0xd0000000
#SIO_GPIO_OUT_SET_REG_OFFSET = 0x18
#SIO_GPIO_OUT_CLR_REG_OFFSET = 0x20
@micropython.viper
def _gp1_set_clr():
while True:
# 0xd0000018 : SIO_GPIO_OUT_SET_REG
ptr32(0xd0000018)[0] = 0x00_02 # 0x00_02 means gpio:1
# 0xd0000020 : SIO_GPIO_OUT_CLR_REG
ptr32(0xd0000020)[0] = 0x00_02 # 0x00_02 means gpio:1
print(machine.freq())
pin1 = Pin(1, Pin.OUT)
_gp1_set_clr()結局XORを使った姑息な高速化と変わらない結果となった。この理由を考えると・・・XORの場合、一つのパルスを作るのに2回XORを実行、SET/CLRの場合は1つのパルスを作るのにSETが1回、CLRが1回で合計2回のコードを実行、よって実行されるレジスタ操作関数の数は変わらないと。むしろSET/CLRの方がループ回数が半分になるので実は有利なのかも。(XORの場合ループ2回回って、1パルス、SET/CLRの場合、ループ1回回って1パルス)
*1:XOR用レジスタに書くだけなので楽な実行で逃げていますが、今後時間があればGPIO_OUT_SET/GPIO_OUT_CLEARのレジスタに書く実装で試したい