10

デリゲートをC#の関数に渡す3つの異なる方法(ラムダ、デリゲート、直接参照)を比較しようとしました。私が本当に驚いたのは、直接参照の方法でした(つまりComputeStringFunctionViaFunc(object[i].ToString))、他の方法よりも6倍遅くなりました。これがなぜであるかを誰かが知っていますか?

完全なコードは次のとおりです。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.CompilerServices;

namespace FunctionInvocationTest
{
    class Program
    {
        static void Main(string[] args)
        {
            object[] objectArray = new object[10000000];
            for (int i = 0; i < objectArray.Length; ++i) { objectArray[i] = new object(); }

            ComputeStringFunction(objectArray[0]);
            ComputeStringFunctionViaFunc(objectArray[0].ToString);
            ComputeStringFunctionViaFunc(delegate() { return objectArray[0].ToString(); });
            ComputeStringFunctionViaFunc(() => objectArray[0].ToString());

            System.Diagnostics.Stopwatch s = new System.Diagnostics.Stopwatch();
            s.Start();
            for (int i = 0; i < objectArray.Length; ++i)
            {
                ComputeStringFunction(objectArray[i]);
            }
            s.Stop();
            Console.WriteLine(s.Elapsed.TotalMilliseconds);

            s.Reset();
            s.Start();
            for (int i = 0; i < objectArray.Length; ++i)
            {
                ComputeStringFunctionViaFunc(delegate() { return objectArray[i].ToString(); });
            }
            s.Stop();
            Console.WriteLine(s.Elapsed.TotalMilliseconds);

            s.Reset();
            s.Start();
            for (int i = 0; i < objectArray.Length; ++i)
            {
                ComputeStringFunctionViaFunc(objectArray[i].ToString);
            }
            s.Stop();
            Console.WriteLine(s.Elapsed.TotalMilliseconds);

            s.Reset();
            s.Start();
            for (int i = 0; i < objectArray.Length; ++i)
            {
                ComputeStringFunctionViaFunc(() => objectArray[i].ToString());
            }
            s.Stop();
            Console.WriteLine(s.Elapsed.TotalMilliseconds);

            Console.ReadLine();
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        public static void ComputeStringFunction(object stringFunction)
        {
        }

        public static void ComputeStringFunctionViaFunc(Func<string> stringFunction)
        {
        }
    }
}
4

2 に答える 2

6

ToString()実際に/を呼び出すようにコードを修正し、stringFunction()Mono2.10.9を使用して測定した後:

ComputeStringFunctionViaFunc(objectArray[i].ToString);object.ToString仮想であるため遅いです。各オブジェクトは、オーバーライドされた場合にチェックされToString、オーバーライドさToStringれたものを呼び出す必要があります。他のデリゲートは、仮想関数(高速)を直接呼び出す非仮想関数(高速)を参照するように作成されます。これが原因であるという事実は、生成されたILを変更するように変更するときに見ることができます

ldelem.ref
dup 
ldvirtftn instance string object::ToString()

ldelem.ref
ldftn instance string object::ToString()

これは常にを参照しobject.ToString、オーバーライド関数ではありません。その場合、3つの方法はすべてほぼ同じ時間かかります。

更新:直接バインドするが、仮想的にobjectArray[i]呼び出す1つの追加メソッド:ToString

for (int i = 0; i < objectArray.Length; ++i)
{
    ComputeStringFunctionViaFunc(objectArray[i].ToStringHelper);
}

static class Extensions
{
    public static string ToStringHelper(this object obj)
    {
        return obj.ToString();
    }
}

また、他の非仮想デリゲートとほぼ同じタイミングを提供します。

于 2012-08-21T21:38:36.147 に答える
4

それぞれの場合に何をしているかを調べてみましょう。

この男は関数をまったく「作成」しません。配列内のアイテム(この場合はオブジェクト)を検索し、そのアイテムをパラメーターとして関数に渡します。

// The cost of doing the array lookup happens right here, before 
// ComputeStringFunction is called
ComputeStringFunction(objectArray[i]);

これはパラメータのないデリゲートを作成し、それを関数に渡します。デリゲート自体が呼び出されることはありません。

// Because ComputeStringFunctionViaFunc doesn't do anything, the
// statement objectArray[i] is never evaluated, so the only cost 
// is that of creating a delegate
ComputeStringFunctionViaFunc(delegate() { return objectArray[i].ToString(); });

これは最初のものと同じことを行いますが、配列からアイテムを取得した直後にアイテムを渡す代わりに、アイテムを呼び出します.ToString()。ここでも、関数は作成されません。

最初のように、これは前もって配列ルックアップのコストがかかりますが、アイテムの.ToStringメソッドを参照するデリゲートを作成します(それをキャッチしてくれた@hvdに感謝します)。他の人と同様に、.ToStringは評価されません。コストは(繰り返しになりますが、@ hvdに感謝します)仮想メソッドを検索するコストです。

// The cost of doing the array lookup happens right here
ComputeStringFunctionViaFunc(objectArray[i].ToString);

最後に、これはラムダと配列アイテムのクロージャーを使用して関数を作成し、そのラムダを関数に渡します。関数のシグネチャに応じて、ラムダはコンパイルされる場合とされない場合があります。

// Again, we create a delegate but don't call it, so the array
// lookup and .ToString are never evaluated.
ComputeStringFunctionViaFunc(() => objectArray[i].ToString());

ここで注意すべき重要な点は、配列ルックアップの評価は2番目と4番目で遅延しますが、1番目と3番目では遅延しないことです。

これらのテストはすべて完全に異なることを行うため、やや無意味です。デリゲートの作成のタイミングをとるには、ほぼ間違いなくより良い方法があります。

于 2012-08-21T21:42:32.780 に答える