65.9K
CodeProject 正在变化。 阅读更多。
Home

速度的需要 - C++ 与汇编语言

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.76/5 (28投票s)

2017年4月18日

CPOL

8分钟阅读

viewsIcon

111843

downloadIcon

2120

无法击败 C++ 编译器?

引言

乍一看,我们倾向于相信汇编代码,即通过汇编器从ASM(汇编语言)源代码生成的机器代码,应该比编译代码运行得更快,即通过编译器从高级语言源代码生成的机器代码。原因是ASM与机器代码几乎是一对一(同构)映射(ASM可以称为符号机器代码),而编译器至少需要经过一个两阶段的过程,最后一个阶段是翻译成ASM。
过去确实如此,但编译器,尤其是某些C++编译器,在优化方面变得越来越巧妙,在大多数情况下,只有非常有才华的ASM程序员才能与之媲美或做得更好。一如既往,这条规则有很多例外,但我们在此不予详述。举一个例子,涉及向量化SIMD指令的任务目前最好使用ASM来处理,尽管内在函数(实际上是伪装的ASM)几乎可以同样好地完成工作。
尽管如此,ASM仍然是一门重要的编程语言,不仅因为存在需要尽可能接近硬件的任务,而且最重要的是,它提供了通过高级语言学习和实践不可能获得的见解。

为了说明起见,我将介绍一个程序,该程序将首先通过调用C++函数,然后通过调用链接的汇编ASM对象文件来执行一些耗时的计算。耗时的计算意味着几秒钟,尽管程序可以轻松修改以花费更少或更多的时间。我没有采取任何步骤来改进C++代码;编译器必须尽力处理现有的代码。另一方面,我对ASM源代码进行了一些优化,并将汇编性能提高了30%以上。

背景

我们的程序将使用克莱姆法则计算行列式,这是线性方程组的显式公式,其方程数与未知数相同。
只需考虑行列式的计算是克莱姆法则的核心——这足以满足我们的目的,我们将不再花费更多时间在背后的数学上。
克莱姆法则非常适合计算小矩阵的行列式;然而,对于较大的矩阵尺寸,它在计算上变得非常昂贵,因为其时间复杂度为O(n.n!)——计算13x13矩阵的行列式需要几秒钟,14x14矩阵需要几分钟,而计算15x15矩阵的行列式则需要等待一个小时。

使用代码

1)它是如何编译的?

C++源代码是用Visual Studio 2015编译的。编译的Release版本具有以下设置:
最大化速度,使用流式SIMD扩展2(/arch:SSE2),快速(/fp:fast)浮点模型,禁用安全检查(/GS-),不启用C++异常,Win32禁用(/SAFESEH:NO)。

2)它是如何工作的?

该程序将使用克莱姆法则计算13x13矩阵的行列式。大部分时间花在递归和循环上。之所以需要递归,是因为矩阵的行列式是从低阶矩阵(称为“子式”)的行列式计算的,而子式是通过一次消除顶行和一列来获得的。低阶矩阵的行列式以同样的方式计算,依此类推。在我们的程序中,当达到4x4矩阵时,递归结束,其行列式直接计算。4x4、3x3和2x2阶的矩阵的行列式是直接计算的,不进行任何递归。该程序通过三个级别进行循环以准备子矩阵。对于13x13矩阵,外层循环将执行321,408,685次,中间循环执行1,359,245,472次,最内层循环将执行7,191,246,668次。
4x4子式行列式的计算次数相对少得多,仅为259,459,200次。

3)准备矩阵

矩阵用范围在-50.0到+50.0之间的随机浮点数填充,四舍五入到小数点后1位。

    int i, j;
    int numRows, numCols;
    float *inMat;

    srand(time(NULL));
    numRows = 13; /*Change as you like for other matrix orders */
    numCols = numRows;
    inMat = (float*)malloc(numRows*numCols * sizeof(float));
    for (i = 0;i < numRows;i++)
        for (j = 0;j < numCols;j++)
            /* Pointer to matrix, filled as one dimensional array */
            inMat[i*numCols + j] = roundf((((float)rand() / (float)(RAND_MAX)) * 100 - 50) * 10) / 10;

    printf("%dx%d Matrix\n\n", numRows, numCols);
    printf("Input Matrix\n");
    for (int i = 0; i < numRows; i++)
    {
        for (int j = 0; j < numCols; j++)
            printf("%.1f\t", inMat[i*numCols + j]);
        printf("\n");
    }
    printf("\n");

