C# 为什么使用实例方法作为委托(delegate)分配 GC0 临时对象但比缓存委托(delegate)快 10%

标签 c# .net performance inline

我目前正在优化一个低级库,发现了一个违反直觉的案例。导致这个问题的提交是 here .

有一个代表

public delegate void FragmentHandler(UnsafeBuffer buffer, int offset, int length, Header header);

和一个实例方法

public void OnFragment(IDirectBuffer buffer, int offset, int length, Header header)
{
    _totalBytes.Set(_totalBytes.Get() + length);
}

关于 this line ,如果我将该方法用作委托(delegate),程序会为临时委托(delegate)包装器分配许多 GC0,但性能会快 10%(但不稳定)。

var fragmentsRead = image.Poll(OnFragment, MessageCountLimit);

如果我改为将方法缓存在循环外的委托(delegate)中,如下所示:

FragmentHandler onFragmentHandler = OnFragment;

然后程序根本不分配,数字非常稳定但速度慢得多。

我查看了生成的 IL,它正在做同样的事情,但在后一种情况下,newobj 只被调用一次,如果加载则调用局部变量。

使用缓存委托(delegate) IL_0034:

IL_002d: ldarg.0
IL_002e: ldftn instance void Adaptive.Aeron.Samples.IpcThroughput.IpcThroughput/Subscriber::OnFragment(class [Adaptive.Agrona]Adaptive.Agrona.IDirectBuffer, int32, int32, class [Adaptive.Aeron]Adaptive.Aeron.LogBuffer.Header)
IL_0034: newobj instance void [Adaptive.Aeron]Adaptive.Aeron.LogBuffer.FragmentHandler::.ctor(object, native int)
IL_0039: stloc.3
IL_003a: br.s IL_005a
// loop start (head: IL_005a)
    IL_003c: ldloc.0
    IL_003d: ldloc.3
    IL_003e: ldsfld int32 Adaptive.Aeron.Samples.IpcThroughput.IpcThroughput::MessageCountLimit
    IL_0043: callvirt instance int32 [Adaptive.Aeron]Adaptive.Aeron.Image::Poll(class [Adaptive.Aeron]Adaptive.Aeron.LogBuffer.FragmentHandler, int32)
    IL_0048: stloc.s fragmentsRead

使用临时分配 IL_0037:

IL_002c: stloc.2
IL_002d: br.s IL_0058
// loop start (head: IL_0058)
    IL_002f: ldloc.0
    IL_0030: ldarg.0
    IL_0031: ldftn instance void Adaptive.Aeron.Samples.IpcThroughput.IpcThroughput/Subscriber::OnFragment(class [Adaptive.Agrona]Adaptive.Agrona.IDirectBuffer, int32, int32, class [Adaptive.Aeron]Adaptive.Aeron.LogBuffer.Header)
    IL_0037: newobj instance void [Adaptive.Aeron]Adaptive.Aeron.LogBuffer.FragmentHandler::.ctor(object, native int)
    IL_003c: ldsfld int32 Adaptive.Aeron.Samples.IpcThroughput.IpcThroughput::MessageCountLimit
    IL_0041: callvirt instance int32 [Adaptive.Aeron]Adaptive.Aeron.Image::Poll(class [Adaptive.Aeron]Adaptive.Aeron.LogBuffer.FragmentHandler, int32)
    IL_0046: stloc.s fragmentsRead

为什么带分配的代码在这里更快?需要什么来避免分配但保持性能?

(在两台不同的机器上测试 .NET 4.5.2/4.6.1,x64,Release)

更新

这是一个按预期运行的独立示例:缓存委托(delegate)的执行速度提高了 2 倍多,分别为 4 秒和 11 秒。所以这个问题特定于所引用的项目 - JIT 编译器或其他问题的哪些细微问题可能会导致意外结果?

using System;
using System.Diagnostics;

namespace TestCachedDelegate {

    public delegate int TestDelegate(int first, int second);

    public static class Program {
        static void Main(string[] args)
        {
            var tc = new TestClass();
            tc.Run();
        }

        public class TestClass {

            public void Run() {
                var sw = new Stopwatch();
                sw.Restart();
                for (int i = 0; i < 1000000000; i++) {
                    CallDelegate(Add, i, i);
                }
                sw.Stop();
                Console.WriteLine("Non-cached: " + sw.ElapsedMilliseconds);
                sw.Restart();
                TestDelegate dlgCached = Add;
                for (int i = 0; i < 1000000000; i++) {
                    CallDelegate(dlgCached, i, i);
                }
                sw.Stop();
                Console.WriteLine("Cached: " + sw.ElapsedMilliseconds);
                Console.ReadLine();
            }

            public int CallDelegate(TestDelegate dlg, int first, int second) {
                return dlg(first, second);
            }

            public int Add(int first, int second) {
                return first + second;
            }

        }
    }
}

最佳答案

因此,在过快地阅读问题并认为它在问其他问题之后,我终于有时间坐下来玩一下有问题的 Aeoron 测试。

我尝试了一些东西,首先我比较了生成的 IL 和汇编程序,发现无论是在我们调用 Poll() 的地方还是在实际上调用了处理程序。

其次,我尝试注释掉 Poll() 方法中的代码,以确认缓存版本确实运行得更快(确实如此)。

第三,我尝试查看 VS 分析器中的 CPU 计数器(缓存未命中、指令失效和分支预测错误),但除了委托(delegate)构造函数显然被调用多次这一事实外,我看不出两个版本之间的任何差异.

这让我想到了我们在移植过程中遇到的一个类似案例 Disruptor-net我们的测试运行速度比 Java 版本慢,但我们确信我们没有做任何更昂贵的事情。测试“缓慢”的原因是我们实际上更快,因此批处理更少,因此我们的吞吐量更低。

如果您在调用 Poll() 之前插入 Thread.SpinWait(5),您将看到与非缓存版本相同或更好的性能。

我当时认为的问题的原始答案是“为什么使用实例方法委托(delegate)比手动缓存委托(delegate)慢”:

线索就在问题中。它是一个实例方法,因此它隐式地捕获了 this 成员,而 this 被捕获的事实意味着它不能被缓存。鉴于 this 在缓存委托(delegate)的生命周期内永远不会改变,它应该是可缓存的。

如果将方法组扩展为 (first, second) => this.Add(first, second),捕获将变得更加明显。

请注意,Roslyn 团队正在努力解决此问题:https://github.com/dotnet/roslyn/issues/5835

关于C# 为什么使用实例方法作为委托(delegate)分配 GC0 临时对象但比缓存委托(delegate)快 10%,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/37372719/

相关文章:

c# - 在 mvc asp.net 的浏览器中找不到 API Controller

c# - 检查点是否是对角线

performance - Flash 中的舞台外 DisplayObjects 是否仍会减慢我的游戏速度?

c# - ef6 include() 用于具有必需属性的可空属性

c# - 间距乱了

c# - html敏捷得不到结果

objective-c - 什么更有效率 : loading and adding a nib or creating the custom controls programmatically?

performance - Erlang 真的很快吗,因为它会在后台产生许多内存副本?

c# - .NET 4.5 异步等待和重载方法

c# - 将 JSON 数组反序列化为字符串数组