Visual Studio 2015 Update 1 で C++ <experimental/generator> を試してみる
Visual Studio 2015 Update 1 のリリースノート関連を見ていたら、 Coroutine が動くぜ!っていう記事があったので試してみました。
コルーチンっていうのは、まぁ、C#でいう yield return で、C++でも yield っていうそのまんまの名前なんですが、rubyとかだとgeneratorって呼ばれている奴ですね。
乱数のジェネレーターを作ってみる
とりあえず、コードを書いてみます。今回は、無限に乱数を発生し続けるというだけのジェネレータを作ってみました。
#include <cstdio> #include <random> #include <experimental/generator> auto random() { std::mt19937 r; for (;;) yield r(); } int main() { for (auto r : random()) std::printf("%d\n", r); }
std::random_device を使って、延々と乱数を発生させ、それを yield で返しています。 C# だったら、乱数の範囲など、厳密には違いますが、
IEnumerable<int> random() { var r = new Random(); for (;;) yield return r.Next(); }
コンパイル時の注意
上で参照しているページにもあるのですが、現時点(Update 1)では、若干の制約があって、 /SDL, /RTCx との互換性がないので、これらの設定はOFFにしないといけません。また、/await というオプションも必要です。
C/C++ All Options だと、次の3つの部分を調整する必要があります。
この、 coroutine っていう単語と、 generator っていう単語、そして、 await という単語が故意にちりばめられている様にも見えて、全部いっぺんに届いてやれることが増えすぎて楽しい感じです。
最適化の具合
さて、このプログラムを、とりあえず思いつく限りの最適化全開でコンパイルすると、本当は、次のコード相当ぐらいに最適化されると嬉しいのですが、どうなるでしょうか。
int main() { std::mt19937 r; for (;;) std::printf("%d\n", r()); }
つまり、コルーチンは、完全に最適化で除去されて、mainにインライン化されたら嬉しいなぁっていう話です。
下のリスティングは、 /FAs で出力された asm に色々と分かりやすいように成形を加えたものですが、残念ながら、コルーチン系の関数はインライン化されないようです。
push ebp mov ebp, esp push ecx push esi ; _Ptr (esi) = [new generator] lea ecx, DWORD PTR _$S1$8[ebp] call ?random@@YA?AU?$generator@IV?$allocator@D@std@@@experimental@std@@XZ ; if (!_Ptr) return; mov esi, DWORD PTR _$S1$8[ebp] test esi, esi je SHORT main_end ; _coro_resume(_Ptr); mov eax, DWORD PTR [esi] push esi call eax mov esi, DWORD PTR _$S1$8[ebp] add esp, 4 ; if (_Coro.done()) goto loop_end; cmp DWORD PTR [esi+4], 0 je SHORT loop_end ; if (!_Ptr) return; test esi, esi je SHORT main_end npad 7 loop: mov eax, DWORD PTR [esi-8] push DWORD PTR [eax] push OFFSET $SG4294967238 call _printf ; _coro_resume(_Ptr); mov eax, DWORD PTR [esi] push esi call eax add esp, 12 ; if (!_Coro.done()) goto loop; cmp DWORD PTR [esi+4], 0 jne SHORT loop mov esi, DWORD PTR _$S1$8[ebp] loop_end: ; if (_Ptr) _coro_destroy(_Ptr); test esi, esi je SHORT main_end or DWORD PTR [esi+4], 1 mov eax, DWORD PTR [esi] push esi call eax add esp, 4 main_end: xor eax, eax pop esi mov esp, ebp pop ebp ret 0
初期化の関数はさておき、内部では、
- _coro_resume()
- _Coro.done()
- _coro_destroy()
などが呼び出されているようですね。機能的には、名前の通り、「次の要素を取り出す」、「イテレーションが完了したかどうか」、「コルーチンの終了処理」とまんまですね。
ということで、普通に書ける処理なら、普通に自分でループを書いた方が速いですね。
一方で、C# LINQ も遅いことは理解しつつも、やっぱり、その生産性の高さという部分で利用価値があるわけで、それがC++でも素直な文法で利用できるようになったことは大歓迎です。楽しくてしょうがない感じですね。