4)测量C++执行时间

void C_example(float* inMat, int rows)
{
    LARGE_INTEGER frequency;
    LARGE_INTEGER t1, t2;
    double elapsedTime;

    QueryPerformanceFrequency(&frequency);
    QueryPerformanceCounter(&t1);
    float det = determinant(inMat, rows);
    QueryPerformanceCounter(&t2);
    elapsedTime = (t2.QuadPart - t1.QuadPart) * 1000.0 / frequency.QuadPart;

    printf("\nDeterminant of Matrix using C++ is %.2f.\n", det);
    printf("Elapsed time: %f miliseconds\n", elapsedTime);
}

5)在C++中计算行列式

float determinant(float*inMat, int rows)
{    
    HANDLE heapPtr;
    float* minorMat;
    int i, j, k, s;
    float det = 0.0f, sign = 1.0f;

    if (4 == rows)
    {
        /*
        float A11 = inMat[0];
        float A12 = inMat[1];
        float A13 = inMat[2];
        float A14 = inMat[3];
        float A21 = inMat[rows];
        float A22 = inMat[rows + 1];
        float A23 = inMat[rows + 2];
        float A24 = inMat[rows + 3];
        float A31 = inMat[rows * 2];
        float A32 = inMat[rows * 2 + 1];
        float A33 = inMat[rows * 2 + 2];
        float A34 = inMat[rows * 2 + 3];
        float A41 = inMat[rows * 3];
        float A42 = inMat[rows * 3 + 1];
        float A43 = inMat[rows * 3 + 2];
        float A44 = inMat[rows * 3 + 3];
        (A13*A24-A14*A23) * ((A31*A42)-(A32*A41)) +
        (A14*A22-A12*A24) * ((A31*A43)-(A33*A41)) +
        (A13*A22-A12*A23) * ((A34*A41)-(A31*A44)) +
        (A11*A24-A14*A21) * ((A32*A43)-(A33*A42)) +
        (A11*A23-A13*A21) * ((A34*A42)-(A32*A44)) +
        (A11*A22-A12*A21) * ((A33*A44)-(A34*A43))
        */
        det = (inMat[2] * inMat[rows + 3] - inMat[3] * inMat[rows + 2]) * ((inMat[rows * 2] * inMat[rows * 3 + 1]) - (inMat[rows * 2 + 1] * inMat[rows * 3])) +
            (inMat[3] * inMat[rows + 1] - inMat[1] * inMat[rows + 3]) * ((inMat[rows * 2] * inMat[rows * 3 + 2]) - (inMat[rows * 2 + 2] * inMat[rows * 3])) +
            (inMat[2] * inMat[rows + 1] - inMat[1] * inMat[rows + 2]) * ((inMat[rows * 2 + 3] * inMat[rows * 3]) - (inMat[rows * 2] *inMat[rows * 3 + 3])) +
            (inMat[0] * inMat[rows + 3] - inMat[3] * inMat[rows]) * ((inMat[rows * 2 + 1] * inMat[rows * 3 + 2]) - (inMat[rows * 2 + 2] * inMat[rows * 3 + 1])) +
            (inMat[0] * inMat[rows + 2] - inMat[2] * inMat[rows]) * ((inMat[rows * 2 + 3] * inMat[rows * 3 + 1]) - (inMat[rows * 2 + 1] * inMat[rows * 3 + 3])) +
            (inMat[0] * inMat[rows + 1] - inMat[1] * inMat[rows]) * ((inMat[rows * 2 + 2] * inMat[rows * 3 + 3]) - (inMat[rows * 2 + 3] * inMat[rows * 3 + 2]));
    }
    else if (3 == rows)
    {
        det = inMat[0] * (inMat[rows + 1] * inMat[rows * 2 + 2] - inMat[rows + 2] * inMat[rows * 2 + 1]) -
            inMat[1] * (inMat[rows] * inMat[rows * 2 + 2] - inMat[rows + 2] * inMat[rows * 2]) +
            inMat[2] * (inMat[rows] * inMat[rows * 2 + 1] - inMat[rows + 1] * inMat[rows * 2]);
    }
    else if (2 == rows)
    {
        det = inMat[0] * inMat[rows + 1] - inMat[1] * inMat[rows];
    }

    else
    {
        heapPtr = GetProcessHeap();
        minorMat = (float*)HeapAlloc(heapPtr, 0, (rows - 1)*(rows - 1) * sizeof(float));
        for (i = 0;i < rows;i++, sign *= -1)
        {
            for (j = 1;j < rows;j++)
            {
                for (s = k = 0;k < rows;k++)
                {
                    if (i != k)
                    {
                        minorMat[((j - 1)*(rows - 1) + s)] = inMat[(j*rows + k)];
                        s++;
                    }
                }
            }

            det += (sign * inMat[i] * determinant(minorMat, rows - 1));
        }
        HeapFree(heapPtr, 0, minorMat);
    }

    return det;
}

 

