新しい VC++ のコンパイラを試す

ツイッターを見てたら、

Introducing a new, advanced Visual C++ code optimizer | Visual C++ Team Blog

こんなのがありまして、へー、でも試すの面倒くさいなぁと思っていたらですね、

Try out the latest C++ compiler toolset without waiting for the next update of Visual Studio | Visual C++ Team Blog

こんなのもあって、要は、「nuget で簡単に最新コンパイラ試せるよ!」ってなってたわけです。
知りませんでした。簡単ですねー。

で、

  • VC++のプロジェクトが既にあれば、 nuget パッケージ足すのは簡単
  • VC++ Build Tools (VSと排他)がインストールされていれば、それをベースに使える

ってあったんですが、コマンドラインから叩きたい。既に VS2015 入っているという僕には、いろいろ面倒そうなので、別の未知を模索してみました。

インストールしてみた(自己流)

とはいえ、やったことは簡単です。

http://dist.nuget.org/win-x86-commandline/latest/nuget.exe

をダウンロードして、適当なフォルダに配置して、

nuget install VisualCppTools -source http://vcppdogfooding.azurewebsites.net/nuget/ -Prerelease

って実行しました。そうすると、 VisualCppTools.14.0.24104-Pre\lib\native に dll やら exe やらが沢山展開される。
このまま実行しても良いですけど、 CRT のヘッダ・ライブラリしかないよんって感じなので、どうにか Windows 系のヘッダとかも参照したい。

しばらく考えた末、

call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat"
set PATH=C:\Users\kawasaki\Desktop\cl\VisualCppTools.14.0.24104-Pre\lib\native\bin\amd64;%PATH%

ってやったら、 cl.exe 動きました(上のは、AMD64版のコンパイラ)。

現時点でのバージョン

見る感じ、出来たてほやほや。

f:id:espresso3389:20160506120552p:plain

f:id:espresso3389:20160506120605p:plain

さて、ひたすら問題を指摘されている例のコードでもコンパイルしてみるかw

続続) 気づいたら、C# が C++ の速度を凌駕している!

先日の記事、

espresso3389.hatenablog.com

.NET Native だとどうよ?っていう話があったので、試してみました。
コードは趣旨を変更しない範囲で弄りました。
スレッドプールのプライオリティとかどうなってんの?っていう疑問はあるんですが、
実行した感じ、それらの影響はなさそうなので、結構適当です。
概要だけ提示できれば良いので、XAMLは提示しませんが、普通にバインドして表示しているだけです。

using System;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Threading.Tasks;
using Windows.UI.Xaml.Controls;

namespace App1
{
  public sealed partial class MainPage : Page
  {
    public ObservableCollection<double> Times { get; } = new ObservableCollection<double>();

    public MainPage()
    {
      this.DataContext = this;
      this.InitializeComponent();
      Loaded += (s, e) => workOnBackground();
    }

    async Task workOnBackground()
    {
      double t1 = 0, t2 = 0;
      await Task.Factory.StartNew(() => {
        int w = 4321;
        int h = 6789;
        int stride = (w + 3) & ~3;
        var a = new byte[stride * h];

        t1 = time(() => test1(a, w, h, stride));
        t2 = time(() => test2(a, w, h, stride));
      });

      Times.Add(t1);
      Times.Add(t2);
    }

    static void test1(byte[] a, int w, int h, int stride)
    {
      for (int y = 0; y < h; y++)
      {
        int offset = y * stride;
        for (int x = 0; x < w; x++)
        {
          a[x + offset] = (byte)(x ^ y);
        }
      }
    }

    static unsafe void test2(byte[] a, int w, int h, int stride)
    {
      fixed (byte* p0 = a)
      {
        for (int y = 0; y < h; y++)
        {
          byte* p = p0 + y * stride;
          for (int x = 0; x < w; x++)
          {
            p[x] = (byte)(x ^ y);
          }
        }
      }
    }

    static long time(Action action, int count = 100)
    {
      var tw = new Stopwatch();
      tw.Start();
      for (int i = 0; i < count; i++)
        action();
      tw.Stop();
      return tw.ElapsedMilliseconds;
    }
  }
}

結果

32-bit/64-bit で特に何の差も見れませんでしたので、そこに関しては割愛。
結局、ここにある、 "Compile with .NET Native tool chain" を ON/OFF した結果だけです。

f:id:espresso3389:20160505103943p:plain

OFF の時。

f:id:espresso3389:20160505104053p:plain

ON の時。

f:id:espresso3389:20160505104043p:plain

面白いですねー。

