【问题标题】:C# Method 100x slower with three returns vs two?C# 方法慢 100 倍,三个返回与两个返回?
【发布时间】:2018-09-09 06:39:01
【问题描述】:

当我尝试对其进行性能测试时,我使用我所做的方法有一些奇怪的行为,基本上如果我注释掉/禁用其中一个 if 语句中的返回之一,它会从 400 毫秒变为 4 毫秒,几乎就像它正在被编译掉,而不是实际运行代码,如果在注释/禁用一个返回之后,它只返回 true 或 false ,所以它只有一个选项,然后我可以看到编译器如何会对其进行优化并始终将其设置为布尔值而不是运行代码。

任何人都知道可能会发生什么或有关于运行测试的更好方法的建议吗?

我的测试代码:

Vec3 spherePos = new Vec3(43.7527, 75.9756, 0);
double sphereRadisSq = 50 * 50;
Vec3 rayPos = new Vec3(-5.32301, 5.97157, -112.983);
Vec3 rayDir = new Vec3(0.457841, 0.680324, 0.572312);

sw.Reset();
sw.Start();
bool res = false;
for (int i = 0; i < 10000000; i++)
{
   res = Intersect.RaySphereFast(rayPos, rayDir, spherePos, sphereRadisSq);
}      
sw.Stop();
Debug.Log($"testTime: {sw.ElapsedMilliseconds} ms");
Debug.Log(res);

还有静态方法:

public static bool RaySphereFast(Vec3 _rp, Vec3 _rd, Vec3 _sp, double _srsq) 
{
    double rs = Vec3.DistanceFast(_rp, _sp);
    if (rs < _srsq)
    {
        return (true); // <-- When I disable this one
    }
    Vec3 p = Vec3.ProjectFast(_sp, _rp, _rd);
    double pr = Vec3.Dot(_rd, (p - _rp));
    if (pr < 0)
    {
        return (false); // <--  Or when I disable this one
    }
    double ps = Vec3.DistanceFast(p, _sp);
    if (ps < _srsq) 
    {
        return (true); // <--  Or when I disable this one
    }
    return (false);
}

