プログラムdeタマゴ

nodamushiの著作物は、文章、画像、プログラムにかかわらず全てUnlicenseです

組み込みでラムダは使って良いけど、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で関数を渡す

 このように、何か勝手にやって欲しい処理の間に、ユーザー定義の処理を挟むには大凡以下の方法があります。

  1. 関数を定義して、関数ポインタを渡す
  2. 関数オブジェクトを渡す
  3. ラムダ式を渡す

 先に書いた例ではラムダ式を使っていますね。なお、実際にはラムダ式は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でも良くなるかも知れませんが。