.NET Native が OFF の場合の速度の傾向はフルの .NET Framework 4.6 のコードと同じですね。配列は遅く、 unsafe は速い。ただ、前回調べた、 .NET Framework 4.6 や C++ と比べると、微妙に遅いですね。

ところが、 .NET Native を ON にすると、配列と unsafe の速度差がグッと縮む。速度的に、2~3%程度の差しかありません。この差だと、 unsafe コードを無理して使わないでも良いような気がする速度です。
この差を見せられると、時間をかけてコンパイルできる AOT コンパイラは馬鹿に出来ないなぁと思わざるを得ません。

.NET Framework 4.6 や C++ との速度差は、プライオリティの問題かもしれませんし、フレームワーク自体の問題かもしれませんし、その他のノイズかもしれません。調べるのが面倒なのでそこまでは追求しません。

結論

UWP 環境だと、フルの .NET Framework/C++ ほど速くありません。ただ、その差は、2~3%程度で、別に気にする程のパフォーマンス差は感じられません。

.NET Native は、配列でコードを書いている人に対しては絶大。unsafe とほぼ同じ程度のパフォーマンスが出る。逆に、 unsafe では、 .NET Native の前後での速度差はあんまり見られない。

これだと、C++ を棄てましょうどころか、 unsafe 要らなくね?っていう議論にまでなりそう。

個人的には、レガシーテクノロジー、レガシー言語、棄てようぜ路線に変更なし。

気づいたら、C# が C++ の速度を凌駕している!

5年半程前に書いた、この記事。

espresso3389.hatenablog.com

C#というか、.NET Framework 4.6 は、 RyuJIT という新しい JIT の導入によって、64-bit 環境での実行が高速化されています。なので、さーて、少しは面白い結果が出るんじゃないかなぁーと。

blogs.msdn.microsoft.com

で、環境は、

Windows 10 Pro Insider Preview Build 14332
.NET Framework 4.6.1
Visual Studio 2015 Update 2
CPU: Intel Core i7-3770S 3.1GHz
Memory: 32GB

という環境です。ここで、実行してみました。

まずは、C#版。

3091
2282

上が配列。下が unsafe コード。unsafe が速くなってます。74%ぐらいの時間で終わるようになってる。
unsafe コードを書くモチベーションが上がります。better C として考えると、モチベーション上がりますねー!

さて、C++版。

2344

んん?あれっ?実行するプログラム間違った?
いや、間違ってない。

遅いじゃないですかー。C#に負けてるじゃないですかー。これ、64-bit 版です。
32-bit だったら?

2343

変わんねー。

C++ 敗北。
もう、C++使う理由がなくなる感じなのでは・・・。

仕事によっては、C++指定、あるいは、そうせざるを得ないことってありますけど、この結果は予想外です。個人的には、嬉しいです。

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

Aero-Snap による Window サイズの変更を検出する

GetWindowPlacement では、ウィンドウが通常の状態でのウィンドウ座標と、実際にウィンドウが、最大化されている(SW_MAXIMIZE)のか、最小化されている(SW_MINIMIZE)のか、あるいは、通常の状態(SW_SHOWNORMAL)なのかが取得できます。従って、普通なら、この関数を呼び出せば、特に何の問題もなく、ウィンドウの状態を取得することが出来ます。

ところが、 Windows 8 で導入された Aero-Snap ([Win] + 矢印キー)では、この関数では関知できない状態になります。
具体的には、ウィンドウの状態が、通常の状態(SW_SHOWNORMAL)であると報告されます。これだと、アプリは自分の状況を正しく認識できません。

なので、 Aero-Snap 状態であることを知るためにはもうちょっと小細工をする必要があります。答えをそのまま書くと、 SW_SHOWNORMAL と認識された場合には、 GetWindowRect を使って取得したウィンドウサイズと rcNormalPosition の値を比較すると、 Aero-Snap かどうかを区別できるようです。

bool isResizeByAerosnap(HWND wnd)
{
  // The function works because of the fact that:
  // When the window is resized by Aero-snap, GetWindowPlacement returns
  // SW_SHOWNORMAL but wp.rcNormalPosition is not identical to the size
  // returned by GetWindowRect.
  WINDOWPLACEMENT wp;
  wp.length = sizeof(WINDOWPLACEMENT);
  GetWindowPlacement(wnd, &wp);
  if (wp.showCmd != SW_SHOWNORMAL)
  	return false;
  RECT rc;
  GetWindowRect(wnd, &rc);
  return memcmp(&rc, &wp.rcNormalPosition, sizeof(RECT)) != 0;
}

まぁ、問題はそんなことよりも、 Aero-Snap 状態を再現する API がないことなんですけどねw