Vec3 结构体(瘦身

public struct Vec3
{
    public Vec3(double _x, double _y, double _z)
    {
        x = _x;
        y = _y;
        z = _z;
    }

    public double x { get; }
    public double y { get; }
    public double z { get; }

    public static double DistanceFast(Vec3 _v0, Vec3 _v1) 
    {
        double x = (_v1.x - _v0.x);
        double y = (_v1.y - _v0.y);
        double z = (_v1.z - _v0.z);
        return ((x * x) + (y * y) + (z * z));
    }

    public static double Dot(Vec3 _v0, Vec3 _v1)
    {
        return ((_v0.x * _v1.x) + (_v0.y * _v1.y) + (_v0.z * _v1.z));
    }

    public static Vec3 ProjectFast(Vec3 _p, Vec3 _a, Vec3 _d) 
    {
        Vec3 ap = _p - _a;
        return (_a + Vec3.Dot(ap, _d) * _d);
    }

    public static Vec3 operator +(Vec3 _v0, Vec3 _v1)
    {
        return (new Vec3(_v0.x + _v1.x, _v0.y + _v1.y, _v0.z + _v1.z));
    }

    public static Vec3 operator -(Vec3 _v0, Vec3 _v1)
    {
        return new Vec3(_v0.x - _v1.x, _v0.y - _v1.y, _v0.z - _v1.z);
    }

    public static Vec3 operator *(double _d1, Vec3 _v0)
    {
        return new Vec3(_d1 * _v0.x, _d1 * _v0.y, _d1 * _v0.z);
    }
}

【问题讨论】:

  • 你评论的是哪个回报?
  • @ChetanRanpariya 在不同的 if 语句中的三个中的任何一个。
  • 这是有道理的,因为在所有情况下它都可以优化掉一堆代码,而且可能确实如此,尽管快 100 倍似乎并不正确
  • 如果我们能看到 Vec3 的实现会更容易。然后我们可以查看生成的 IL
  • @TheGeneral 当我将循环再增加 10 倍时,它会从 ~4000ms 变为 ~28ms,这肯定是不对的。

标签: c# performance optimization


【解决方案1】:

这很可能会发生,因为当您注释掉返回时,方法的复杂性会低于禁用自动内联的阈值。

此内联在生成的 IL 中不可见 - 它由 JIT 编译器完成。

我们可以通过用[MethodImpl(MethodImplOptions.AggressiveInlining)] 属性装饰有问题的方法来检验这个假设。

当我用你的代码尝试这个时,我得到了以下结果(发布,x64 构建):

Original code:                      302 ms
First return commented out:           2 ms
Decorated with AggressiveInlining:    2 ms

注释掉第一个返回的时间与我用AggressiveInlining装饰方法时获得的时间相同(启用第一个返回)。

因此我得出结论,假设是正确的。

【讨论】:

    【解决方案2】:

    这里发生了一些有趣的事情。正如其他人在注释掉其中一个返回值时所指出的那样,RaySphereFast 方法现在变得足够小,可以内联,实际上 jit 决定内联它。这反过来又内联了它调用的所有辅助方法。结果,循环体最终没有调用。

    一旦发生这种情况,jit 就会“提升”各种Vec3 实例,并且由于您已经使用常量初始化了所有字段,因此 jit 会传播这些常量并在各种操作中折叠它们。因此,jit 意识到调用的结果将始终是true

    由于循环的每次迭代都返回相同的值,jit 意识到循环中的这些计算实际上都不是必需的(因为结果是已知的)并将它们全部删除。因此,在“快速”版本中,您正在计时一个空循环:

    G_M52940_IG04:
           BF01000000           mov      edi, 1
           FFC1                 inc      ecx
           81F980969800         cmp      ecx, 0x989680
           7CF1                 jl       SHORT G_M52940_IG04
    

    在“慢”版本中,调用不会被内联,也不会启动任何优化:

    G_M32193_IG04:
           488D4C2478           lea      rcx, bword ptr [rsp+78H]
           C4617B1109           vmovsd   qword ptr [rcx], xmm9
           C4617B115108         vmovsd   qword ptr [rcx+8], xmm10
           C4617B115910         vmovsd   qword ptr [rcx+16], xmm11
           488D4C2460           lea      rcx, bword ptr [rsp+60H]
           C4617B1121           vmovsd   qword ptr [rcx], xmm12
           C4617B116908         vmovsd   qword ptr [rcx+8], xmm13
           C4617B117110         vmovsd   qword ptr [rcx+16], xmm14
           488D4C2448           lea      rcx, bword ptr [rsp+48H]
           C4E17B1131           vmovsd   qword ptr [rcx], xmm6
           C4E17B117908         vmovsd   qword ptr [rcx+8], xmm7
           C4617B114110         vmovsd   qword ptr [rcx+16], xmm8
           488D4C2478           lea      rcx, bword ptr [rsp+78H]
           488D542460           lea      rdx, bword ptr [rsp+60H]
           4C8D442448           lea      r8, bword ptr [rsp+48H]
           C4E17B101D67010000   vmovsd   xmm3, qword ptr [reloc @RWD64]
           E8D2F8FFFF           call     X:RaySphereFast(struct,struct,struct,double):bool
           8BD8                 mov      ebx, eax
           FFC7                 inc      edi
           81FF80969800         cmp      edi, 0x989680
           7C95                 jl       SHORT G_M32193_IG04
    

    如果您真的有兴趣对 RaySphereFast 的速度进行基准测试,请确保在每次迭代中使用不同或非常量的参数调用它,并确保使用每次迭代的结果。

    【讨论】:

      【解决方案3】:

      只是在@Matthew Watson 的答案中添加一个(明显的)免责声明

      结果取决于 .NET 版本、JIT 版本等。 仅供参考,我无法重现这种差异,并且在我的环境中返回的结果几乎相同。

      我将 benchmarkDotNet 与 .NET Core 2.1.0 一起使用,请参阅下面的详细信息

      // * Summary *
      
      BenchmarkDotNet=v0.11.1, OS=Windows 10.0.17134.228 (1803/April2018Update/Redstone4)
      Intel Core i7-4700MQ CPU 2.40GHz (Max: 1.08GHz) (Haswell), 1 CPU, 8 logical and 4 physical cores
      Frequency=2338346 Hz, Resolution=427.6527 ns, Timer=TSC
      .NET Core SDK=2.2.100-preview1-009349
        [Host]     : .NET Core 2.1.0 (CoreCLR 4.6.26515.07, CoreFX 4.6.26515.06), 64bit RyuJIT
        DefaultJob : .NET Core 2.1.0 (CoreCLR 4.6.26515.07, CoreFX 4.6.26515.06), 64bit RyuJIT
      
      
                       Method |     Mean |     Error |    StdDev |
      ----------------------- |---------:|----------:|----------:|
       RaySphereFast_Original | 40.06 ns | 0.3693 ns | 0.3455 ns |
       RaySphereFast_NoReturn | 40.46 ns | 0.0860 ns | 0.0805 ns |
      
      // * Legends *
        Mean   : Arithmetic mean of all measurements
        Error  : Half of 99.9% confidence interval
        StdDev : Standard deviation of all measurements
        1 ns   : 1 Nanosecond (0.000000001 sec)
      
      // ***** BenchmarkRunner: End *****
      Run time: 00:00:34 (34.86 sec), executed benchmarks: 2
      
      // * Artifacts cleanup *
      

      【讨论】:

        猜你喜欢
        • 2014-06-26
        • 2012-11-23
        • 1970-01-01
        • 2014-12-22
        • 1970-01-01
        • 2021-01-27
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多