6)测量汇编ASM的执行时间

void Asm_Example(float* inMat, int rows)
{
    LARGE_INTEGER frequency;
    LARGE_INTEGER t1, t2;
    double elapsedTime;

    QueryPerformanceFrequency(&frequency);
    QueryPerformanceCounter(&t1);
    float det = Determinant(inMat, rows);
    QueryPerformanceCounter(&t2);
    elapsedTime = (t2.QuadPart - t1.QuadPart) * 1000.0 / frequency.QuadPart;

    printf("\nDeterminant of Matrix using Asm is %.2f\n", det);
    printf("Elapsed time: %f miliseconds\n", elapsedTime);
}

请注意,Determinant(大写D)函数是一个外部函数。

extern "C"
{
    float Determinant(void* intMat, size_t rows);
}

7)ASM源代码。

我们有3种源代码变体:1个用于32位,2个用于64位。
32位版本可以使用MASM直接汇编。
对于64位,我们有一个版本准备好使用JWASM(或HJWASM,它是前者的最新演进)进行汇编。JWASM在64位下的功能至少与MASM在32位下的功能相同。换句话说,MASM 64位缺少其自身的32位功能,但它运行良好,并且有其粉丝不断开发宏来弥补不足(或者说,实际上MASM的意思是“宏汇编器”,所以它在你期望做什么方面是名副其实的!)。无论哪种方式,我还制作了一个可以使用MASM 64位进行汇编的版本。

以下是JWASM/HJWASM的64位版本(32位和MASM 64位版本可以从上面下载)

option casemap:none
option frame:auto

_MM_SHUFFLE MACRO fp3, fp2, fp1, fp0
    exitm <( ( ( fp3 ) shl 6)  or  ( ( fp2 ) shl 4 )  or  ( ( fp1 ) shl 2 ) or  ( ( fp0 ) ) ) >
ENDM

_PERMUTE_PS MACRO v, c
    shufps v, v, c
ENDM

_VECTORSWIZZLE MACRO v, fp0, fp1, fp2, fp3
    _PERMUTE_PS v, _MM_SHUFFLE (fp3, fp2, fp1, fp0)
ENDM

_REALSTOXMM MACRO par1, par2, par3, par4
Local xmmValue
  .const
  align 16
  xmmValue dd par1, par2, par3, par4
  .code
  exitm <xmmValue>
ENDM    

T4x4MATRIX struct 16
    r0 XMMWORD ?
    r1 XMMWORD ?
    r2 XMMWORD ?
    r3 XMMWORD ?
T4x4MATRIX ends

TFLOAT3X3 struct 4
    r0 REAL4 3 dup (?)
    r1 REAL4 3 dup (?)
    r2 REAL4 3 dup (?)
TFLOAT3X3 ends

