C# vs C++ (1)

最近の、ヴァーチャルマシン系の処理系は、実行時最適化が頭がいいから、場合によってはネイティブコンパイラより速い事がある、という議論をしたので、本当かどうか試してみました。
自作の単純なプログラムで、どっちが速いか確認してみたのです。
やってみようと思ったのが実家に帰省していたときだったので、Visual Studio 2008がインストールされていたネットブックのちょっとだけマシなやつ(Cerelon SU2300搭載)で試したのと、自宅に帰ってからVisual Studio 2010 がインストールされているCore i7で試しました。
Visual C++ 2008の最適化がかなりエゲツナイことをしていそうだ、というのも見えてきました。
また、性能の比較は、「一概に言えない」んだな、ということも判りました。
ターゲット環境での実際比較、が必要ですね。
実行時最適化の進歩もすごいんですが、ネイティブコンパイラの新プロセッサに対する最適化もまたすごい、んでしょうね。

テストコード

テストコードは以下のとおりです。
C#
namespace ConsoleApplication1CSharp
{
    class Program
    {

        uint fooOne(uint loopCount)
        {
            uint acc = 0;
            for (uint i = 0; i < loopCount; i++)
            {
                for (uint j = 0; j < loopCount; j++)
                {
                    acc = (i * 3 + 123) / 65 + acc / 1000 + j;
                }
            }

            return acc;
        }

        double fooTwo(uint loopCount)
        {
            double acc = 0.0;
            for (uint i = 0; i < loopCount; i++)
            {
                for (uint j = 0; j < loopCount; j++)
                {
                    acc = (i * 3.5 + 123.1) / 65.7 + acc / 1000.9 + j;
                }
            }

            return acc;
        }

        static void Main(string[] args)
        {

            Program p = new Program();

            // JITにコンパイルさせる
            p.fooOne(1);
            p.fooTwo(1);

            // ここから本番
            DateTime begin = DateTime.Now;
            uint ui = p.fooOne(20000);
            DateTime endInt = DateTime.Now;
            double d = p.fooTwo(20000);
            DateTime endDouble = DateTime.Now;

            TimeSpan tsInt = endInt - begin;
            TimeSpan tsDouble = endDouble - endInt;

            Console.WriteLine("Integer time: " + tsInt.TotalMilliseconds.ToString());
            Console.WriteLine("Integer value: " +  ui.ToString());
            Console.WriteLine("Double time: " + tsDouble.TotalMilliseconds.ToString());
            Console.WriteLine("Double value: " + d.ToString());
            Console.WriteLine("Press Enter to Exit");
            Console.ReadLine();
        }
    }
}

C++(というか、文法的にはC)
#include "stdafx.h"

#include "windows.h" // この形式なのは、HTML化したときタグとして反応しないように
#include "mmsystem.h"

unsigned int fooOne(unsigned int loopCount)
{
    unsigned int acc = 0;
    for (unsigned int i = 0; i < loopCount; i++)
    {
        for (unsigned int j = 0; j < loopCount; j++)
        {
            acc = (i * 3 + 123) / 65 + acc / 1000 + j;
        }
    }

    return acc;
}

double fooTwo(unsigned int loopCount)
{
	DWORD fooTwoBegin = timeGetTime();

	double acc = 0.0;
	for (unsigned int i = 0; i < loopCount; i++)
	{
		for (unsigned int j = 0; j < loopCount; j++)
		{
		    acc = (i * 3.5 + 123.1) / 65.7 + acc / 1000.9 + j;
		}
	}

	DWORD fooTwoEnd = timeGetTime();
	// (ページ下部の注参照)
	printf("Double time in fooTwo: %u\n", fooTwoEnd - fooTwoBegin);
	return acc;
}

int _tmain(int argc, _TCHAR* argv[])
{

	// 意味はないが一応C#と等価にするため
	fooOne(1);
	fooTwo(1);

	timeBeginPeriod(1); 

	// ここから本番
	DWORD begin = timeGetTime();
	unsigned int ui = fooOne(20000);
	DWORD endInt = timeGetTime();
	double d = fooTwo(20000);
	DWORD endDouble = timeGetTime();

	DWORD tsInt = endInt - begin;
	DWORD tsDouble = endDouble - endInt;

	printf("Integer time: %d\n", tsInt);
	printf("Integer value: %u\n", ui);
	printf("Double time: %u\n", tsDouble);
	printf("Double value: %f\n",  d);
	puts("Press Enter to Exit");
	getchar();

	return 0;
}


