【问题标题】:Why is calling masm from C# so slow?为什么从 C# 调用 masm 这么慢?
【发布时间】:2017-09-09 21:06:03
【问题描述】:

这是一个 64 位 masm 程序,将两个 64 位数字相乘,再将一个 64 位数字相加得到 128 位结果(使用标准的 64 位调用约定):

; public static extern ulong MulAdd64(ulong U, ulong V, ref ulong k);
; Return  (U*V + k) % ß  and set  k = (U*V + k) / ß.
;  U in rcx, V in rdx, &k in r8
; Note  0  <=  0*0 + 0  <=  (ß-1)*(ß-1) + (ß-1) = ß*(ß-1)  <  ß^2
MulAdd64  proc  public
        mov     rax,rcx
        mul     rdx
        add     rax,qword ptr [r8]      ; low part of product
        adc     rdx,0
        mov     qword ptr [r8],rdx      ; high part of product
        ret
MulAdd64  endp

这是通过以下方式导入到 C# 代码中的:

    [DllImport(@"C:\path\MulAdd64.dll")]
    public static extern ulong MulAdd64 (ulong U, ulong V, ref ulong k);

下面是用 C# 编写的同一个函数以及一个测试程序:

public static void TestCS_Masm_Speed ()
{
    ulong x = 3141592653589793238, y = 2718281828459045, aux = 1234567890123456789, lo = 0;
    // just in case the first invocation is excessivly slow
    //lo = MulAdd64(x, y, ref aux);
    lo = CS_MulAdd64(x, y, ref aux);
    Stopwatch sw = new Stopwatch();
    sw.Restart();
    for (int i = 0; i < 10000000; i++) {
        //lo = MulAdd64(x, y, ref aux);
        lo = CS_MulAdd64(x, y, ref aux);
    }
    Console.WriteLine("Ticks = {0}", sw.ElapsedTicks);
    // verify low-order results  hi:lo = x*y + aux;
    if (x*y+aux != lo) Console.WriteLine("Error in low order result");
    else Console.WriteLine("Low order result is OK");
}

[DllImport(@"C:\path\MulAdd64.dll")]
public static extern ulong MulAdd64 (ulong U, ulong V, ref ulong k);

/*
    Multiplication. We need to multiply two unsigned 64-bit integers, obtaining an
    unsigned 128-bit product. Using Algorithm 4.3.1M of Seminumerical Algorithms,
    with ß = 2^32, the following subroutine computes hi:lo = y*z + aux .  Then
    sets aux to hi and return lo.
*/
public static ulong CS_MulAdd64 (ulong y, ulong z, ref ulong aux)
{
    ulong[] u = new ulong[2],  v = new ulong[2],  w = new ulong[4];
    // Unpack the multiplier, multiplicand, and aux  to  u, v, and w 
    u[1] = (ulong)y >> 32;      u[0] = (ulong)y & 0xFFFFFFFF;
    v[1] = (ulong)z >> 32;      v[0] = (ulong)z & 0xFFFFFFFF;
    w[1] = (ulong)aux >> 32;    w[0] = (ulong)aux & 0xFFFFFFFF;
    // Multiply
    for (int j = 0; j < 2; j++) {
        ulong k = 0;
        for (int i = 0; i < 2; i++) {
            ulong t = u[i] * v[j] + w[i+j] + k;
            w[i+j] = t & 0xFFFFFFFF; k = t >> 32;
        }
        w[j+2] = k;
    }
    // Pack w into the outputs aux and return w
    aux = ((ulong)w[3] << 32) + (ulong)w[2];
    return ((ulong)w[1] << 32) + (ulong)w[0];
}   

优化后的 C# 代码比 masm 代码长得多(153 条指令对 6 条指令),但运行速度几乎是两倍(941694 滴答对 1722289 滴答)!怎么会这样?一切都在寄存器中传递,没有内存可以固定!显然,在 C# 的调用和 masm 中的执行之间发生了一些事情,但是什么?我无法进入该代码。

【问题讨论】:

  • 源代码的长度与执行时间并没有很好的相关性。这个基本假设就是这里的问题所在。
  • but runs faster。发布您如何衡量它的代码。
  • 据我所知,跨越托管/本机边界时会产生开销......此外,也许编译器能够比您想象的更优化您的长代码,因此实际代码可能是比你写的更优化。
  • 通常在循环中进行此类性能测试,您可能会通过对任一侧的循环进行计时来了解托管 本机转换。
  • 我同意@Servy 的观点,即源代码与执行时间并没有很好的相关性。我的观点是,masm 中的 6 条指令乘加远没有解包、执行 4 次乘法、重新打包和执行各种开销所需的 C# 代码那么复杂。

标签: c# masm slowdown


【解决方案1】:

通过使用 Google,您可以更好地了解与托管/原生转换相关的开销。以下是一些文章:

在像您这样的情况下,调整调用约定和其他所需调整的开销可能高于调用本身。

通常,如果您进行了足够多的此类乘法运算,您会将它们批处理在一起以最大程度地减少开销。而且你会做一些测试来找到最佳大小。但是,如果你真的很重视性能,那么你会考虑缓存大小、并行处理等一切......

我相信 C# 编译器可以相对较好地优化您的代码,以便在 CPU 级别的指令可以正确排序,从而几乎没有浪费的周期。它可能能够使用 SIMD 或其他技巧来优化代码。否则,您可能可以在 C# 中编写更高效的乘法。

顺便说一句,既然 Windows 已经提供了这样的乘法函数:UnsignedMultiply128 function,你为什么还要编写自己的代码?

您是否比较了BigInteger 的性能?

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-10-26
    • 1970-01-01
    • 2015-10-05
    • 1970-01-01
    • 1970-01-01
    • 2021-09-03
    相关资源
    最近更新 更多