HANDLE TYPEDEF PTR
HeapAlloc proto :HANDLE, : DWORD, :QWORD
HeapFree proto :HANDLE, : DWORD, : PTR
GetProcessHeap proto

.const
_MINUSONE REAL4 -1.0

.code

RecursiveDetCalc proc private FRAME uses rsi rdi r12 r13 r14 inMat : ptr, rows : qword
    LOCAL heapPtr : ptr
    LOCAL det : REAL4
    LOCAL sign : REAL4

    .if rdx==4
        ASSUME rcx : ptr T4x4MATRIX
        movups xmm6, [rcx].r2
        movups xmm1, [rcx].r3
        movaps xmm2, xmm6
        _VECTORSWIZZLE xmm2, 1, 0, 0, 0
        movaps xmm3, xmm1
        _VECTORSWIZZLE xmm3, 2, 2, 1, 1
        movaps xmm4, xmm6
        _VECTORSWIZZLE xmm4, 1, 0, 0, 0
        movaps xmm5, xmm1
        _VECTORSWIZZLE xmm5, 3, 3, 3, 2
        movaps xmm0, xmm6
        _VECTORSWIZZLE xmm0, 2, 2, 1, 1
        movaps xmm7, xmm1
        _VECTORSWIZZLE xmm7, 3, 3, 3, 2

        mulps xmm2, xmm3
        movaps xmm8, xmm2
        mulps xmm4, xmm5
        movaps xmm9, xmm4
        mulps xmm0, xmm7
        movaps xmm10, xmm0
    
        movaps xmm2, xmm6
        _VECTORSWIZZLE xmm2, 2, 2, 1, 1
        movaps xmm3, xmm1
        _VECTORSWIZZLE xmm3, 1, 0, 0, 0
        movaps xmm4, xmm6
        _VECTORSWIZZLE xmm4, 3, 3, 3, 2
        movaps xmm5, xmm1
        _VECTORSWIZZLE xmm5, 1, 0, 0, 0
        movaps xmm0, xmm6
        _VECTORSWIZZLE xmm0, 3, 3, 3, 2
        movaps xmm7, xmm1
        _VECTORSWIZZLE xmm7, 2, 2, 1, 1

        mulps xmm2, xmm3
        movaps xmm3, xmm8
        subps xmm3, xmm2
        movaps xmm8, xmm3
    
        mulps xmm4, xmm5
        subps xmm9, xmm4

        mulps xmm0, xmm7
        subps xmm10, xmm0

        movups xmm6, [rcx].r1
        movaps xmm2, xmm6
        _VECTORSWIZZLE xmm2, 3, 3, 3, 2
        movaps xmm3, xmm6
        _VECTORSWIZZLE xmm3, 2, 2, 1, 1
        movaps xmm4, xmm6
        _VECTORSWIZZLE xmm4, 1, 0, 0, 0
    
        movups xmm0, [rcx].r0
        mulps xmm0, XMMWORD ptr _REALSTOXMM(1.0, -1.0, 1.0, -1.0)

        mulps xmm2, xmm8
    
        mulps xmm3, xmm9
        subps xmm2, xmm3
    
        mulps xmm4, xmm10
        addps xmm4, xmm2
    
        mulps xmm0, xmm4
        shufps xmm4, xmm0, _MM_SHUFFLE(1,0,0,0)
        addps xmm4, xmm0
        shufps xmm0, xmm4, _MM_SHUFFLE(0,3,0,0)
        addps xmm0, xmm4
        _PERMUTE_PS xmm0, _MM_SHUFFLE(2,2,2,2)

        ASSUME rcx : nothing
    .elseif rdx==3 ; Only used for 3x3 matrixes
        ASSUME rcx : ptr TFLOAT3X3
        movups xmm3, XMMWORD ptr [rcx].r0
        mulps xmm3, XMMWORD ptr _REALSTOXMM(1.0,-1.0,1.0,1.0)

        movups xmm0, XMMWORD ptr [rcx].r1+4
        movq xmm2, qword ptr [rcx].r2+4
        shufps xmm0, xmm2, _MM_SHUFFLE(1,0,1,0)
        ;calculate first determinant
        shufps xmm1, xmm0, _MM_SHUFFLE(0,1,0,0)
        mulps xmm0, xmm1
        shufps xmm1, xmm0, _MM_SHUFFLE(2,0,0,0)
        shufps xmm0, xmm0, _MM_SHUFFLE(3,3,3,3)
        shufps xmm1, xmm1, _MM_SHUFFLE(3,3,3,3)
        subps xmm0, xmm1
        movaps xmm4, xmm0
    
        movups xmm0, XMMWORD ptr [rcx].r1
        movups xmm2, XMMWORD ptr [rcx].r1+8
        shufps xmm0, xmm2, _MM_SHUFFLE(3,1,2,0)
        ;calculate second determinant
        shufps xmm1, xmm0, _MM_SHUFFLE(0,1,0,0)
        mulps xmm0, xmm1
        shufps xmm1, xmm0, _MM_SHUFFLE(2,0,0,0)
        shufps xmm0, xmm0, _MM_SHUFFLE(3,3,3,3)
        shufps xmm1, xmm1, _MM_SHUFFLE(3,3,3,3)
        subps xmm0, xmm1
        movaps xmm5, xmm0
    
        movups xmm0, XMMWORD ptr [rcx].r1
        movq xmm2, qword ptr [rcx].r2
        shufps xmm0, xmm2, _MM_SHUFFLE(1,0,1,0)
        ;calculate third determinant
        shufps xmm1, xmm0, _MM_SHUFFLE(0,1,0,0)
        mulps xmm0, xmm1
        shufps xmm1, xmm0, _MM_SHUFFLE(2,0,0,0)
        shufps xmm0, xmm0, _MM_SHUFFLE(3,3,3,3)
        shufps xmm1, xmm1, _MM_SHUFFLE(3,3,3,3)
        subps xmm0, xmm1

        unpckhps xmm4, xmm5
        shufps xmm4, xmm0, _MM_SHUFFLE(0,0,1,0)
        mulps xmm3, xmm4
    
        movaps xmm0, xmm3
        movhlps xmm0, xmm0
        addps xmm0, xmm3
        shufps xmm3, xmm3, 1
        addss xmm0, xmm3
        shufps xmm0, xmm0,0
        ASSUME rcx : nothing        
    .elseif rdx==2 ; Only used for 2x2 matrixes
        movss xmm0, dword ptr [rcx+12]
        movss xmm1, dword ptr [rcx+8]
        mulss xmm0, dword ptr [rcx]
        mulss xmm1, dword ptr [rcx+4]
        subss xmm0, xmm1
    .else
        mov rsi, rcx
        mov rows, rdx
        mov r13, rdx
        mov det, 0.0
        mov sign, 1.0; sign

        INVOKE GetProcessHeap
        mov heapPtr, rax  
        mov rax, rows
        dec rax
        mul rax
        shl rax, 2
        INVOKE HeapAlloc, heapPtr, 0, rax ; space for new (n-1)x(n-1) matrix
        mov rdi, rax        
        
        mov r12,0

        .while r12<r13
            mov r10, 1
            .while r10<r13
                mov r11,0
                mov r9,0
            
                lea rax, [4*r13]
                mul  r10
                mov r14, rax

                lea rax, [4*r13-4]
                lea rcx, [r10-1]
                mul rcx
                mov r8, rax

                .while r11<r13
                    .if r12 != r11
                        lea rcx, [r14 + 4*r11]
                        mov eax, REAL4 ptr [rsi+rcx]
                        lea rcx, [r8+4*r9]
                        mov REAL4 ptr [rdi+rcx], eax
                        add r9, 1
                    .endif
                    add r11, 1
                .endw
                add r10, 1
            .endw
            
            lea rdx, [r13-1]
            INVOKE RecursiveDetCalc, rdi, rdx

            movss xmm1, dword ptr [rsi+4*r12]
            mulss xmm1, sign
            mulss xmm0, xmm1    
            
            addss xmm0, det
            
            movss xmm1, sign
            movss det, xmm0
            
            mulss xmm1, _MINUSONE
            movss sign, xmm1
            add r12, 1
        .endw
        INVOKE HeapFree, heapPtr, 0, rdi
        movss xmm0, det
    .endif
    ret
