首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >C#为什么使用实例方法作为委托分配GC0临时对象,但比缓存的委托快10%

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

Stack Overflow用户
提问于 2016-05-22 09:43:35
回答 1查看 403关注 0票数 7

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

有一位代表

代码语言:javascript
复制
public delegate void FragmentHandler(UnsafeBuffer buffer, int offset, int length, Header header);

和一个实例方法

代码语言:javascript
复制
public void OnFragment(IDirectBuffer buffer, int offset, int length, Header header)
{
    _totalBytes.Set(_totalBytes.Get() + length);
}

这条线上,如果我使用该方法作为委托,程序会为临时委托包装器分配许多GC0,但是性能要快10% (但不稳定)。

代码语言:javascript
复制
var fragmentsRead = image.Poll(OnFragment, MessageCountLimit);

如果我将该方法缓存在循环外部的委托中,则如下所示:

代码语言:javascript
复制
FragmentHandler onFragmentHandler = OnFragment;

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

我查看了生成的IL,它正在执行相同的操作,但在后面的情况下,如果加载,只调用一次newobj,然后调用局部变量。

使用缓存的委托IL_0034:

代码语言:javascript
复制
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:

代码语言:javascript
复制
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,版本,在两台不同的机器上进行测试)

更新

下面是一个独立的示例,它的行为与预期相同:缓存委托的执行速度比预期的快2倍以上,分别为4秒和11秒。因此,这个问题是针对所引用的项目的-- JIT编译器或其他什么细微的问题会导致意外的结果吗?

代码语言:javascript
复制
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;
            }

        }
    }
}
EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2016-08-18 09:21:45

所以,在读完这个问题后,我想这是在问别的问题,我终于有时间坐下来玩Aeoron考试了。

我尝试了一些东西,首先,我比较了IL和汇编程序产生的,发现在我们调用Poll()的站点或者在实际调用处理程序的站点上,基本上没有区别。

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

我试图查看VS分析器中的CPU计数器(缓存丢失、指令退役和分支错误预测),但是除了委托构造函数显然被调用次数更多这一事实之外,看不到这两个版本之间的任何区别。

这让我想到了一个类似的例子,我们在移植干扰物网时遇到了一个比java版本运行更慢的测试,但我们确信我们没有做任何更昂贵的事情。测试“缓慢”的原因是我们实际上更快,因此批处理更少,因此我们的吞吐量更低。

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

最初回答了我当时认为“为什么使用实例方法委托比手动缓存委托慢”的问题:

线索就在问题里。它是一个实例方法,因此它隐式地捕获了this成员,并且它被捕获的事实意味着它不能被缓存。考虑到this在缓存委托的生存期内永远不会更改,那么它应该是可缓存的。

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

请注意,罗斯林团队正在致力于修复这个问题:https://github.com/dotnet/roslyn/issues/5835

票数 2
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/37372719

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档