Raspberry Pi PicoとTinyGoでSDカードを使う (FatFs)の記事では、TinyGoでのSDカードアクセスは特にRead性能において、C++と比べてかなりの低下が見られましたので、その原因と改善を試みました。
速度低下の要因
比較に用いたのは以下のプロジェクトです。
ロジアナ波形比較
まず、ロジアナでリードアクセス時 (512 Byte転送時) のSPI波形をモニターしてみました。
TinyGoでのSPI波形 |
C++でのSPI波形 |
どちらもSPIのSCK周波数は125 MHz / 4 = 31.25 MHzであるのに対して、8bitアクセスを開始してから次の8bitアクセスを開始するまでの時間に、C++では310ns、TinyGoでは1.75usと非常に大きな差があることが確認できました。
コードの比較
そこで、ロジアナで確認した波形部分に相当する双方のコードを確認してみました。どちらもプロジェクトではなくSDK側にあるコードです。
tinygo/src/machine/machine_rp2040_spi.go
func (spi SPI) rx(rx []byte, txrepeat byte) error { var deadline = ticks() + _SPITimeout plen := len(rx) const fifoDepth = 8 // see txrx var rxleft, txleft = plen, plen for txleft != 0 || rxleft != 0 { if txleft != 0 && spi.isWritable() && rxleft < txleft+fifoDepth { spi.Bus.SSPDR.Set(uint32(txrepeat)) txleft-- } if rxleft != 0 && spi.isReadable() { rx[plen-rxleft] = uint8(spi.Bus.SSPDR.Get()) rxleft-- continue // if reading succesfully in rx there is no need to check deadline. } if ticks() > deadline { return ErrSPITimeout } } return nil }
pico-sdk/src/rp2_common/hardware_spi/spi.c
int __not_in_flash_func(spi_read_blocking)(spi_inst_t *spi, uint8_t repeated_tx_data, uint8_t *dst, size_t len) { invalid_params_if(SPI, 0 > (int)len); const size_t fifo_depth = 8; size_t rx_remaining = len, tx_remaining = len; while (rx_remaining || tx_remaining) { if (tx_remaining && spi_is_writable(spi) && rx_remaining < tx_remaining + fifo_depth) { spi_get_hw(spi)->dr = (uint32_t) repeated_tx_data; --tx_remaining; } if (rx_remaining && spi_is_readable(spi)) { *dst++ = (uint8_t) spi_get_hw(spi)->dr; --rx_remaining; } } return (int)len; }
まとめるとこんな感じでしょうか。
- TinyGoではマイコンのレジスタアクセスのようなmachineパッケージの一番最下層まで(Tiny)Goで記述されている。
- TinyGoのrx()関数は、タイムアウトエラー処理の追加、戻り値の違いを除き、spi.cのspi_read_blocking()関数の内容がそっくりそのまま移植されている。
行っている処理の内容自体は、TinyGoとCで全く同じと言えるので、原因はTinyGoを介したレジスタアクセスによるオーバーヘッドにありそうです。
速度改善の修正
machineパッケージ
関数の目星がついたところで、machineパッケージのうち、machine.SPIに対して、パフォーマンスに影響がある部分に関しては、C関数をコールするように修正を考えます。
通常のパッケージであれば、ローカルに修正版を置いてそちらを参照するようにすれば既存パッケージの名前を変更することなく置き換えができるのですが、machineパッケージの場合、この方法はうまく行きませんでした。
その理由はこのあたりにも記載されていましたが、machineが厳密にはパッケージではなく、TinyGoのライブラリであるからのようです。
したがって、埋め込み(embedded)により、mymachine.SPIタイプを定義して、mymachine.SPIをレシーバとした関数を定義していくことにしました。
またmachine.SPIを呼び出しているTinyFsのsdcardパッケージについては、mymachine.SPIに置き換えが必要なのでローカルに持ってきて修正を加えました。
pico_tinygo_fatfs_test/mymachine/machine_rp2040_spi.go
// +build rp2040 package mymachine // #include "./spi.h" import "C" import ( "machine" "device/rp" "errors" "unsafe" ) type SPI struct { *machine.SPI } ...
pico_tinygo_fatfs_test/sdcard/sdcard.go
package sdcard import ( "fmt" "machine" "time" "pico_tinygo_fatfs_test/mymachine" ) ... func New(b mymachine.SPI, sck, sdo, sdi, cs machine.Pin) Device { // machine.SPIは、mymachine.SPIに置き換える。(その他はmachineを維持) return Device{ bus: b, cs: cs, sck: sck, sdo: sdo, sdi: sdi, cmdbuf: make([]byte, 6), dummybuf: make([]byte, 512), tokenbuf: make([]byte, 1), sdCardType: 0, } } ...
main側では、修正したパッケージをローカルからのインポートに変更して、spi変数をmachine.SPIからmymachine.SPIに置き換えました。
pico_tinygo_fatfs_test/main.go
package main import ( "fmt" "machine" "time" "os" //"tinygo.org/x/drivers/sdcard" "pico_tinygo_fatfs_test/sdcard" //"tinygo.org/x/tinyfs/fatfs" "pico_tinygo_fatfs_test/fatfs" "pico_tinygo_fatfs_test/mymachine" ) var ( spi mymachine.SPI sckPin machine.Pin sdoPin machine.Pin sdiPin machine.Pin csPin machine.Pin ledPin machine.Pin serial = machine.Serial ) ...
rx()関数からC呼び出し
rx()関数をC側のspi_read_blocking()関数を呼び出すために書き換えます。 spi_read_blocking()関数の第1引数spi_instに関しては、該当SPIデバイスのレジスタの先頭アドレスを示すように合わせこみが必要です。 その他の引数に関しては、そのままC側の型に合わせてキャストをするのみです。 なお、タイムアウト処理については削除しました。
pico_tinygo_fatfs_test/mymachine/machine_rp2040_spi.go
func (spi SPI) rx(rx []byte, txrepeat byte) error { spi_inst := (*C.spi_inst_t)(unsafe.Pointer(&spi.Bus.SSPCR0.Reg)) repeated_tx_data := C.uint8_t(txrepeat) dst := (*C.uint8_t)(unsafe.Pointer(&rx[0])) plen := C.size_t(len(rx)) C.spi_read_blocking(spi_inst, repeated_tx_data, dst, plen) return nil }
Cソースに関しては、pico-sdk/src/rp2_common/hardware_spi/spi.cを、できるだけそのままの形で/pico_tinygo_fatfs_test/mymachine以下に配置しました。 また、spi.c, spi.hのコンパイルを通すため、pico-sdkからいくつかのファイルを配置しました。(mymachine/hardware以下)
pico_tinygo_fatfs_test/mymachine/spi.c
int spi_read_blocking(spi_inst_t *spi, uint8_t repeated_tx_data, uint8_t *dst, size_t len) { invalid_params_if(SPI, 0 > (int)len); const size_t fifo_depth = 8; size_t rx_remaining = len, tx_remaining = len; while (rx_remaining || tx_remaining) { if (tx_remaining && spi_is_writable(spi) && rx_remaining < tx_remaining + fifo_depth) { spi_get_hw(spi)->dr = (uint32_t) repeated_tx_data; --tx_remaining; } if (rx_remaining && spi_is_readable(spi)) { *dst++ = (uint8_t) spi_get_hw(spi)->dr; --rx_remaining; } } return (int)len; }
速度改善の確認
rx()関数の中身をC呼び出しに変更した後に、改めてベンチマークとロジアナで波形を確認しました。
ベンチマーク
- pico_fatfs_test (C++)
===================== == pico_fatfs_test == ===================== mount ok Type is FAT32 Card size: 32.00 GB (GB = 1E9 bytes) FILE_SIZE_MB = 5 BUF_SIZE = 512 bytes Starting write test, please wait. write speed and latency speed,max,min,avg KB/Sec,usec,usec,usec 447.7192, 6896, 1007, 1142 446.4797, 7589, 1024, 1145 Starting read test, please wait. read speed and latency speed,max,min,avg KB/Sec,usec,usec,usec 974.9766, 1050, 403, 524 974.4066, 1049, 402, 524
- pico_tinygo_fatfs_test (TinyGo machine.SPIのC呼び出し変更後)
============================ == pico_tinygo_fatfs_test == ============================ mount ok Type is FAT32 Card size: 32.00 GB (GB = 1E9 bytes) FILE_SIZE_MB = 5 BUF_SIZE = 512 bytes Starting write test, please wait. write speed and latency speed,max,min,avg KB/Sec,usec,usec,usec 390.2342, 17208, 1092, 1292 362.8215, 54979, 897, 1383 Starting read test, please wait. read speed and latency speed,max,min,avg KB/Sec,usec,usec,usec 835.5080, 16601, 554, 591 830.9257, 21483, 559, 594
かなり速度改善されて、C++に近いパフォーマンスとなりました。
ロジアナ波形
TinyGo machine.SPIのC呼び出し変更後 |
8bitアクセスを開始してから次の8bitアクセスを開始するまでの時間が1.75us → 360nsと大幅に改善されていることが確認できました。C++版と全く同じC関数をTinyGo環境でも使用しているのですが、まだ微妙な差があります。C++版のコンパイラがarm-none-eabi-gccであるのに対して、TinyGoではLLVM/Clangであることと関連があるかもしれませんが、かなり近い速度が得られているので、一旦ここまでとします。
まとめ
TinyGoのmachineパッケージの一部をCgo化することによって、C/C++水準のアクセス速度を実現することが出来ました。Cgoの機能も、呼び出し前のコピーの必要性はあるにせよ、非常にシンプルで使いやすいと言えます。 一方で、もともとmachineパッケージがすべてTinyGoで記述されているのは、Go言語の利点を損なわないための意図があると思われますので、 パフォーマンス向上のためにCgo化するアプローチが、選択肢になるかどうかは、意見の分かれるところでもあります。 例えば、今回のようなデータ転送部分には、レジスタの直接アクセスを頻繁に用いず、DMAなどを活用してオーバーヘッドを減らしつつGo記述を保つような方法も検討すべきかもしれません。
0 件のコメント:
コメントを投稿