当我在我的机器上以释放模式执行以下代码时,具有非null目标的委托的执行总是比委托具有空目标(我希望它等效或更慢)时快一些。
我真的不是在寻找微优化,但我想知道为什么会出现这种情况?
static void Main(string[] args)
{
// Warmup code
long durationWithTarget =
MeasureDuration(() => new DelegatePerformanceTester(withTarget: true).Run());
Console.WriteLine($"With target: {durationWithTarget}");
long durationWithoutTarget =
MeasureDuration(() => new DelegatePerformanceTester(withTarget: false).Run());
Console.WriteLine($"Without target: {durationWithoutTarget}");
}
/// <summary>
/// Measures the duration of an action.
/// </summary>
/// <param name="action">Action which duration has to be measured.</param>
/// <returns>The duration in milliseconds.</returns>
private static long MeasureDuration(Action action)
{
Stopwatch stopwatch = Stopwatch.StartNew();
action();
return stopwatch.ElapsedMilliseconds;
}
class DelegatePerformanceTester
{
public DelegatePerformanceTester(bool withTarget)
{
if (withTarget)
{
_func = AddNotStatic;
}
else
{
_func = AddStatic;
}
}
private readonly Func<double, double, double> _func;
private double AddNotStatic(double x, double y) => x + y;
private static double AddStatic(double x, double y) => x + y;
public void Run()
{
const int loops = 1000000000;
for (int i = 0; i < loops; i++)
{
double funcResult = _func.Invoke(1d, 2d);
}
}
}
答案 0 :(得分:7)
我会写这篇文章,其后面有相当不错的编程建议,这对任何关心编写快速代码的C#程序员来说都很重要。我一般谨慎使用微基准测试,由于现代CPU核心上代码执行速度的不可预测性,15%或更低的差异在统计上并不显着。降低测量不存在的几率的好方法是重复测试至少10次以消除缓存效应并交换测试,以便消除代码对齐效果。
但是你看到的是真实的,调用静态方法的委托实际上更慢。在x86代码中效果非常小,但在x64代码中效果要差一些,请务必修改项目&gt;属性&gt;构建标签&gt;首选32位和平台目标设置以尝试这两种设置。
了解速度慢的原因需要查看抖动产生的机器代码。在代理的情况下,该代码非常隐藏得很好。使用Debug&gt;查看代码时,您将看不到它。 Windows&gt;拆卸。而且你甚至无法单步执行代码,编写托管调试程序来隐藏它并完全拒绝显示它。我将不得不描述一种技术来放置&#34; visual&#34;回到Visual Studio。
我必须谈谈&#34;存根&#34;。除了抖动生成的代码之外,存根是CLR动态创建的机器代码的一小部分。存根用于实现接口,它们提供了灵活性,使得方法表中方法的方法顺序不必与接口方法的顺序相匹配。它们对代表来说很重要,这是这个问题的主题。存根对即时编译也很重要,存根中的初始代码指向抖动的入口点以获取在调用时编译的方法。之后更换存根,现在调用jitted目标方法。它是使静态方法调用更慢的存根,静态方法目标的存根比实例方法的存根更精细。
要查看存根,您必须调试调试器以强制它显示其代码。需要进行一些设置:首先使用工具&gt;选项&gt;调试&gt;一般。解开&#34; Just My Code&#34;复选框,取消勾选&#34;抑制JIT优化&#34;复选框。如果您使用VS2015,然后勾选&#34;使用托管兼容模式&#34;,VS2015调试器非常错误,并且认真对待此类调试,此选项通过强制VS2010托管调试器引擎提供解决方法用过的。切换到发布配置。然后项目&gt;属性&gt;调试,勾选&#34;启用本机代码调试&#34;复选框。和项目&gt;属性&gt;构建,取消选择&#34;首选32位&#34;复选框和&#34;平台目标&#34;应该是AnyCPU。
在Run()方法上设置断点,请注意断点在优化代码中不是很准确。在方法头上设置是最好的。一旦命中,请使用Debug&gt; Windows&gt;反汇编,查看抖动产生的机器代码。在Haswell核心上,委托调用看起来像这样,如果你有一个不支持AVX的旧处理器,可能与你看到的不一致:
funcResult += _func.Invoke(1d, 2d);
0000001a mov rax,qword ptr [rsi+8] ; rax = _func
0000001e mov rcx,qword ptr [rax+8] ; rcx = _func._methodBase (?)
00000022 vmovsd xmm2,qword ptr [0000000000000070h] ; arg3 = 2d
0000002b vmovsd xmm1,qword ptr [0000000000000078h] ; arg2 = 1d
00000034 call qword ptr [rax+18h] ; call stub
64位方法调用传递寄存器中的前4个参数,任何其他参数都通过堆栈传递(不在此处)。这里使用XMM寄存器,因为参数是浮点数。此时,抖动还不知道方法是静态的还是实例的,在该代码实际执行之前无法找到。存根的作用是隐藏差异。它假设它将是一个实例方法,这就是为什么我注释了arg2和arg3。
在CALL指令上设置断点,第二次命中(因此在存根不再指向抖动之后),您可以查看它。这必须手工完成,使用Debug&gt; Windows&gt;注册并复制RAX寄存器的值。调试&gt; Windows&gt;记忆&gt; Memory1并粘贴值,put&#34; 0x&#34;在它前面并添加0x18。右键单击该窗口并选择&#34; 8字节整数&#34;,复制第一个显示的值。这是存根代码的地址。
现在的诀窍是,此时托管调试引擎仍在使用,不允许您查看存根代码。您必须强制进行模式切换,以便控制非托管调试引擎。使用Debug&gt; Windows&gt;调用Stack并双击底部的方法调用,如RtlUserThreadStart。强制调试器切换引擎。现在你很高兴可以将地址粘贴到地址框中,放入&#34; 0x&#34;在它面前。 Out弹出存根代码:
00007FFCE66D0100 jmp 00007FFCE66D0E40
非常简单,直接跳转到委托目标方法。这将是快速代码。抖动在实例方法中正确猜测,并且委托对象已经在RCX寄存器中提供了this
参数,因此不需要做任何特殊操作。
继续进行第二次测试并完成同样的事情来查看实例调用的存根。现在存根非常不同:
000001FE559F0850 mov rax,rsp ; ?
000001FE559F0853 mov r11,rcx ; r11 = _func (?)
000001FE559F0856 movaps xmm0,xmm1 ; shuffle arg3 into right register
000001FE559F0859 movaps xmm1,xmm2 ; shuffle arg2 into right register
000001FE559F085C mov r10,qword ptr [r11+20h] ; r10 = _func.Method
000001FE559F0860 add r11,20h ; ?
000001FE559F0864 jmp r10 ; jump to _func.Method
代码有点不稳定而且不是最优的,微软可能会在这里做得更好,而且我并不是100%确定我正确地注释了它。我想不必要的mov rax,rsp指令只与存根有超过4个参数的方法有关。不知道为什么添加指令是必要的。最重要的细节是XMM寄存器移动,它必须重新洗牌,因为静态方法没有this
参数。正是这种重新调整的要求使代码变慢。
您可以使用x86抖动执行相同的练习,静态方法存根现在看起来像:
04F905B4 mov eax,ecx
04F905B6 add eax,10h
04F905B9 jmp dword ptr [eax] ; jump to _func.Method
比64位存根简单得多,这就是32位代码几乎没有受到减速影响的原因。其中一个非常不同的原因是32位代码通过FPU堆栈上的浮点并且它们不必重新洗牌。当你使用整数或对象参数时,这不一定会更快。
非常晦涩,希望我还没有让所有人都入睡。请注意,我可能会得到一些错误的注释,我不完全理解存根以及CLR烹饪委托对象成员以尽可能快地编写代码的方式。但这里肯定有不错的编程建议。你真的喜欢将实例方法作为委托目标,使static
不进行优化。