C#中委托的意外低性能表现

4

我之前发布了一个有关在C#中动态编译代码的问题,链接在此,得到的答案引发了另一个问题。

其中一个建议是使用委托,我尝试过并且它们运行良好。然而,它们的性能比直接调用慢了约8.4倍,这没有道理。

这段代码哪里有问题?

我的结果,.Net 4.0,64位,直接运行exe:62, 514, 530。

public static int Execute(int i) { return i * 2; }

private void button30_Click(object sender, EventArgs e)
{
    CSharpCodeProvider foo = new CSharpCodeProvider();

    var res = foo.CompileAssemblyFromSource(
        new System.CodeDom.Compiler.CompilerParameters()
        {
            GenerateInMemory = true,
            CompilerOptions = @"/optimize",                    
        },
        @"public class FooClass { public static int Execute(int i) { return i * 2; }}"
    );

    var type = res.CompiledAssembly.GetType("FooClass");
    var obj = Activator.CreateInstance(type);
    var method = type.GetMethod("Execute");
    int i = 0, t1 = Environment.TickCount, t2;
    //var input = new object[] { 2 };

    //for (int j = 0; j < 10000000; j++)
    //{
    //    input[0] = j;
    //    var output = method.Invoke(obj, input);
    //    i = (int)output;
    //}

    //t2 = Environment.TickCount;

    //MessageBox.Show((t2 - t1).ToString() + Environment.NewLine + i.ToString());

    t1 = Environment.TickCount;

    for (int j = 0; j < 100000000; j++)
    {
        i = Execute(j);
    }

    t2 = Environment.TickCount;

    MessageBox.Show("Native: " + (t2 - t1).ToString() + Environment.NewLine + i.ToString());

    var func = (Func<int, int>) Delegate.CreateDelegate(typeof (Func<int, int>), method);

    t1 = Environment.TickCount;

    for (int j = 0; j < 100000000; j++)
    {
        i = func(j);
    }

    t2 = Environment.TickCount;

    MessageBox.Show("Dynamic delegate: " + (t2 - t1).ToString() + Environment.NewLine + i.ToString());

    Func<int, int> funcL = Execute;

    t1 = Environment.TickCount;

    for (int j = 0; j < 100000000; j++)
    {
        i = funcL(j);
    }

    t2 = Environment.TickCount;

    MessageBox.Show("Delegate: " + (t2 - t1).ToString() + Environment.NewLine + i.ToString());
}

一个猜测是,在本地情况下,编译器可以内联函数,而在委托情况下它不能,必须执行方法调用。你能检查生成的低级代码吗? - ron
4
问题不在于代理的速度缓慢,而是普通方法调用非常快。当在这样的代码中使用时,它们应该需要零个周期,因为即时编译器优化器完全消除了调用并内联了代码。你大多数情况下测量到的是for()循环的成本。 - Hans Passant
@HansPassant,反汇编并没有显示你所说的内容。它显示了一个调用。而且,我认为调用并不昂贵。我们只是在这里进行了一些快速的操作:推送、调用、弹出。在我看来,开销不到8.4倍。 - IamIC
2
@IanC 它可能会被JIT编译器内联。您应该在调试器中检查最终的汇编代码(在运行测试后附加,否则JIT不会进行每个优化)。 - Adriano Repetti
@gabba 你是在VS里面还是外面执行的?我不确定为什么本地执行会更慢。 - IamIC
显示剩余5条评论
2个回答

5
正如汉斯在你的问题评论中提到的,Execute方法非常简单,以至于它几乎肯定被Jitter在你的“本地”测试中内联了。
因此,你看到的不是标准方法调用和委托调用之间的比较,而是内联的i * 2操作和委托调用之间的比较。(而且这个i * 2操作可能只会归结为一个机器指令,速度非常快。)
让你的Execute方法变得更加复杂以防止内联(和/或使用MethodImplOptions.NoInlining编译器提示来完成);然后你将得到一个更现实的标准方法调用和委托调用之间的比较。在大多数情况下,差异可能是可以忽略不计的:
[MethodImpl(MethodImplOptions.NoInlining)]
static int Execute(int i) { return ((i / 63.53) == 34.23) ? -1 : (i * 2); }
public static volatile int Result;

private static void Main(string[] args)
{
    const int iterations = 100000000;

    {
        Result = Execute(42);  // pre-jit
        var s = Stopwatch.StartNew();

        for (int i = 0; i < iterations; i++)
        {
            Result = Execute(i);
        }
        s.Stop();
        Console.WriteLine("Native: " + s.ElapsedMilliseconds);
    }

    {
        Func<int, int> func;
        using (var cscp = new CSharpCodeProvider())
        {
            var cp = new CompilerParameters { GenerateInMemory = true, CompilerOptions = @"/optimize" };
            string src = @"public static class Foo { public static int Execute(int i) { return ((i / 63.53) == 34.23) ? -1 : (i * 2); } }";

            var cr = cscp.CompileAssemblyFromSource(cp, src);
            var mi = cr.CompiledAssembly.GetType("Foo").GetMethod("Execute");
            func = (Func<int, int>)Delegate.CreateDelegate(typeof(Func<int, int>), mi);
        }

        Result = func(42);  // pre-jit
        var s = Stopwatch.StartNew();

        for (int i = 0; i < iterations; i++)
        {
            Result = func(i);
        }
        s.Stop();
        Console.WriteLine("Dynamic delegate: " + s.ElapsedMilliseconds);
    }

    {
        Func<int, int> func = Execute;
        Result = func(42);  // pre-jit

        var s = Stopwatch.StartNew();
        for (int i = 0; i < iterations; i++)
        {
            Result = func(i);
        }
        s.Stop();
        Console.WriteLine("Delegate: " + s.ElapsedMilliseconds);
    }
}

2
我添加了no-inline提示并重新运行了测试,你说得对。委托只慢了约50%。不过奇怪的是,动态编译方法的调用时间始终比本地方法的调用时间快91%。我想知道为什么会这样。 - IamIC

4

这是有道理的。委托不是函数指针,它们涉及类型检查、安全性和许多其他方面。它们更接近虚函数调用的速度(请参见此文章),即使性能影响来自完全不同的东西。

如果您想对不同的调用技术进行良好比较(其中一些未在问题中提到),请阅读此篇文章


同意。这也很正常。当在发布模式下运行时,使用委托的速度比本地调用慢了4倍。 - Darin Dimitrov
哇,我曾经读到过它们只应该是一半的速度。我可以指出认为如此的专家。但是...一个人无法否认结果。 - IamIC
@DarinDimitrov 你运行了我的代码,但速度慢了4倍?我不知道为什么我得到的是8倍。 - IamIC
@IanC 我猜真正的比较是不可能的。由于JIT编译,结果会因每台机器而异(你只能说它们要慢得多)。此外,测试环境也非常重要(我读过一篇关于Java性能测试的好文章,但我想不起来在哪里看到的了)。 - Adriano Repetti

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接