RecursiveDetCalc endp

Determinant proc public FRAME uses xmm6 xmm7 xmm8 xmm9 xmm10 inMat : ptr, rows : qword
    ; Save xmm registers here and do the calculation in the recursive function.
    
    INVOKE RecursiveDetCalc, rcx, rdx
    ret
Determinant endp

end

8)编译64位Release版本并运行后,您将获得类似下面图像的结果。

 


忽略行列式值的差异,这是由于不同计算变体下的舍入误差造成的。

 

为什么ASM更慢?

如前所述,我对ASM代码进行了许多优化,但仍不足以击败C++编译。SIMD部分,特别是4x4子式行列式计算,在ASM代码中更快,但它们在整个过程的总耗时中所占比例不大。
我看到了C++编译器生成的ASM列表,有些部分简直令人难以置信——没有人会相信人类会那样编码(如果他这样做了,代码将几乎无法维护)。编译器以自动化方式使用了各种技巧——难以超越。它了解管道和预测分支的所有工作原理,它重新排序指令,进行循环切片并使用缓存无关算法。

 


更新1

在撰写本文时,与其它编程语言或编译器相比,ASM编程仍然能产生更快お代码。现在断言ASM编程已经过时还为时过早。我选择了两种流行的语言和环境,并复制了C++代码。

