micro:bit用のビジュアル言語処理系であるMakeCodeを使っていて気になっていたものの、今まで使ったことが無かった奴らがあります。Advancedなカテゴリの一番下の Control タブ内のブロックたちです。面白そうです。そして、これこそMakeCode処理系を支える仕組みの基礎部分にも思えます。
※「ブロックを積みながら」投稿順 index はこちら
(今回実験はすべて BBC micro:bit v1.5のデバイスで行いました。)
このControl関係のブロックは、「Advanced」をクリックしなければ現れないモノドモの中でも一番下の黒のブロックで、いかにも「無暗と触るなよ」感を醸し出しています。しかし、micro:bit上のJavaScript(TypeScript)処理系の実行の仕組みの中核部分にも見えるブロックです。バックグラウンドで何かを実行したり、イベントを待ち受けたり、「深いところ」につながっているようです。その辺の仕組みのざっくりした「解説」的なものは以下のページにあります。
The micro:bit – a reactive system
勝手な理解でまとめるとこんな感じ(本当か?)
-
- (この項は上記のページに書いてないので勝手な推測含みます)無線通信をサポートするために本物RTOS的な実時間処理(プリエンプティブ)な制御もされている。こちらは軽量スレッド「ファイバー」ベース。しかし、プリエンプティブな制御はJavaScript処理層から明示的には見えない。
- JavaScript処理系自体は、非プリエンプティブなスケジューラ(リアクティブ)によって「マルチタスク」的な処理を行っている。
- JavaScript処理系内では、各タスクは自主的に制御権を「返納」しないとならない。これにはpauseブロックを使う。
- 疑似的な「タスク」間でメッセージを伝えるための仕組みがある。
今回はその中で、バックグラウンドで処理を走らせる “Run In Background” ブロックとその「回転」具合について調べてみました。
foreverブロックの「回転速度」
MakeCode処理系の「基本」はArduino処理系に似ています。初期化や1度だけの処理を行う on startブロックは、Arduino処理系のsetup()に、繰り返し処理を行う foreverブロックは Arduinoのloop()に対応づけられます。
上記の説明文書を読むと分かるのは、何気につかっている “forever” ブロックもまた、Run In Backgroundの仕組みの上に構築されていることです。foreverブロックの実体は foreverブロック内部に置かれた処理を関数として呼び出しているバックグラウンドの無限ループであるようです。その無限ループの「回転速度」を以下のような方法で測ってみました。P16端子をひたすらトグルします。これを外部で観察すれば「回転速度」が分かると。
上記のコードを実行したときの波形が以下です。トグル周波数からforever内部を1回通過するのにかかる時間を計算すると、約24.5msということになります。
上記のドキュメントから、foreverループの実体内部に20msのpauseを含んでいることが分かります。20msはpauseの引数の待ち時間なので、pause命令そのものを含むループとその他の処理時間はざっくり4.5msと推測できます。
上のコードであると、条件分岐していたりしてイマイチ速そうではないので、下のように書き換えてみました。分岐なし。
計測結果は、以下の通り。条件分岐の一つやそこら関係ないっす。4.5msはリアクティブな制御、スケジューリングその他にかかる時間が大部分でないかと推測されます。
まあ、内部に pause 20ms を含むことから、foreverはどうやっても秒速50回転以下、上記のように軽い処理でも秒速40回転というところが実力かと思われます。内部の処理が重ければもっと落ちるはず。逆に言えば、forever1回転あたり、常にforever以外の処理に20msは割り当てられるような制御をしている、とも言えます。background処理はこの時間を超えないようにしておけば良いのかな。
バックグラウンドでもう一つループ
上ではもともとの forever ループ1個でしたが、foreverループと「同等」なループをもう一つ設けてみました。以下のような感じ。こちらはP8端子をトグルすることでその回転速度を示します。まずは foreverと同等な pause 20msで動かしてみます。
以下がその結果。黄色がforever、青が run in backgroundです。注文通り。まったく同様な回転を示しました。
ループ1個ごとに4.5ms程度のオーバヘッドがあるようなので、単純計算上は、このようなループを pause 20msの20ms内に4個くらい(あと3つ)押し込めそうです。実際にやると他のオーバヘッドが現れるかもしれませんが、多分、並行ループ3,4個で限界か?
foreverも run in backgroundもその内部で pause を呼び出している
ところが肝心です。pauseを呼び出すとスケジューラが動いて、「待っている他のタスク」に制御を渡してくれる、というわけです。pauseの呼び出し必須、そしてpauseの引数は1msが最短、ということで1msで動かしてみました。
バックグラウンド「最速」”pause 1ms” に設定
青い方、トグル83.335Hzより、1ループ166.67Hz、時間にして6.0msを得ました。先ほどの結果から、1ループあたり約4.5msがオーバヘッドとしてある筈なので、残り1.5msです。pauseの引数の1msよりもやや大きいですが、foreverループ側でも青のループ4~5回あたり4.5msくらいの処理時間がかかっていると考えると、実際にはパッツンパッツンに見えます。
ここでは結構安定して走っているように見えますが、オシロでしばらく眺めていると、ときどき波形が乱れます。JavaScriptのバッググラウンド処理のさらに後ろで走っている本物の実時間処理が見えているのかもしれません。
pauseを無くすとどうなるか?
run in background内のpauseに替えて wait を使ってみます。単純に時間を消費するだけと考えれば ms単位の pause、us単位のwait という単位の違いだけに見えます。実際には制御をスケジューラに返す pause に対して waitは時間を消費するだけのようです。その効果は大きく異なります。
foreverループ内には暗黙のpauseがあるので、run in backgroundに制御が渡ります。一方、run in backgroundは制御を放さないので結果は以下のようになります。foreverループが止まってしまう。
wait自体は高々4μsですが、その他の部分もあるので1周30μs近くもかかっています。
waitも無くすとどうなるか?
waitもなくして、「最速ループ」化してみます。
1周15μsくらい。これをみると結構 wait ブロックそのものの処理が重いみたい。
グダグダな実験でしたが、非プリエンプティブな制御の受け渡しの雰囲気は分かったかも。ただ、裏で無線(bluetooth)が走ると、状況が変わる筈。また、micro:bitでも処理速度にもリソースに余裕がある v2 だとどうですかね?無線が走っているときでも余力有りそうな気もしますが。