世界最速のタスク・スイッチ・ライブラリ (Task Switch Library for ARM-G++ [M0/M0+/M1])

前回投稿したAVR用のタスク・スイッチ・ライブラリをARM(M0/M0+/M1)向けにポーティングしてみた。

世界最速のタスク・スイッチ・ライブラリ (Task Switch Library for AVR-G++)

8ビットのAVRではポインターのRWが2命令になってしまうが、16ビット以上のCPUなら1命令で済むので効率が良い。また、AVRと同じくARMは演算のみなら1サイクルで動作するうえにCPUクロックも高いので非常に高速に動作する。ただ、CPUとBUSのクロックの違いによりタイミングによってはBUS経由で実行する命令が待たされてしまうことがあるのが難点だ。理解不足なので間違ってるのかもしれないがメモリやI/Oなどの[BUS経由で実行する命令]~[BUS経由で実行する命令]までの実行命令サイクル数が(一般的には)偶数になるように命令をうまく配置できたら待たされることがなくなるのではないかと思う。但し、割り込みやDMAなども考慮するとどれだけ改善できるのかは定かではないが...

関数呼び出しのオーバーヘッドを含めたスイッチング時間は次のようになった。これ以上の高速化は不可能だろう。たぶん。

 65 cycles (ATSAMD21(48MHz)=1.354us,
       RP2040(133MHz)=0.489us)

※計算上でのサイクル数。状況によりバス・ウェイトがときどき入るので現実的には2割増程度になると思った方が良い。かも。

Seeeduino XIAO(ATSAMD21)で試験してて、まだRP2040(Raspberry Pi Pico)は試してみてないけどマイクロ秒は楽に切れそうだ。(/・ω・)/

タスクスイッチングについてSVC命令を使うべきという意見も聞こえてきそうだがオーバーヘッドが大きくかなり遅くなってしまうので、そこまで拘る必要がない小規模開発向けとして最速を目指してみた。

それとM0/M0+/M1のみ考えていたのでM4以降で対応されているFPUについては何も考慮してないが特に気にする必要はないのだろうか?とか、M3は動くのかな?とか、M3以降をOS無しで素で使ってる人などいないからそもそも考える必要がないのかな?とか...疑問は尽きない。

あと、インライン・アセンブリの仕様が良くわからなくて色々試していたら、たまたま解決策が見つかったのはいいんだけど、かなりイレギュラーっぽい書き方なのが若干不安。普通に書くと余計な命令が追加されてしまうのが嫌だったのと使われるレジスタを指定したくて★のような記述をしてみたら狙い通りになったのはいいがこういう書き方ってあり?なし?どっち?

しかし、まだarmアセンブラ2度目の初心者ではあるが、armの命令はなんとなく覚えずらい気がする...って、もしかして、命令の問題じゃなくて脳の老化現象のほうが問題か?(-_-;)

特に(説明が)理解しずらかったのがpush/pop命令。レジスタ・リストを指定することができて便利そうなのだがドキュメントの説明を読んだときに同じレジスタ・リストを指定したpush/popでは元に戻らないのか?と勘違いしてしまった。そんな命令使いものにならんと思っていたところコンパイラの吐き出したコードを眺めていたらある関数が次のようなコードにコンパイルされていた。

あれ?これができるということは使えるんじゃん?と思って次のようなコードで試験してみたところ正しくpush/popされていることが確認できた。ググッてみたら私のように勘違いして説明してるサイトも見受けられるので注意したほうが良いだろう。

【概要】

新しいタスクを登録する。sizeにはスタック領域のバイト数、funcには実行する関数を指定する。登録したタスクはyield呼び出しにより順番に実行される。malloc()で割り当てられたメモリ・アドレスがfuncの引数として渡され、funcからリターンするとタスク終了する。

タスクを切り替える。実行待ちのタスクがなければ何もせずに戻る。

実行待ちのタスクがなければtrue、あればfalseを返す。

[Arduinoのみ] 指定時間(ミリ秒)待つ。待っている間は他タスクを実行し続ける。

[Arduinoのみ] 指定時間(マイクロ秒)待つ。待っている間は他タスクを実行し続ける。

自タスクを起床待ちリストに追加し休止する。

タスクを起床する。prepend=falseのときは実行待ちタスクリストの最後に、prepend=trueのときは実行待ちタスクリスト先頭に追加する。all=falseのときは起床待ちの先頭タスクのみ起床し、all=trueのときは起床待ちの全タスクを起床する。

起床待ちのタスクがなければtrue、あればfalseを返す。

【サンプル・スケッチ】

【修正履歴】
2021-10-24
割り込み処理にも対応できるよう割り込み禁止処理を追加。この影響によりスイッチングが1サイクル遅くなってしまった。あと、疑似的に実行優先度を変更できるよう起床するタスクを実行待ちタスクリスト先頭にも追加できるように仕様変更。

2021-10-16
スタック・ポインタは8バイト境界にする必要があるとの記述を見つけたので修正。

2021-10-15
おまけで追加したSync::sleep()がスタックやレジスタを壊すというバグがあったので修正。ついでに関数からリターンしたときにタスク終了するようにしてみたり、スタック領域としてmallocで割り当てたメモリ領域のアドレスを関数引数として渡すように改良。引数はタスク固有の識別情報として利用したり、余裕を持ったスタック領域の割り当てによりタスク固有の情報などが格納できるようになる。

2021-10-14
ipレジスタの保存復旧を削除しスタック構成を最適化した結果さらに高速化できた。これ以上は無理っぽい。

おまけとしてタスク同期のためにタスクを休止/起床するだけのクラス(Sync)も作ってみた。興味のある方はそれを利用してevent/semaphore/mutexなどの同期機能を作ることが出来るはずなので挑戦してみてほしい。なお、mutexの実装に必要なタスク識別情報がこのライブラリには存在しないので重複しないIDを各タスクに割当するなどの工夫が必要となる。

2021-10-13
壊してはいけない一部のレジスタを壊していたので修正。

【ライブラリ】