none
ジェネリクスの制限やC#の方向性など RRS feed

  • 質問

  • ジェネリクスにポインタは指定できない という事で良いでしょうか?

    例えば以下のコードでは

    using System;
    
    namespace MyLibrary {
    
         unsafe sealed class Class0 {
    
            const int LEN_ = 10;
            
            void Check<T>(Action<T> f, T d){ f(d); }
            
            internal Class0(){
                double* d = stackalloc double[LEN_];
                Check<double*>(dp => { for (int i = 0; i<LEN_; ++i) dp[i] = i; }, d);
            }
    
        }
    
    }


    エラー    1    型 'double*' は、型引数に使用されない可能性があります。

    となり、コンパイルエラーです。

    「*」を「[]」に変え

    stackallocをnewに変えれば通りますが

    stackallocと

    new配列からのfixedでのポインタ使用

    だと

    結構処理速度に差が出てしまう感じです。

    (10000*10000のループで3.6秒対2.6秒程度)


    (というよりまぁ、やっぱり多大な数値計算とかは、現状ではC#でやるのではなく、C++/CLIやネイティブC++に委託した方が良いというのは鉄板、ですよね?)

    ただこれについては、.NET Frameworkのバージョンや、OSや、64bitか32bitかとかでも速度はだいぶ変わってくるものなのでしょうか?


    その他、ジェネリクスに指定できる型の制限はありましたでしょうか?


    また、C#のラムダ式(デリゲート)・ジェネリクスとC++11のラムダ式(関数オブジェクト)・テンプレート

    では、やはり速度面では、結構開きがあるだろうと考えていいでしょうか?

    • 編集済み mr.setup 2012年3月9日 19:23
    2012年3月9日 19:18

回答

  • Generics とstackallocを組み合わせる意味は殆どないのではないかというのが個人的な見解だったりします。

    ソモソモC#の Generics は一緒にみなせる物を一緒にみなしてしまう事で汎用なコードを書けるようにするのが目的な技術なので、最適化のかかりも最大限の最適化に比べて今一ですし性能が必要ならそれを妥協しない書き方をしなければならないでしょうね。

    計測されている通りで良くて1.5倍から悪ければ数十倍の性能差が出てしまうのでホットスポットのチューニングに関してはC++等のネイティブコンパイラ/ないしはSIMD命令等を使ってアセンブラで行う事をお勧めします。

    まぁ、ホットスポット以外のそれほど性能要件の厳しくない所で手数がかかる所にそんなに性能が問題とならない形で実装を供給する仕組みとしてのGenericsはそれほど線の悪い技術とは思いますが求めてる要件が違うから目的の用法に対しては無力というより事情を悪化させるだけなので拘らない方が良いと思います。


    Kazuhiko Kikuchi

    • 回答としてマーク mr.setup 2012年3月10日 9:23
    2012年3月10日 8:36
  • 意図通りです。通常のコーディングならDLLなどのロード時にバインディングが解決されますが、MakeGenericMethod()を使って実行時に同的に指定することもできます。その際には型パラメーターの制約を満たしていることが必要な点も変わりません。
    • 回答としてマーク mr.setup 2012年3月10日 10:36
    2012年3月10日 9:39
  • 質問に対する直接的な回答ではありませんが…。

    「+」とは何をするものでしょうか? a + bとあったとき、左辺値・右辺値が数値なら加算を行うコードが生成されます。a + b + c とあったときに1つでもstring型ならString.Concat( a, b, c )を呼び出すコードが生成されます。これらの動作は、コンパイル時にコンパイラーがデータ型を判断した上で、生成するコードが決定されます。

    それに対してジェネリックは、実行時に型引数が決定されます。つまりコンパイル時にはデータ型が不定です。するとどうなるでしょう。コンパイラーは前段に挙げたような判断を出来ませんから、コンパイルエラーになります。

    • 回答としてマーク mr.setup 2012年3月10日 16:09
    2012年3月10日 14:34
  • ふと思い出しましたが、dynamicがそれに近いですかね。

    dynamic a, b, c;
    
    a = 1;
    b = 2;
    c = a + b;
    Console.WriteLine(c);
    
    a = Guid.NewGuid();
    b = " ヾ(''*";
    c = a + b;
    Console.WriteLine(c);
    • 回答としてマーク mr.setup 2012年3月11日 2:30
    2012年3月10日 22:08
  • それを考えると、Genericsはどうしても動的な型の解決が必要になるというシステム、だと思うので

    ここは、勘違いされていそうです。

    Genericsは与えられた型がパラメータ型として静的に解決できないとコンパイル時にも実行時にもエラーになります。このため、なんらかの制約をかけないとGenerics型からメソッドを呼び出したりできません。

    この解決策として、制約を与えないで動的な呼び出しを行うという方法をとることはできますが、それはGenericsに関係ない話ですね。(非Generics型であっても型情報を無視してメソッドを呼び出すために動的に解決を行う必要があるのは同じなので)

    あとは、命令レベルのチューニングを行わないプログラミング言語のレベルのロジックの場合、C/C++ と C#やJava に演算速度面に差異はないと言われていることが多いです。(むしろ、C#/Javaのほうが高速になる可能性があるとも言われています) 実際に、単純な演算処理で C/C++ に対して劣るほどの処理速度差がでることはほぼないと思います。

    しかし、I/O 面は実用に耐えないぐらいの致命的な差がでるような分野があるのが現状ですね。書かれているようなメモリI/Oであれば差の程度はそこそこたかが知れていますが、ハードウェアやネットワークの I/O コストは C/C++ と比べるとかなり厳しいオーダーで影響が出ることがあります。

    そんなかんじなので、

    > というよりまぁ、やっぱり多大な数値計算とかは、現状ではC#でやるのではなく、
    > C++/CLIやネイティブC++に委託した方が良いというのは鉄板、ですよね?

    というところは、「多大な計算」よりも、計算に伴って発生する「多大なI/O」のほうがネックであって、計算そのものはC#で書いても十分ではないかな?と個人的には思っています。

    あとは、単純に多大な計算を行うだけなら、並列化や記述性の面からも評価してもよいかと思います。そっち方面は、C/C++ の並列化ライブラリと比べるとだいぶ楽になっていると思いますよ。

    • 回答としてマーク mr.setup 2012年3月12日 15:22
    2012年3月12日 14:46
  • と、結構な開きがあります。

    意味的にはほとんど同じコードですよね?(しかも良く見るとC++のが行数少ない・・・w これは単純な演算で、後始末とかの必要がないからこそ、ですが)

    なので、実測値やC++側の出力アセンブリの最適化具合、C#の仕様などから判断すると

    純粋に、演算部分ではC++が優勢なのはやっぱり結構あると思います。

    Environment.TickCountね。

    その点はまっさきにkazukさんが答えられています。私もC++が優勢と考えています。

    計測されている通りで良くて1.5倍から悪ければ数十倍の性能差が出てしまうのでホットスポットのチューニングに関してはC++等のネイティブコンパイラ/ないしはSIMD命令等を使ってアセンブラで行う事をお勧めします。
    • 回答としてマーク mr.setup 2012年3月13日 1:37
    2012年3月12日 21:19
  • 挙げられているコードを実行してみましたが、C++ 156ms、C# 188msでした。

    C++の方は昔ながらのx87コードでした。C#の方は.NET 4.5 betaが入っているおかげかSSE2が使われていました。それでもC#は負けてます。

    CPUはCore i7-2600Sです。

    • 回答としてマーク mr.setup 2012年3月13日 3:22
    2012年3月13日 2:44
  • まずは[オプション] ダイアログ ボックス - [デバッグ]の中から[モジュールの読み込み中に JIT 最適化を抑制する (マネージのみ)]のチェックを外す必要があります。
    後はデバッガ上で[逆アセンブル] ウィンドウを開けば参照できます。

    # C# / .NETはC++のように.asmを出力できるわけではありません。実行時にコード生成するので当然ですが…。

    • 回答としてマーク mr.setup 2012年3月13日 5:57
    2012年3月13日 5:07

すべての返信

  • Generics とstackallocを組み合わせる意味は殆どないのではないかというのが個人的な見解だったりします。

    ソモソモC#の Generics は一緒にみなせる物を一緒にみなしてしまう事で汎用なコードを書けるようにするのが目的な技術なので、最適化のかかりも最大限の最適化に比べて今一ですし性能が必要ならそれを妥協しない書き方をしなければならないでしょうね。

    計測されている通りで良くて1.5倍から悪ければ数十倍の性能差が出てしまうのでホットスポットのチューニングに関してはC++等のネイティブコンパイラ/ないしはSIMD命令等を使ってアセンブラで行う事をお勧めします。

    まぁ、ホットスポット以外のそれほど性能要件の厳しくない所で手数がかかる所にそんなに性能が問題とならない形で実装を供給する仕組みとしてのGenericsはそれほど線の悪い技術とは思いますが求めてる要件が違うから目的の用法に対しては無力というより事情を悪化させるだけなので拘らない方が良いと思います。


    Kazuhiko Kikuchi

    • 回答としてマーク mr.setup 2012年3月10日 9:23
    2012年3月10日 8:36
  • 回答ありがとうございます♪


    やはりそうですよね。(「意味的にはほとんど同じコード」では、20倍程度までは確認できました)

    C++テンプレートの用に

    genericも先に型ごとにコンパイル出来ないものだろうかと思いましたが

    よくよく考えてみると、たとえinternalやprivateであっても

    リフレクションとバインディングフラグの併用などで無理やり別アセンブリからのぞき見ることが可能なので

    それを考えると、Genericsはどうしても動的な型の解決が必要になるというシステム、だと思うので

    その通りなら、「なるほど速度の話であれば無理ぽ」ですね。

    (C#の言語仕様はC++より遥かにインテリセンス向きですし、ジェネリクスは実行時速度は犠牲になるけど、かわりにコンパイルが速くなる・実行ファイルサイズはテンプレートのように膨張しない、といったメリットはあります。やはり、基本的には生産性重視でガンガン書けるって部分に強いって事ですね。)


    ただ、そう思って気になったのですが

    私はC#はまだ触り始めて2週間かそこらぐらいなので、Genericとdll間のリフレクションが絡む場合とかちゃんと理解できてないかもしれないので、よろしければ以下のコード、特にMakeGenericMethodあたりが、これで正しいかどうか教えていただけませんでしょうか?

    (もちろん、internalやprivateなものは、テストとかでもよっぽどのことがない限り、ましてやリリース段階では別アセンブリからは触ることはほとんどないとは思いますので、「実験」的なところが強いです。また、実験なので例外チェックとかは省いてあります。)


    C++/CLIサイド(別途dll)

    namespace MySampleLibrary { ref class MyInternalClass1; }
    
    ref class MySampleLibrary::MyInternalClass1 {
    	generic < class T > 
    	System::String^ Func( T a, T b ){
    		return a->ToString() + b->ToString();
    	}
    };


    C#

    using System;
    using System.Reflection;
    
    namespace ConsoleApplication1 {
       class Program {
    
           static void Main(string[] args) {
                
                Assembly asm = Assembly.Load("MySampleLibrary");
                Type myType = asm.GetType("MySampleLibrary.MyInternalClass1");
                MethodInfo myMethod = myType.GetMethod("Func",
                    BindingFlags.NonPublic | BindingFlags.Instance);
    
                object obj = Activator.CreateInstance(myType);
    
                var arg = new object[]{ 5, 10 };
                var Func = myMethod.MakeGenericMethod(new Type[] { typeof(int) });
    
                object ret = Func.Invoke(obj, arg);
                Console.WriteLine(myType.ToString() + " " + ret.ToString());
                Console.Read();
    
            }
    
        }
    }


    実行結果

    MySampleLibrary.MyInternalClass1 510

    (結果だけ見ると意図どおりっぽいですね。・・・う~ん、でもやっぱり別dllもメインのプログラムも自分で作る分なら必要ないかなぁ)

    • 編集済み mr.setup 2012年3月10日 9:37
    2012年3月10日 9:23
  • 意図通りです。通常のコーディングならDLLなどのロード時にバインディングが解決されますが、MakeGenericMethod()を使って実行時に同的に指定することもできます。その際には型パラメーターの制約を満たしていることが必要な点も変わりません。
    • 回答としてマーク mr.setup 2012年3月10日 10:36
    2012年3月10日 9:39
  • おお、あってましたか

    どうもありがとうございます♪


    あーそうそうそう型パラメーターの制約で思い出したんですが

    C#だとこんな感じで書けますよね

    System.String Func2<T>(T a, T b) where T : struct, IConvertible {


    で、同じようなことをC++/CLIでやろうとしてさっぱりわからなかったのですが

    色々調べて試してみたら以下のようにして「何とかコンパイルは通りました」


    namespace MySampleLibrary { ref class MyInternalClass1; } ref class MySampleLibrary::MyInternalClass1 { //色々面倒なので実装はソースに移動 generic < class T > where T : ref struct, System::IConvertible System::String^ Func( T a, T b ); };


    こんなやば気な形が正しいんですかね?w(C++/CLI)

    (なかなかstruct →ref structが分かんなかったのです。←て正しいのか・・・?)

    そんで、中身としては例えば

    T Func<T>( T a, T b ){
            return a+b;
    }

    こんな感じの事をやってみたいのですが

    どうもジェネリクスと値型の相性が悪いっぽい感じで


    //ソース
    using namespace System;
    using MySampleLibrary::MyInternalClass1;
    
    generic < class T > where T : ref struct, IConvertible
    String^ MyInternalClass1::Func( const T a, const T b ){
    	switch (a->GetTypeCode()){
    		case TypeCode::Int32: 	
    			return "" + (((IConvertible^)a)->ToInt32(nullptr) + ((IConvertible^)b)->ToInt32(nullptr));
    		default: return ""; 
    	}
    }


    ようやくこれで通ったっぽいです。

    しかし現実はさらに上で

    object[] arg = new object[] { 5, 10 };
    var func = myMethod.MakeGenericMethod(new Type[] { typeof(int) });
    
    object ret = func.Invoke(obj, arg);
    Text = myType.ToString() + " " + ret.ToString();


    の 「Invokeのためにobject[]にすべき」と「int[]にしたい」という狭間で死亡してしまいますw

    やっぱり「万が一」動的ロードでごしゃごしゃやりたい場合は値型はやめる方向で(あるいは演算とかはデリゲートで渡す?)行った方が良いですかね?

    (ていうか、そんな変にマニアックなことせずにフツーなコーディングでやりゃ全然出来るし、そのほうが断然良いとは思いますがw この辺は知識として「もしもっとまともな方法で可能なのであればしっておきたい」という興味本位です。)


    • 編集済み mr.setup 2012年3月10日 10:43
    2012年3月10日 10:35
  • 質問に対する直接的な回答ではありませんが…。

    「+」とは何をするものでしょうか? a + bとあったとき、左辺値・右辺値が数値なら加算を行うコードが生成されます。a + b + c とあったときに1つでもstring型ならString.Concat( a, b, c )を呼び出すコードが生成されます。これらの動作は、コンパイル時にコンパイラーがデータ型を判断した上で、生成するコードが決定されます。

    それに対してジェネリックは、実行時に型引数が決定されます。つまりコンパイル時にはデータ型が不定です。するとどうなるでしょう。コンパイラーは前段に挙げたような判断を出来ませんから、コンパイルエラーになります。

    • 回答としてマーク mr.setup 2012年3月10日 16:09
    2012年3月10日 14:34
  • 重ねてどうもありがとうございます♪

    ああ、そうかw

    where T : ref struct, System::IConvertible

    (仕様上の壁以前に、意味的に考えて)この縛りじゃ不足ですね。


    あれ?でもこの状況、「TがString」っていうのは出来る方法ありましたっけ?


    C#だと

            T Func2<T>( T a ) where T : struct, IConvertible {
                return a;
            }


    //で、単純にどっかで

    Func2("");


    というコードは不可能ですよね?


    デフォルトであるやつで値型に該当するのは

    Byte

    Short

    Integer

    Long

    Single

    Double

    Char

    Decimal

    Boolean

    Date

    こんなもんでしょうか


    出来れば「最悪でもDecimalかDoubleあたりで手を打ってほしい」

    あるいは

    switch (a->GetTypeCode()){

    以下のcase内では単純に「+」が使えてほしいかも

    という「願望」はあったりなかったりしますがw


    しっかり考えてみると

    where T : struct

    としたところで、やはりそれだけではいきなり+演算子とか使うのは無理なよう(?)ですね。

    やってみたいと思ったことは、正常に動作するコードに整理してみるとこんな感じです(String^をreturnしてますが)

    generic < class T > where T : IConvertible //ref struct指定をやめる
    String^ MyInternalClass1::Func( const T a, const T b ){
    	switch (a->GetTypeCode()){
    		case TypeCode::Int32: 				
    			return ( Convert::ToInt32( a ) + Convert::ToInt32( b ) ) + "";
    		default: return ""; 
    	}
    }

    ほんとはTypeCode::Int32のときにintかInt32を返すことが可能ならそういうのも知ってみたいですが、どうすればいいのやらw

    (予感としては、そこもやっぱり値型直にじゃなくて、戻り値Objectにしちゃってgcnew Int32(~);とかが妥当な線ですかね?)

    これならC#サイドで

    object[] arg = new object[] { 5, 10 };
    //その他さっきと同じ
    Console.WriteLine( myType.ToString() + " " + ret.ToString() );

    結果

    MySampleLibrary.MyInternalClass1 15

    15 = 5 + 10 的な感じですね

    • 編集済み mr.setup 2012年3月10日 16:20
    2012年3月10日 16:08
  • ふと思い出しましたが、dynamicがそれに近いですかね。

    dynamic a, b, c;
    
    a = 1;
    b = 2;
    c = a + b;
    Console.WriteLine(c);
    
    a = Guid.NewGuid();
    b = " ヾ(''*";
    c = a + b;
    Console.WriteLine(c);
    • 回答としてマーク mr.setup 2012年3月11日 2:30
    2012年3月10日 22:08
  • あ、そう言えばそんなものがありましたね。(5日ほど前に説明ページだけ見たのですがまだまったく試してなくて、すっかり忘れてました)

    ありがとうございます♪

    いやはや、勉強になります


    とりあえず、dynamicを

    使うためにはMicrosoft.CSharp.dllへの参照が必要とのことで、追加したかったのですが

    場所が分からなかったので適当に探していたらこちらのパスにて確認できました。

    Where is Microsoft.CSharp Anyway?  << Paul Galvin's SharePoint Space


    そして

    佐祐理さんご紹介のMSDNのページと

    岩永さんの++C++;んとこの

    [雑記] 動的コード生成のパフォーマンス


    などを参照しながら、大よその現状を把握

    あとはこちらもコードを書いて試してみました。

    (ただしC#から別C#のDLLを呼び出す、という形で)


    総括としてはこんな感じでも良さ気(可能)ってとこでしょうかね?

    呼び出されるdllは

    using System;
    
    namespace ClassLibrary1 {
        static class Class1 {
            static dynamic Func3(dynamic a, dynamic b) {
                switch ((TypeCode)a.GetTypeCode()) {
                    case TypeCode.Boolean: return a|b;
                    case TypeCode.String: return a + "`*:;,.★ ~☆・:.,;*" + b;
                    case TypeCode.Int32: return a*2 + b;
                    default: return a + b;
                }
            }
        }
    }


    呼び出し側

    using System;
    using System.Reflection;
    
    namespace ConsoleApplication1 {
        class Program {
    
            delegate dynamic AddDyCallback(dynamic a, dynamic b);
    
            static void Main(string[] args) {
    
                Assembly asm = Assembly.Load("ClassLibrary1");
                Type myType = asm.GetType("ClassLibrary1.Class1");
                MethodInfo myMethod = myType.GetMethod("Func3",
                    BindingFlags.NonPublic | BindingFlags.Static );
                
                AddDyCallback func3 = 
                    (AddDyCallback)Delegate.CreateDelegate(typeof(AddDyCallback), myMethod);
    
                Console.WriteLine(func3(3, 1.77));
                Console.WriteLine(func3(false, true));
                Console.WriteLine(func3(1, 0));
                Console.WriteLine(func3("・:*:・°★,。・:*:・°☆ ", " ヾ(''*"));
                Console.Read();
    
            }
        }
    }


    そうすると実行結果は

    7.77

    True

    2

    ・:*:・°★,。・:*:・°☆ `*:;,.★ ~☆・:.,;* ヾ(''*


    C++/CLIでは流石に今のところdynamicは無理ですかね~(内部的に同じことやるならcallSite使用?)

    まぁ、たぶん困りはしないだろうけどもw

    ただ、このdynamic、使うとものすごくシンプルに書けますね~。素晴らしい


    2週間程度で言うのもあれかもしれませんが、一応C#は2.0あたりから順番に学んできたので、敢えて言わせていただくと

    4.0になってまた色々と強くなりましたねぇ、C#

    • 編集済み mr.setup 2012年3月11日 6:26
    2012年3月11日 2:30
  • それを考えると、Genericsはどうしても動的な型の解決が必要になるというシステム、だと思うので

    ここは、勘違いされていそうです。

    Genericsは与えられた型がパラメータ型として静的に解決できないとコンパイル時にも実行時にもエラーになります。このため、なんらかの制約をかけないとGenerics型からメソッドを呼び出したりできません。

    この解決策として、制約を与えないで動的な呼び出しを行うという方法をとることはできますが、それはGenericsに関係ない話ですね。(非Generics型であっても型情報を無視してメソッドを呼び出すために動的に解決を行う必要があるのは同じなので)

    あとは、命令レベルのチューニングを行わないプログラミング言語のレベルのロジックの場合、C/C++ と C#やJava に演算速度面に差異はないと言われていることが多いです。(むしろ、C#/Javaのほうが高速になる可能性があるとも言われています) 実際に、単純な演算処理で C/C++ に対して劣るほどの処理速度差がでることはほぼないと思います。

    しかし、I/O 面は実用に耐えないぐらいの致命的な差がでるような分野があるのが現状ですね。書かれているようなメモリI/Oであれば差の程度はそこそこたかが知れていますが、ハードウェアやネットワークの I/O コストは C/C++ と比べるとかなり厳しいオーダーで影響が出ることがあります。

    そんなかんじなので、

    > というよりまぁ、やっぱり多大な数値計算とかは、現状ではC#でやるのではなく、
    > C++/CLIやネイティブC++に委託した方が良いというのは鉄板、ですよね?

    というところは、「多大な計算」よりも、計算に伴って発生する「多大なI/O」のほうがネックであって、計算そのものはC#で書いても十分ではないかな?と個人的には思っています。

    あとは、単純に多大な計算を行うだけなら、並列化や記述性の面からも評価してもよいかと思います。そっち方面は、C/C++ の並列化ライブラリと比べるとだいぶ楽になっていると思いますよ。

    • 回答としてマーク mr.setup 2012年3月12日 15:22
    2012年3月12日 14:46
  • ご回答ありがとうございます♪


    >Genericsは与えられた型がパラメータ型として静的に解決できないとコンパイル時にも実行時にもエラーになります。このため、なんらかの制約をかけないとGenerics型からメソッドを呼び出したりできません。


    あ~、確かにあの時点ではそんなに深く意識してなかったですw

    (ここまでの内容を振り返れば明白になっているように)

    はい、そのとおりですね。

    ただ、強く意識してなかったです・・・が

    確か頭にあったのは「C++のテンプレートと比較して」って部分だったと思います。


    あれは型ごとにコードが作られて、しかも状況によってインライン化までしてくれますからね。

    関数オブジェクトと組み合わせることで「多大な計算」でも型の自由が効きながら、しかも型ごとにポインタ用意してやったのと同じ事まであっさりと出来ちゃいます。

    あれと比べれば「動的解決は少なからず必要」で、ジェネリクスとデリゲートなどを使うと、大差になってしまうのは否めないと思います。(実測値で5~20倍程度になるコードは確認)


    それ以外の「多大な計算」のみに絞るために

    ジェネリクスやデリゲート・関数オブジェクト、テンプレートなどを使わずに計測してみますと


    ネイティブC++

    #include <stdio.h>
    #include <windows.h>
    
    int main(void){	
    	
    	const volatile DWORD be = GetTickCount(); //念のためのvolatile
    
    	const int LEN_ = 20000;
    	double d = 0;
    	for (int i=0; i<LEN_; ++i){
    		for (int j=0; j<LEN_; ++j){
    			d = (i*3 + 123)/65 + d/1000 + j; 
    		}
    	}
    
    	const volatile DWORD af = GetTickCount();
    	
    	printf("time(ms): %u\n", af-be );	
    	printf("%.10f\n", d );
    
    	getchar();
    	return 0;
    
    }

    Precise (/fp:precise)での結果

    time(ms): 5250
    20943.9429419409



    Fast (/fp:fast)での結果

    time(ms): 2250
    20943.9429419409


    C#


    using System;
    using System.Runtime.InteropServices;
    
    namespace ConsoleApplication1 {
    
        class Program {
    
            [DllImport("kernel32.dll", CallingConvention = CallingConvention.StdCall)]
            internal extern static uint GetTickCount();
    
           static void Main(string[] args) {
    
               uint be = GetTickCount();
               
               const int LEN_ = 20000;
               double d = 0;
               for (int i=0; i<LEN_; ++i){
                   for (int j=0; j<LEN_; ++j){
                       d = (i*3 + 123)/65 + d/1000 + j; 
                   }
               }
    
               uint af = GetTickCount();
               
               Console.WriteLine("time(ms): " + ( af - be ) );
               Console.WriteLine(d);
               Console.Read();
    
            }
        }
    }

    time(ms): 11015

    20943.9429419409

    と、結構な開きがあります。

    意味的にはほとんど同じコードですよね?(しかも良く見るとC++のが行数少ない・・・w これは単純な演算で、後始末とかの必要がないからこそ、ですが)

    おそらく演算部の変数についてはどっちもスタック上の話になるので、I/Oの影響はないと思います。

    また、ポインタを使いだすとfixedなどでの固定の必要ないネイティブC++はさらに有利になるのと

    並列性もOpenMPとか使えば結構簡単になると思いますし、GPU(&ビデオメモリ)とのやりとりとかも結構ダイレクトに行けると思います。

    なので、実測値やC++側の出力アセンブリの最適化具合、C#の仕様などから判断すると

    純粋に、演算部分ではC++が優勢なのはやっぱり結構あると思います。

    (あとローカル変数とか引数とかポインタ・参照・メンバ関数に付けれるconstは可読性上がって好きですね)

    純粋に、記述性の面では、そう言う部分じゃない「GUI的なとことか」はC#の方が楽だと思いますね♪(同じように.NETバリバリに使おうとすると、C++/CLIは結構見づらくなりやすいですし)

    • 回答としてマーク mr.setup 2012年3月12日 15:22
    • 回答としてマークされていない mr.setup 2012年3月13日 0:32
    • 編集済み mr.setup 2012年3月13日 1:39
    2012年3月12日 15:18
  • と、結構な開きがあります。

    意味的にはほとんど同じコードですよね?(しかも良く見るとC++のが行数少ない・・・w これは単純な演算で、後始末とかの必要がないからこそ、ですが)

    なので、実測値やC++側の出力アセンブリの最適化具合、C#の仕様などから判断すると

    純粋に、演算部分ではC++が優勢なのはやっぱり結構あると思います。

    Environment.TickCountね。

    その点はまっさきにkazukさんが答えられています。私もC++が優勢と考えています。

    計測されている通りで良くて1.5倍から悪ければ数十倍の性能差が出てしまうのでホットスポットのチューニングに関してはC++等のネイティブコンパイラ/ないしはSIMD命令等を使ってアセンブラで行う事をお勧めします。
    • 回答としてマーク mr.setup 2012年3月13日 1:37
    2012年3月12日 21:19
  • ありがとうございますw

    >その点はまっさきにkazukさんが答えられています。私もC++が優勢と考えています。

    はい、そうですね。(一応コード込みでの実験結果を実測値付きで明記すると説得力がより増すと思ったので、そうしてみた次第です)

    とりあえずは、ノーマルなC++の範囲に絞り

    SIMD命令・インラインアセンブラなどの直書きはやめて、設定だけいじっといてコンパイラの裁量に任せる方針で行くことにします。


    Environment.TickCountね。

    DateTime.NowとQueryPerformanceCounterだと結構差があった気がしたので、公平を期すためにまったく同じ条件のAPI呼び出しにしようと考えたのですが

    なるほど

    試してみたところ、GetTickCountの場合はEnvironment.TickCountで特にコストに差がないんですねw

    (これらは精度には若干の難ありですが、上ほどの差ならば、それは誤差でありましょう)

    そしたら上の書式でも後3、4行は縮まって、行数互角になりますね。


    と・こ・ろ・が


    なんと「ポインタ(アドレス)操作」「ループ」「浮動小数の足し算」

    といった操作だけで、

    かなり限定された状況でですが、こちらの環境にてC#がC++を一歩上回る、(個人的には)興味深い状況が発生しました。


    少々長くなりますので興味のない方は↓はスルーでもかまいませんが


    実装方法に開きがあるとフェアじゃないので

    標準ライブラリとかは計測部分では極力使わず

    片方向のシンプルな(機能は最小限の)連結リストを自分の手で作ってみました。ただし要素の追加は先頭または末尾を選べます。


    VC++2010

    #include <stdio.h>
    #include <windows.h>
    
    #define null nullptr
    
    template < class T >
    class MyList {
    
    	typedef struct NODE { 
    		NODE* next;
    		T data; 
    		NODE( NODE* next_, T data_ ) : next(next_), data( data_ ){}
    	} *PTR;
    
    	PTR first, *lastpp;
    		
    public:
    	
    	void Release(){ for ( PTR p; p = first; delete p ) first = p->next; lastpp = &first;  }
    
    	MyList() : first(null), lastpp(&first) {}
    	~MyList() { Release(); }
    
    	void AddLast( T data_ ){ 
    		*lastpp = new NODE( null, data_ );
    		lastpp = &(*lastpp)->next;
    	}
    
    	void AddFirst( T data_ ){ first = new NODE( first, data_ ); }
    
    	template < class F >
    	void ForEach( const F& f ) const { 
    		for ( PTR p = first; p; p = p->next) f(p->data);
    	}
    
    };
    
    int main(void){	
    
    	auto list = new MyList<double>;
    	const volatile DWORD be = GetTickCount();
    
    	for ( int i=0; i<10000; ++i ) list->AddFirst( i );
    	double d = 0;
    	for ( int i=0; i<10000; ++i ) list->ForEach( [&d](double j){ d+=j; } );
    
    	const volatile DWORD af = GetTickCount();
    	
    	delete list;
    
    	printf("time(ms): %u\n", af-be );	
    	printf("%f\n", d );
    	getchar();
    	return 0;
    
    }

    56行。

    今回は浮動小数の設定やSIMD命令の設定などでは変化しませんでした。

    time(ms) : 515

    499950000000.000000


    VC#2010(LinkedListを参考に)

    using System;
    
    namespace ConsoleApplication1 {
        class Program {
    
            class MyList<T> {          
                  
                internal class Node {
                    internal Node Next { get; set; }
                    internal T Value { get; set; }
                    internal Node(Node next_, T t) { Next = next_; Value = t;  }
                }
    
                internal Node First { get; set; }
                internal Node Last { get; set; }
    
                internal MyList() { Last = First = null; }
                internal void AddLast(T t) {
                    Node p = new Node(null, t);
                    if (Last != null) {
                        Last.Next = p;
                        Last = Last.Next;
                    } else Last = First = p;
                }
    
                internal void AddFirst(T t) {
                    Node p = new Node(First, t);
                    if (First == null) Last = p;
                    First = p;
                }
    
            }       
    
            static void Main(){
    
                var list = new MyList<double>();
                int be = Environment.TickCount;
    
                for (int i = 0; i < 10000; ++i) list.AddFirst(i);            
                double d = 0;
                for (int i = 0; i < 10000; ++i) {
                    for (var p = list.First; p != null; p = p.Next) { 
                        d += p.Value; 
                    }
                }
    
                int af = Environment.TickCount;   
    
                Console.WriteLine("time(ms): " + (af - be));
                Console.WriteLine(d);
                Console.Read();
    
            }
    
        }
    }


    56行。

    time(ms) : 485

    499950000000



    なお、両方ともループで追加するときに、AddFirstでなくAddLastの側にすると

    422ms程度で釣り合います。


    思い当たる節はキャッシュラインへの読み込み方針(CPU依存)と、繋ぎ合わさったポインタ、あるいは参照のアドレスの関係です。

    new のたびにアドレスが大きい方向へ動いていくと考えると

    AddFirstで追加した後、それを順方向にたどっていくと

    アドレスが小さい方向へ遷移していくことになります。

    これが、俺んちのCPUタンは若干大きい方向へ遷移するのより苦手とするようです。


    そしてこの時、ガベージコレクタが働くマネージドなC#では、何らかの有利となるアドレスに対する内部処理、キャッシュへの配慮か何かが発生している可能性があるのではないかと思います。

    Native C++の場合はプログラマの書かないアドレスの勝手な移動は出来ません。

    またC++の方だけtemplateとラムダ式になってますが、C++の場合は最適化でほぼ同じ結果になるので問題ありません。


    そこは、もし

    internal void ForEach(Action<T> f) {
         for (var p = First; p != null; p = p.Next) f(p.Value);
    }

    などのように、C#サイドがデリゲートを使用すると3対1ぐらいでC++が圧勝となってしまいます。

    なので、C#が勝てる状況にするためには、FirstやNextを隠蔽して、処理だけ渡すという、オブジェクト指向的な手法に頼れない、ということになっています。


    また、もしお互いに


              internal void AAAAA() {
                    Node[] aaaa = new Node[10000]; //10000直打ちは手抜きw
                    int i = 0;
                    for (Node p = First; p != null; p = p.Next) {
                        aaaa[i++] = p;
                    }
                    First = null; Last = aaaa[0];
                    for (i = 0; i < 10000; ++i) {
                        aaaa[i].Next = First;
                        First = aaaa[i];
                    }
                }     

    といったメンバを追加し

    for (int i = 0; i < 10000; ++i) list.AddFirst(i);     

    等の直後に

    list.AAAAA();

    などと呼び出せば、ポインタあるいは参照が逆順に総付け替えになり、AddLastの場合と同じ理屈で

    速度的に釣り合います。(アドレスが減少方向になる走査回数は1回で済むため、ほとんどマネージドの優位性はなくなる)

    現実的には、デリゲートが使えない、Firstなどを隠蔽できないというのは結構辛いので

    同程度の速度でどっちが使いやすいか・汎用的が高いか・拡張性が高いか、と言ったら、解放の事を考慮してやる必要があることを差し引いてもC++の方がこの場合は楽そうですね。

    (色々一般論と逆転してるw)


    • 編集済み mr.setup 2012年3月13日 3:15
    2012年3月13日 1:37
  • 挙げられているコードを実行してみましたが、C++ 156ms、C# 188msでした。

    C++の方は昔ながらのx87コードでした。C#の方は.NET 4.5 betaが入っているおかげかSSE2が使われていました。それでもC#は負けてます。

    CPUはCore i7-2600Sです。

    • 回答としてマーク mr.setup 2012年3月13日 3:22
    2012年3月13日 2:44
  • おお、やはりまさに「環境依存」ですねw
    ご報告ありがとうございます♪

    質問文にもちらっとそれっぽい事を書きましたが

    CPU含めて環境による差異はやっぱりあるのかどうかとか、知りたかった事なので助かります。

    こっちのCPUは

    Athlon64 X2 の4200+

    ですね。

    このコードでAddLastでもAddFirstでも両方でC++が勝つとしたら

    例えばCore i7-2600Sの場合

    ではC#がC++に勝つのは至難の業なんでしょうかね。(上記コードでC++が勝つケース、C#が勝つケース両方があることが分かっただけでも十分ですけど)

    ※C++のRelease関数の最後に lastpp = &first;を付け足すの忘れてましたので、書きくわえておきました。これやんないとAddLast→Release→AddLastの三手一組でリークしちゃいますからね。・・・ここまで横にのびると改行したくなりますが、せっかくなので56行と56行のままで (改行してもtemplate<class T> class MyList {など、templete関連を一行にするとか言ったことも考えられますが)


    って

    >C#の方は.NET 4.5 betaが入っているおかげかSSE2が使われていました。

    VC#の使い方がまだちゃんと分かってなくて

    コンパイル結果の確認方法(機械語やアセンブリ)が分かんないので、C#との比較では計測にしか頼れなかったのですが、よろしければやり方教えていただけませんか?

    • 編集済み mr.setup 2012年3月13日 3:49
    2012年3月13日 3:22
  • まずは[オプション] ダイアログ ボックス - [デバッグ]の中から[モジュールの読み込み中に JIT 最適化を抑制する (マネージのみ)]のチェックを外す必要があります。
    後はデバッガ上で[逆アセンブル] ウィンドウを開けば参照できます。

    # C# / .NETはC++のように.asmを出力できるわけではありません。実行時にコード生成するので当然ですが…。

    • 回答としてマーク mr.setup 2012年3月13日 5:57
    2012年3月13日 5:07
  • ご丁寧にありがとうございます♪

    ># C# / .NETはC++のように.asmを出力できるわけではありません。実行時にコード生成するので当然ですが…。

    やはり、ですね

    「もしかしたらある程度はなってて、残りの部分だけが環境に応じて変わったりするのかな?」とか一瞬思いましたが、やっぱり、全般にわたりしっかり実行時にコード生成するんですね。


    チェックを外すまでは行けたのですが、なんと逆アセンブルウインドウはExpressにはついてないとのことでw

    2010自体は1週間ほど前にC++、C#ともにどんな感じで改善されてるか確かめようと思った分なので、現状まだEEなのです。

    年内ぐらいに11が出る予定、ってことで迷いどころではありますが

    やはり製品版は欲しいし、親孝行もしたいし、助けたい人(たち)もいるので、やっぱり急いで動かないとな、と改めて思いました


    かゆい所に手が届くような突っ込みやアドバイスをいただけるので、佐祐理さんとのやりとりは特に楽しく、ついついのめり込んでしまいましたがw

    だいぶ必要だった情報がそろってきたことですし、長年の闘いに決着をつけるべく

    一仕事集中して来ますね。

    (またのめりこんじゃうとアレなんで、せめて最低でも数日程度は投稿などを自制しようと思いますw 結構難しいことやらないといけないので、実際にはもうちょいかかるかも知れませんが)


    それでは、重ねてありがとうございました♪

    • 編集済み mr.setup 2012年3月13日 5:58
    2012年3月13日 5:56