Lazarus/Free Pascal

Lazarus是一个IDE,具有Delphi(旧版本)的大部分RAD功能。其集成的Free Pascal编译器似乎能生成比Delphi更快的64位可执行文件(尽管32位稍慢)。
与Visual Studio C++编译器相比,它在我们测试中落后了,花费了将近30%的时间。
正如您所知,对于大多数应用程序来说,这不成问题。

您可以从上面下载测试的完整Free Pascal源代码。有几点需要提及。

  • 对于32位,您需要使用JWASM/HJWASM(使用-zt0开关)而不是MASM来汇编ASM,以删除API调用的STDCALL名称修饰(例如_HeapAlloc@12)。Free Pascal(以及Delphi)无法处理这一点。
  • 与Delphi不同,Free Pascal在链接时无法在ASM代码中处理API引用,而无需包装函数。这是一个不便之处,尽管对整体性能没有实质性影响。

 

 


Visual Studio/C#

C#是微软发明的一个不错的编程语言。在其当前实现中,它与庞大的.Net Framework和庞大的公共语言运行时(Common Language Runtime)紧密集成。
尽管C#的性能多年来一直在不断提高,但在我们的测试中,JIT编译器在速度方面并未产生惊人的结果。我确实相信C#源代码可以针对速度进行改进,这一点毫无疑问,但C++和Free Pascal也可以。因此,不要急于得出结论。

您可以从上面下载测试的完整C#源代码。照常,ASM通过C++ CLI类调用,否则我们需要另一个DLL。

  • 我们使用了VS 2015。您必须为x64或x86 CPU构建,而不是为AnyCpu构建。
  • 您也可以通过简单的批处理文件将其构建为单个x86或x64可执行文件(完全没有DLL),如您的下载中的CSharpSingleExe目录所示。这是基于我们的文章在独立的64位exe中混合.NET和汇编语言

 

 

更新2

我决定在不使用堆内存的情况下,仅使用堆栈内存来复制C++与ASM的测试。
堆分配的影响是真实的,但并非首要(约16-18%),并且对C++和ASM的影响程度(几乎)相同。结论仍然是相同的——正如预期的那样——但测试现在更加透明。更改后的代码可以从上面下载(CalcDetStackMem.zip)。

 

 

更新历史

  • 2017年4月18日 - 初始文章
  • 2017年4月28日 - 包含Free Pascal和C#的等效测试。x86和x64的ASM进行了小型修复。
  • 2017年5月5日 - C++与ASM测试使用堆栈而不是堆内存分配。

 

© . All rights reserved.