組み込みでラムダは使って良いけど、std::functionは駄目っぽい
例えば、組み込みでウォッチドックタイマー(WDT)を使う場面を想定しましょう。
void main() { クロック設定とか; WDT->Prescaler = プリスケーラの設定; WDT->Clear = クリア; WDT->Control = イネーブル; for(;;){ WDT->Clear = クリア; 何か処理; } }
無茶苦茶長い処理をする場合は処理中にクリアを挟むことはあるだろうけど、基本形はこれでいいでしょう。
さて、どうだろう?なんとも思わない?よし、君は素晴らしい人格者だ。幸せに生きて欲しい。
さぁ、無駄な時間を過ごす前に、ブラウザバックするなり、タブを今すぐ閉じるんだ。
モヤモヤした何かを感じるかい?奇遇だ、私もだ。さぁ、私と一緒に地獄に堕ちようよ。
こう書きたい
さて、上記の様なコードを見ると、私としては、例えばこう書きたくなります。
void main() { クロック設定とか; WDT.period( 1ms, 100_MHz ) // WDTの基準クロックと、オーバーフロー周期 .loop( [](){ 何か処理; }); }
クリアだなのなんだのは隠蔽して、勝手にやって欲しい。一回のループでするべき処理だけ定義して、後は自動でやって欲しいのです。
つまり、処理の「意味」だけに注力したいのです。レジスタとか後から読んで読めないゴミクズを、一々触りたくないのです。
(なお、1msはstd::chrono_literalsに定義してあります。 100_MHz は、ユーザー定義リテラルで自作できます。)
std::functionで関数を渡す
このように、何か勝手にやって欲しい処理の間に、ユーザー定義の処理を挟むには大凡以下の方法があります。
- 関数を定義して、関数ポインタを渡す
- 関数オブジェクトを渡す
- ラムダ式を渡す
先に書いた例ではラムダ式を使っていますね。なお、実際にはラムダ式は2の糖衣構文なので、基本的には二つですね。
「C++は関数ポインタよりも、関数オブジェクトの方が最適化が効きやすい」という話もありますが、最近のコンパイラは賢いので、関数ポインタでも最適化がいけてる気がします。実際、手元にあるIARのコンパイラでリリースビルドすると、関数ポインタの最適化ができています。
どれを使うにせよ、受け取る側(loopメソッド)はC++11以上では、std::functionで簡単に同じように受け取ることができます。
#include <functional> void loop(std::function< void () > func);
これをビルドするとどうなるのでしょうか?
以降はWDTの例を引きずるとコードが長くなるので、以下の様な単純なコードで考えてみましょう。
#include <functional> volatile int a=0,b=1;//レジスタを想定 void foo(std::function<void()> f) { while(1) { a++;//ループの最適化をさせない f(); } } void main() { foo([](){ int bb = b; b = bb * (bb + 1); // volatileなので最適化はされない }); }
IARのコンパイラで上記のコードをデバッグビルドすると、マップファイルは以下の様になります。
Entry Address Size Type Object
----- ------- ---- ---- ------
.iar.init_table$$Base 0x478 -- Gb - Linker created -
.iar.init_table$$Limit 0x488 -- Gb - Linker created -
?main 0x489 Code Gb cmain.o [4]
(略)
std::allocator<int>::allocator(const std::allocator<int>&) [subobject]
0x3a1 0x8 Code Gb main.o [1]
(略)
operator delete (void *)
0x41d 0xa Code Gb delop_0.o [3]
operator new (unsigned int, void *)
0x339 0x4 Code Gb main.o [1]
(略)
newとかdeleteとかallocatorとか、組み込みの敵なメソッドがずらり。
最適化してないからしょうが無いのかと思ったら、リリースビルドにしても、deleteや仮想テーブルが残る結果になりました。
これでは、組み込み的にはちょっと使えません。
ラムダ式がインライン化されて、もっとがっつり消えてくれるのかと思ってたんですけど、少なくともIAR Embedded Workbench for ARM8.40のコンパイラでは消えませんでした。
そこで、もうちょっと最適化させやすくしてみます。
inline void foo(std::function<void()> f)
fooにinlineを付けてみました。これでリリースビルドをすると、デリータ関連が消えました。fooも消えたので、fooがinline展開されて、デリータがいらなくなったのでしょう。(というか、C++において、inlineは基本的にヘッダーに関数をかける以上の意味は無いと思っていたのですが、消えてびっくり)
しかし、仮想テーブルは残りました。std::functionという情報を消せてないようですね。(無論コンパイラによっても違うでしょうが)
ということは、どうも組み込み的にはstd::functionが敵のようです。そうか、君もstd::vectorとかと同類の禁止薬物だったか。
sdt::functionを使わず、templateにしてみる
functionがだめならば、使わなければいいのだろう?と単純にtemplate化してみました。
template<typename F> void foo(F f)
これを、デバッグビルドしたときのマップファイルが以下です。
Entry Address Size Type Object
----- ------- ---- ---- ------
.iar.init_table$$Base 0xa4 -- Gb - Linker created -
.iar.init_table$$Limit 0xb4 -- Gb - Linker created -
?main 0xb5 Code Gb cmain.o [3]
CSTACK$$Base 0x2000'0008 -- Gb - Linker created -
CSTACK$$Limit 0x2000'0408 -- Gb - Linker created -
Region$$Table$$Base 0xa4 -- Gb - Linker created -
Region$$Table$$Limit 0xb4 -- Gb - Linker created -
__cmain 0xb5 Code Gb cmain.o [3]
__exit 0x119 0x14 Code Gb exit.o [4]
__iar_data_init3 0x7d 0x28 Code Gb data_init.o [3]
__iar_debug_exceptions 0xd6 0x1 Data Gb unwind_debug.o [4]
__iar_program_start 0x12d Code Gb cstartup_M.o [3]
__iar_zero_init3 0x41 0x3a Code Gb zero_init3.o [3]
__low_level_init 0xd3 0x4 Code Gb low_level_init.o [2]
__vector_table 0x0 Data Gb vector_table_M.o [3]
_call_main 0xc1 Code Gb cmain.o [3]
_exit 0x10d Code Gb cexit.o [3]
_main 0xcf Code Gb cmain.o [3]
a 0x2000'0000 0x4 Data Gb main.o [1]
b 0x2000'0004 0x4 Data Gb main.o [1]
exit 0xf1 0x4 Code Gb exit.o [2]
main 0xe9 0x8 Code Gb main.o [1]
main::[lambda() (instance 1)]::operator ()() const
0xd9 0xc Code Lc main.o [1]
void foo<main::[lambda() (instance 1)]>(main::[lambda() (instance 1)])
0xf5 0x18 Code Gb main.o [1]
おぉ、単にmain::[lambda()(instance 1)]という型の関数オブジェクトになったお陰で、デバッグビルドだというのに、newだとかallocとかdeleteとか仮想テーブルが一切なくなっています。
ちなみに、最適化すると、fooもラムダ式もインライン展開されて、全部消えてました。
これなら、組み込みでもニッコリですね。
関数ポインタ
テンプレートでもニッコリですが、関数ポインタにもしてみましょう。
例の様に、キャプチャしていないラムダ式は関数ポインタに暗黙に変換できます。
volatile int a=0,b=1;//単に最適化させない為だけ void foo(void (*f) ()) { while(1) { a++;// レジスタを想定して、ループの最適化をさせない f(); } } int main() { foo([](){ int bb = b; b = bb * (bb + 1); // volatileなので変に最適化はされない }); return 0; }
これを同様にIARでデバッグビルドするとこんなんができてて、微妙に8byteほどサイズが大きい。
main::[lambda() (instance 1)]::_FUN()
0x71 0xa Code Lc main.o [1]
main::[lambda() (instance 1)]::operator ()() const
0x41 0xc Code Lc main.o [1]
この_FUNってなんじゃらほい?というわけで、逆アセンブル。
0x70: 0xb580 PUSH {R7, LR}
0x72: 0x2000 MOVS R0, #0
0x74: 0xf7ff 0xffe4 BL _ZZ4mainENKUlvE_clEv ; 0x40
main::[lambda() (instance 1)]::operator ()() const
0x78: 0xbd01 POP {R0, PC}
単にインスタンスを置いてラムダ式をコールしてreturnしてるだけですね。暗黙変換されるとき、こいつが渡されるようです。こういう処理が入る分、8byteほどデバッグビルドのサイズが大きくなった様子。
なお、最適するとどっちも同じ結果になりました。
まとめ
組み込みにおいては、肥大化しても良いならテンプレートを、そうでないなら関数ポインタを使いましょう。近年のコンパイラは賢いので、関数ポインタでもちゃんと最適化してくれます。
無論、これは2019年現在の話であって、コンパイラが賢くなれば、std::functionでも良くなるかも知れませんが。