vc *_* 74 2 .net c# performance delegates
当我在我的机器上以释放模式执行以下代码时,具有非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);
}
}
}
Run Code Online (Sandbox Code Playgroud)
我会写这篇文章,其背后有相当不错的编程建议,这对任何关心编写快速代码的C#程序员来说都很重要.我一般谨慎使用微基准测试,由于现代CPU核心上代码执行速度的不可预测性,15%或更低的差异在统计上并不显着.降低测量不存在的几率的好方法是重复测试至少10次以消除缓存效应并交换测试,以便可以消除代码对齐效果.
但是你看到的是真实的,调用静态方法的委托实际上更慢.在x86代码中效果非常小,但在x64代码中效果要差一些,请务必修改项目>属性>构建选项卡>首选32位和平台目标设置以尝试两者.
知道速度慢的原因需要查看抖动产生的机器代码.在代理的情况下,该代码非常隐蔽.使用Debug> Windows> Disassembly查看代码时,您将看不到它.而你甚至无法单步执行代码,编写托管调试程序来隐藏它并完全拒绝显示它.我将不得不描述一种将"视觉"重新放回Visual Studio的技术.
我不得不谈谈"存根".除了抖动生成的代码之外,存根是CLR动态创建的机器代码的一小部分.存根用于实现接口,它们提供了灵活性,使得方法表中方法的方法顺序不必与接口方法的顺序相匹配.它们对代表来说很重要,这是这个问题的主题.存根对即时编译也很重要,存根中的初始代码指向抖动的入口点以获取在调用时编译的方法.之后更换存根,现在调用jitted目标方法.它是使静态方法调用更慢的存根,静态方法目标的存根比实例方法的存根更精细.
要查看存根,您必须调试调试器以强制它显示其代码.需要进行一些设置:首先使用工具>选项>调试>常规.取消选中"Just My Code"复选框,取消选中"Suppress JIT optimization"复选框.如果您使用VS2015然后勾选"使用托管兼容模式",则VS2015调试器非常错误并且严重妨碍此类调试,此选项通过强制使用VS2010托管调试器引擎来提供解决方法.切换到发布配置.然后选择Project> Properties> Debug,勾选"启用本机代码调试"复选框.和Project> Properties> Build,取消选中"Prefer 32-bit"复选框,"Platform target"应为AnyCPU.
在Run()方法上设置断点,请注意断点在优化代码中不是很准确.在方法头上设置是最好的.一旦命中,使用Debug> Windows> Disassembly查看抖动生成的机器代码.在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
Run Code Online (Sandbox Code Playgroud)
64位方法调用传递寄存器中的前4个参数,任何其他参数都通过堆栈传递(不在此处).这里使用XMM寄存器,因为参数是浮点数.此时,抖动无法知道方法是静态还是实例,在此代码实际执行之前无法找到.存根的作用是隐藏差异.它假设它将是一个实例方法,这就是我注释arg2和arg3的原因.
在CALL指令上设置断点,第二次命中(因此在存根不再指向抖动之后),您可以查看它.这必须手动完成,使用Debug> Windows> Registers并复制RAX寄存器的值.Debug> Windows> Memory> Memory1并粘贴该值,在其前面加上"0x"并添加0x18.右键单击该窗口并选择"8字节整数",复制第一个显示的值.这是存根代码的地址.
现在的诀窍是,此时托管调试引擎仍在使用,不允许您查看存根代码.您必须强制进行模式切换,以便控制非托管调试引擎.使用Debug> Windows> Call Stack并双击底部的方法调用,如RtlUserThreadStart.强制调试器切换引擎.现在你很高兴可以将地址粘贴到地址框中,在其前面加上"0x".Out弹出存根代码:
00007FFCE66D0100 jmp 00007FFCE66D0E40
Run Code Online (Sandbox Code Playgroud)
非常简单,直接跳转到委托目标方法.这将是快速代码.抖动在实例方法中正确猜测,并且委托对象已经this在RCX寄存器中提供了参数,因此不需要做任何特殊操作.
继续第二个测试并执行完全相同的操作来查看实例调用的存根.现在存根非常不同:
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
Run Code Online (Sandbox Code Playgroud)
代码有点不稳定而且不是最优的,微软可能会在这里做得更好,而且我并不是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
Run Code Online (Sandbox Code Playgroud)
比64位存根简单得多,这就是为什么32位代码几乎不会受到减速的影响.它之所以如此不同的一个原因是32位代码在FPU堆栈上传递浮点并且它们不必重新洗牌.使用整数或对象参数时,这不一定会更快.
非常晦涩,希望我没有让所有人都入睡.注意我可能有一些注释错误,我不完全理解存根以及CLR烹饪委托对象成员以尽可能快地编写代码的方式.但这里肯定有不错的编程建议.你真的有利于实例方法为代表的目标,使它们static是不是一种优化.