実行結果は以下のとおり。
なお、コンパイラオプション等はすべてデフォルトのまま。
C#は「コンソールアプリケーション」。C++は「Win32コンソールアプリケーション」(CLRコンソールアプリケーションではなく)で作成。
C++は、winmm.libをマルチメディアタイマ(timeBeginTime系)のために追加リンク。
シアン(水色)が、速かったタイム。
コーディング言語コンパイル環境ビルドモード実行環境結果(Integer time)結果(Double time)
C++VS 2010 on Win7 Ultimate x64 on Core i7DebugWin7 Ultimate x64 on Core i73983ms11274ms
C#VS 2010 on Win7 Ultimate x64 on Core i7Debug(x86)Win7 Ultimate x64 on Core i74469ms13734ms
C#VS 2010 on Win7 Ultimate x64 on Core i7Debug(AnyCPU)Win7 Ultimate x64 on Core i74469ms13734ms
C++VS 2010 on Win7 Ultimate x64 on Core i7ReleaseWin7 Ultimate x64 on Core i71019ms3995ms
C#VS 2010 on Win7 Ultimate x64 on Core i7Release(x86)Win7 Ultimate x64 on Core i73709ms12983ms
C#VS 2010 on Win7 Ultimate x64 on Core i7Release(AnyCPU)Win7 Ultimate x64 on Core i71014ms6583ms
C++VS 2008 on Win7 HomePremium x64 on SU2300DebugWin7 Ultimate x64 on Core i7実行不能実行不能
C#VS 2008 on Win7 HomePremium x64 on SU2300DebugWin7 Ultimate x64 on Core i72808ms9297ms
C++VS 2008 on Win7 HomePremium x64 on SU2300ReleaseWin7 Ultimate x64 on Core i71153ms3995ms
C#VS 2008 on Win7 HomePremium x64 on SU2300ReleaseWin7 Ultimate x64 on Core i71014ms6583ms
C++VS 2010 on Win7 Ultimate x64 on Core i7DebugWin7 HomePremium x64 on SU23009375ms24148ms
C#VS 2010 on Win7 Ultimate x64 on Core i7Debug(x86)Win7 HomePremium x64 on SU23009110ms43804ms
C#VS 2010 on Win7 Ultimate x64 on Core i7Debug(AnyCPU)Win7 HomePremium x64 on SU23009103ms28298ms
C++VS 2010 on Win7 Ultimate x64 on Core i7ReleaseWin7 HomePremium x64 on SU23002586ms9098ms
C#VS 2010 on Win7 Ultimate x64 on Core i7Release(x86)Win7 HomePremium x64 on SU23008361ms42497ms
C#VS 2010 on Win7 Ultimate x64 on Core i7Release(AnyCPU)Win7 HomePremium x64 on SU23002652ms16738ms
C++VS 2008 on Win7 HomePremium x64 on SU2300DebugWin7 HomePremium x64 on SU23008592ms23613ms
C#VS 2008 on Win7 HomePremium x64 on SU2300DebugWin7 HomePremium x64 on SU23006567ms20217ms
C++VS 2008 on Win7 HomePremium x64 on SU2300ReleaseWin7 HomePremium x64 on SU23003053ms9184ms
C#VS 2008 on Win7 HomePremium x64 on SU2300ReleaseWin7 HomePremium x64 on SU23002652ms16754ms

さすがにC++は全般的に速い、という感じ。
ただ、場合によってはC#が逆転しているケースも、特に整数系ではあります。
浮動小数点に関しては、この比較の限り、C++が圧勝ですが、整数だとC#が速かったり、あるいは遅れをとっていても肉薄している感じがあります。
なので「ヴァーチャルマシン系の処理系は一概に遅い」とは言いにくい状況です。
ただ3Dゲーム等で浮動小数点をゴリゴリやりたい場合は、まっとうな方法ではC++のほうが圧倒に速そうです。
業務システムにおいては浮動小数点(Double)よりも十進小数(Decimal)を用いるのですが、C#は組み込みの型でdecimalありますが、C++は組み込みとしてはありません(ですよね?)。
ここの比較もしたいのですが、C++だとWinAPIをがりがり呼ぶことになりそう。
VarDecDiv();とか。
こちらは、また時間があるときにも試してみます。

(注)
あと、C++のコードで、fooTwo関数の最後でprintfしているのはなぜか、といいますと。
VS 2008 で、Relaseモードで実行したとき、ここにprintfがないと、Double Timeが取れない(0)という現象が発生しました。
実行すると、Integer Time, Interger Valueが正しく表示され), そして Double Time が0で表示されて、しばらくしてからDouble Valueが表示されます。
おそらく、最適化の関係で、fooTwoの戻り値dを使う直前までを先に実行して、fooTwoの戻り値dを使うところではじめてfooTwoが呼び出される、というような改変が発生しているのではないかと思います。
かなりエゲツナイ最適化だな、と思った次第です。
これは、VS2010では発生しませんでした。なので上記表でvs2010でコンパイルした場合は、該当箇所をコメントアウトしています。

Top of Site
Copyright (c) 2010 Takao Tamura