Visual Studio 2015 Update 1 で C++ <experimental/generator> を試してみる

Visual Studio 2015 Update 1 のリリースノート関連を見ていたら、 Coroutine が動くぜ!っていう記事があったので試してみました。

blogs.msdn.com

コルーチンっていうのは、まぁ、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();
}

っていうのと同じです。C++の方が戻り値にも型推論が使えて auto って書けるので少し楽です。

コンパイル時の注意

上で参照しているページにもあるのですが、現時点(Update 1)では、若干の制約があって、 /SDL, /RTCx との互換性がないので、これらの設定はOFFにしないといけません。また、/await というオプションも必要です。

C/C++ All Options だと、次の3つの部分を調整する必要があります。

f:id:espresso3389:20151203013854p:plain

この、 coroutine っていう単語と、 generator っていう単語、そして、 await という単語が故意にちりばめられている様にも見えて、全部いっぺんに届いてやれることが増えすぎて楽しい感じです。

最適化の具合

f:id:espresso3389:20151203014129p:plain

さて、このプログラムを、とりあえず思いつく限りの最適化全開でコンパイルすると、本当は、次のコード相当ぐらいに最適化されると嬉しいのですが、どうなるでしょうか。

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++でも素直な文法で利用できるようになったことは大歓迎です。楽しくてしょうがない感じですね。