为什么Mono运行一个简单的方法较慢而RyuJIT运行速度明显更快?

dym*_*oid 8 c# mono benchmarking ryujit benchmarkdotnet

我出于好奇创建了一个简单的基准,但无法解释结果.

作为基准数据,我准备了一些带有一些随机值的结构数组.准备阶段没有基准:

struct Val 
{
    public float val;
    public float min;
    public float max;
    public float padding;
}

const int iterations = 1000;
Val[] values = new Val[iterations];
// fill the array with randoms
Run Code Online (Sandbox Code Playgroud)

基本上,我想比较这两个钳位实现:

static class Clamps
{
    public static float ClampSimple(float val, float min, float max)
    {
        if (val < min) return min;          
        if (val > max) return max;
        return val;
    }

    public static T ClampExt<T>(this T val, T min, T max) where T : IComparable<T>
    {
        if (val.CompareTo(min) < 0) return min;
        if (val.CompareTo(max) > 0) return max;
        return val;
    }
}
Run Code Online (Sandbox Code Playgroud)

以下是我的基准方法:

[Benchmark]
public float Extension()
{
    float result = 0;
    for (int i = 0; i < iterations; ++i)
    {
        ref Val v = ref values[i];
        result += v.val.ClampExt(v.min, v.max);
    }

    return result;
}

[Benchmark]
public float Direct()
{
    float result = 0;
    for (int i = 0; i < iterations; ++i)
    {
        ref Val v = ref values[i];
        result += Clamps.ClampSimple(v.val, v.min, v.max);
    }

    return result;
}
Run Code Online (Sandbox Code Playgroud)

我正在使用BenchmarkDotNet版本0.10.12,有两个作业:

[MonoJob]
[RyuJitX64Job]
Run Code Online (Sandbox Code Playgroud)

这些是我得到的结果:

BenchmarkDotNet=v0.10.12, OS=Windows 7 SP1 (6.1.7601.0)
Intel Core i7-6920HQ CPU 2.90GHz (Skylake), 1 CPU, 8 logical cores and 4 physical cores
Frequency=2836123 Hz, Resolution=352.5940 ns, Timer=TSC
  [Host]    : .NET Framework 4.7 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3062.0
  Mono      : Mono 5.12.0 (Visual Studio), 64bit
  RyuJitX64 : .NET Framework 4.7 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3062.0


    Method |       Job | Runtime |      Mean |     Error |    StdDev |
---------- |---------- |-------- |----------:|----------:|----------:|
 Extension |      Mono |    Mono | 10.860 us | 0.0063 us | 0.0053 us |
    Direct |      Mono |    Mono | 11.211 us | 0.0074 us | 0.0062 us |
 Extension | RyuJitX64 |     Clr |  5.711 us | 0.0014 us | 0.0012 us |
    Direct | RyuJitX64 |     Clr |  1.395 us | 0.0056 us | 0.0052 us |
Run Code Online (Sandbox Code Playgroud)

我可以接受Mono在一般情况下有点慢.但我不明白的是:

为什么单声道运行的Direct方法要慢Extension在牢记Direct使用,而一个很简单的比较方法Extension使用带有附加的方法调用的方法?

RyuJIT在这里显示了简单方法的4倍优势.

有谁能解释一下?

dym*_*oid 2

由于没有人想做一些反汇编的事情,我回答我自己的问题。

\n\n

看来原因是 JIT 生成的本机代码,而不是注释中提到的数组边界检查或缓存问题。

\n\n

RyuJIT 为该方法生成非常高效的代码ClampSimple

\n\n
    vucomiss xmm1,xmm0\n    jbe     M01_L00\n    vmovaps xmm0,xmm1\n    ret\n\nM01_L00:\n    vucomiss xmm0,xmm2\n    jbe     M01_L01\n    vmovaps xmm0,xmm2\n    ret\n\nM01_L01:\n    ret\n
Run Code Online (Sandbox Code Playgroud)\n\n

它使用 CPU 的本机ucomiss操作来比较floats,并使用快速操作在 CPU 的寄存器之间movaps移动这些s。float

\n\n

扩展方法速度较慢,因为它有几个对 的函数调用System.Single.CompareTo(System.Single),这是第一个分支:

\n\n
lea     rcx,[rsp+30h]\nvmovss  dword ptr [rsp+38h],xmm1\ncall    mscorlib_ni+0xda98f0\ntest    eax,eax\njge     M01_L00\nvmovss  xmm0,dword ptr [rsp+38h]\nadd     rsp,28h\nret\n
Run Code Online (Sandbox Code Playgroud)\n\n

让我们看一下 Mono 为该ClampSimple方法生成的本机代码:

\n\n
    cvtss2sd    xmm0,xmm0  \n    movss       xmm1,dword ptr [rsp+8]  \n    cvtss2sd    xmm1,xmm1  \n    comisd      xmm1,xmm0  \n    jbe         M01_L00  \n    movss       xmm0,dword ptr [rsp+8]  \n    cvtss2sd    xmm0,xmm0  \n    cvtsd2ss    xmm0,xmm0  \n    jmp         M01_L01 \n\nM01_L00: \n    movss       xmm0,dword ptr [rsp]  \n    cvtss2sd    xmm0,xmm0  \n    movss       xmm1,dword ptr [rsp+10h]  \n    cvtss2sd    xmm1,xmm1  \n    comisd      xmm1,xmm0  \n    jp          M01_L02\n    jae         M01_L02  \n    movss       xmm0,dword ptr [rsp+10h]  \n    cvtss2sd    xmm0,xmm0  \n    cvtsd2ss    xmm0,xmm0  \n    jmp         M01_L01\n\nM01_L02:\n    movss       xmm0,dword ptr [rsp]  \n    cvtss2sd    xmm0,xmm0  \n    cvtsd2ss    xmm0,xmm0  \n\nM01_L01:\n    add         rsp,18h  \n    ret \n
Run Code Online (Sandbox Code Playgroud)\n\n

Mono 的代码转换floatsdoubles 并使用 进行比较comisd。此外,在准备返回值时,还有奇怪的“转换翻转” float\xe2\x9e\x9e double\xe2\x9e\x9e 。float而且内存和寄存器之间还有更多的移动。这解释了为什么 Mono 的简单方法代码比 RyuJIT 的代码慢。

\n\n

Extension方法代码与 RyuJIT 的代码非常相似,但同样具有奇怪的转换翻转float\xe2\x9e\x9e double\xe2\x9e\x9e float

\n\n
movss       xmm0,dword ptr [rbp-10h]  \ncvtss2sd    xmm0,xmm0  \nmovsd       xmm1,xmm0  \ncvtsd2ss    xmm1,xmm1  \nlea         rbp,[rbp]  \nmov         r11,2061520h  \ncall        r11  \ntest        eax,eax  \njge         M0_L0 \nmovss       xmm0,dword ptr [rbp-10h]  \ncvtss2sd    xmm0,xmm0  \ncvtsd2ss    xmm0,xmm0\nret\n
Run Code Online (Sandbox Code Playgroud)\n\n

看来 RyuJIT 可以生成更高效的代码来处理floats。Mono将floats视为doubles并每次转换值,这也导致CPU寄存器和内存之间额外的值传输。

\n\n

请注意,所有这些仅适用于 Windows x64。我不知道这个基准测试在 Linux 或 Mac 上的表现如何。

\n