Обычный код С++ в 10 раз быстрее, чем встроенный ассемблер. Зачем?

Эти два фрагмента кода выполняют одно и то же: добавьте два массива с плавающей точкой и сохраните результат в них.

Inline Assembler:

void vecAdd_SSE(float* v1, float* v2) { 
    _asm {
        mov esi, v1
        mov edi, v2
        movups xmm0, [esi]
        movups xmm1, [edi]
        addps xmm0, xmm1
        movups [esi], xmm0
        movups [edi], xmm0
    }
}

Обычный код С++:

void vecAdd_Std(float* v1, float* v2) {
    v1[0] = v1[0]+ v2[0];
    v1[1] = v1[1]+ v2[1];
    v1[2] = v1[2]+ v2[2];
    v1[3] = v1[3]+ v2[3];

    v2[0] = v1[0];
    v2[1] = v1[1];
    v2[2] = v1[2];
    v2[3] = v1[3];
}

Разборка для кода С++ (разборка в режиме отладки, потому что по какой-то причине я не могу просмотреть демонтаж в режиме Release):

 void vecAdd_Std(float* v1, float* v2) {
 push        ebp  
 mov         ebp,esp  
 sub         esp,0C0h  
 push        ebx  
 push        esi  
 push        edi  
 lea         edi,[ebp-0C0h]  
 mov         ecx,30h  
 mov         eax,0CCCCCCCCh  
 rep stos    dword ptr es:[edi]  

    v1[0] = v1[0]+ v2[0];
 mov         eax,4  
 imul        ecx,eax,0  
 mov         edx,4  
 imul        eax,edx,0  
 mov         edx,dword ptr [v1]  
 mov         esi,dword ptr [v2]  
 movss       xmm0,dword ptr [edx+ecx]  
 addss       xmm0,dword ptr [esi+eax]  
 mov         eax,4  
 imul        ecx,eax,0  
 mov         edx,dword ptr [v1]  
 movss       dword ptr [edx+ecx],xmm0  
    v1[1] = v1[1]+ v2[1];
 mov         eax,4  
 shl         eax,0  
    v1[1] = v1[1]+ v2[1];
 mov         ecx,4  
 shl         ecx,0  
 mov         edx,dword ptr [v1]  
 mov         esi,dword ptr [v2]  
 movss       xmm0,dword ptr [edx+eax]  
 addss       xmm0,dword ptr [esi+ecx]  
 mov         eax,4  
 shl         eax,0  
 mov         ecx,dword ptr [v1]  
 movss       dword ptr [ecx+eax],xmm0  
    v1[2] = v1[2]+ v2[2];
 mov         eax,4  
 shl         eax,1  
 mov         ecx,4  
 shl         ecx,1  
 mov         edx,dword ptr [v1]  
 mov         esi,dword ptr [v2]  
 movss       xmm0,dword ptr [edx+eax]  
 addss       xmm0,dword ptr [esi+ecx]  
 mov         eax,4  
 shl         eax,1  
 mov         ecx,dword ptr [v1]  
 movss       dword ptr [ecx+eax],xmm0  
    v1[3] = v1[3]+ v2[3];
 mov         eax,4  
 imul        ecx,eax,3  
 mov         edx,4  
 imul        eax,edx,3  
 mov         edx,dword ptr [v1]  
 mov         esi,dword ptr [v2]  
 movss       xmm0,dword ptr [edx+ecx]  
 addss       xmm0,dword ptr [esi+eax]  
 mov         eax,4  
 imul        ecx,eax,3  
 mov         edx,dword ptr [v1]  
 movss       dword ptr [edx+ecx],xmm0  

    v2[0] = v1[0];
 mov         eax,4  
 imul        ecx,eax,0  
 mov         edx,4  
 imul        eax,edx,0  
 mov         edx,dword ptr [v2]  
 mov         esi,dword ptr [v1]  
 mov         ecx,dword ptr [esi+ecx]  
 mov         dword ptr [edx+eax],ecx  
    v2[1] = v1[1];
 mov         eax,4  
 shl         eax,0  
 mov         ecx,4  
 shl         ecx,0  
 mov         edx,dword ptr [v2]  
 mov         esi,dword ptr [v1]  
 mov         eax,dword ptr [esi+eax]  
 mov         dword ptr [edx+ecx],eax  
    v2[2] = v1[2];
 mov         eax,4  
 shl         eax,1  
 mov         ecx,4  
 shl         ecx,1  
 mov         edx,dword ptr [v2]  
 mov         esi,dword ptr [v1]  
 mov         eax,dword ptr [esi+eax]  
 mov         dword ptr [edx+ecx],eax  
    v2[3] = v1[3];
 mov         eax,4  
 imul        ecx,eax,3  
 mov         edx,4  
 imul        eax,edx,3  
 mov         edx,dword ptr [v2]  
 mov         esi,dword ptr [v1]  
 mov         ecx,dword ptr [esi+ecx]  
 mov         dword ptr [edx+eax],ecx  

}

Теперь я сделал измерение времени для этих функций и заметил, что код встроенного ассемблера занимает примерно 10 раз дольше (в режиме Release). Кто-нибудь знает, почему?

Ответ 1

На моей машине (64-разрядный режим VS2015) компилятор строит vecAdd_Std и производит

00007FF625921C8F  vmovups     xmm1,xmmword ptr [[email protected] (07FF625929D60h)]  
00007FF625921C97  vmovups     xmm4,xmm1  
00007FF625921C9B  vcvtss2sd   xmm1,xmm1,xmm4  

Тестовый код

int main() {
    float x[4] = {1.0, 2.0, 3.0, 4.0};
    float y[4] = {1.0, 2.0, 3.0, 4.0};

    vecAdd_Std(x, y);

    std::cout << x[0];
}

Ответ 2

Вы действительно не вызываете функцию, которая выполняет одну инструкцию SSE, не так ли? Там нетривиальные накладные расходы связаны с настройкой регистров xmm, и вы копируете значения из памяти в регистры и обратно, что займет гораздо больше времени, чем фактический расчет.

Я бы не удивился, обнаружив, что компилятор встроил С++-версию этой функции, но не выполняет (не может, действительно) то же самое для функций, которые содержат встроенную сборку.