代码之家  ›  专栏  ›  技术社区  ›  dymanoid

为什么Mono运行一个简单的方法要慢一些,而RyuJIT运行它要快得多?

  •  8
  • dymanoid  · 技术社区  · 6 年前

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

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

    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
    

    基本上,我想比较这两种钳制实现:

    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;
        }
    }
    

    以下是我的基准方法:

    [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;
    }
    

    我在用 基准点网络 版本0.10.12,包含两个作业:

    [MonoJob]
    [RyuJitX64Job]
    

    我得到的结果是:

    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 |
    

    我可以接受Mono在这里的速度一般比较慢。但我不明白的是:

    为什么Mono要运行 Direct 方法 更慢的 Extension 记住 直接 使用非常简单的比较方法 延伸 使用带有附加方法调用的方法?

    RyuJIT在这里展示了简单方法的4倍优势。

    有人能解释一下吗?

    1 回复  |  直到 6 年前
        1
  •  2
  •   dymanoid    6 年前

    既然没人想做一些拆卸工作,我就回答自己的问题。

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

    RyuJIT为 ClampSimple 方法:

        vucomiss xmm1,xmm0
        jbe     M01_L00
        vmovaps xmm0,xmm1
        ret
    
    M01_L00:
        vucomiss xmm0,xmm2
        jbe     M01_L01
        vmovaps xmm0,xmm2
        ret
    
    M01_L01:
        ret
    

    它使用CPU的本机 ucomiss 要比较的操作 float 也很快 movaps 移动那些 浮动 在CPU寄存器之间。

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

    lea     rcx,[rsp+30h]
    vmovss  dword ptr [rsp+38h],xmm1
    call    mscorlib_ni+0xda98f0
    test    eax,eax
    jge     M01_L00
    vmovss  xmm0,dword ptr [rsp+38h]
    add     rsp,28h
    ret
    

    让我们看看Mono为 子痫样 方法:

        cvtss2sd    xmm0,xmm0  
        movss       xmm1,dword ptr [rsp+8]  
        cvtss2sd    xmm1,xmm1  
        comisd      xmm1,xmm0  
        jbe         M01_L00  
        movss       xmm0,dword ptr [rsp+8]  
        cvtss2sd    xmm0,xmm0  
        cvtsd2ss    xmm0,xmm0  
        jmp         M01_L01 
    
    M01_L00: 
        movss       xmm0,dword ptr [rsp]  
        cvtss2sd    xmm0,xmm0  
        movss       xmm1,dword ptr [rsp+10h]  
        cvtss2sd    xmm1,xmm1  
        comisd      xmm1,xmm0  
        jp          M01_L02
        jae         M01_L02  
        movss       xmm0,dword ptr [rsp+10h]  
        cvtss2sd    xmm0,xmm0  
        cvtsd2ss    xmm0,xmm0  
        jmp         M01_L01
    
    M01_L02:
        movss       xmm0,dword ptr [rsp]  
        cvtss2sd    xmm0,xmm0  
        cvtsd2ss    xmm0,xmm0  
    
    M01_L01:
        add         rsp,18h  
        ret 
    

    Mono的代码转换 floats double 并使用 comisd . 此外,还有一些奇怪的“转换翻转” 浮动 ➞ 双重的 ➞ 浮动 准备返回值时。而且在内存和寄存器之间还有更多的移动。这解释了为什么Mono的simple方法的代码比RyuJIT的代码慢。

    这个 Extension 方法代码与RyuJIT的代码非常相似,但是同样有奇怪的转换翻转 浮动 ➞ 双重的 ➞ 浮动 :

    movss       xmm0,dword ptr [rbp-10h]  
    cvtss2sd    xmm0,xmm0  
    movsd       xmm1,xmm0  
    cvtsd2ss    xmm1,xmm1  
    lea         rbp,[rbp]  
    mov         r11,2061520h  
    call        r11  
    test        eax,eax  
    jge         M0_L0 
    movss       xmm0,dword ptr [rbp-10h]  
    cvtss2sd    xmm0,xmm0  
    cvtsd2ss    xmm0,xmm0
    ret
    

    似乎RyuJIT可以生成更有效的代码来处理 浮动 单核细胞增多症 浮动 作为 双重的 并每次转换这些值,这也会导致CPU寄存器和内存之间的附加值传输。

    请注意,所有这些仅对Windows x64有效。我不知道这个基准在Linux或Mac上会如何运行。