2017-02-11 7 views
6

제목은 넌센스로 보일 수 있지만 설명하겠습니다. 나는 다음과 같은 어셈블리 코드가 발생했을 때 다른 일 프로그램을 공부 :shufps가 메모리 액세스보다 느린가요?

movaps xmm3, xmmword ptr [rbp-30h] 
lea  rdx, [rdi+1320h] 
movaps xmm5, xmm3 
movaps xmm6, xmm3 
movaps xmm0, xmm3 
movss dword ptr [rdx], xmm3 
shufps xmm5, xmm3, 55h 
shufps xmm6, xmm3, 0AAh 
shufps xmm0, xmm3, 0FFh 
movaps xmm4, xmm3 
movss dword ptr [rdx+4], xmm5 
movss dword ptr [rdx+8], xmm6 
movss dword ptr [rdx+0Ch], xmm0 
mulss xmm4, xmm3 

을하며 네 개의 수레 [RDX]에 [RBP-30H]에서 대부분 그것을 단지 복사처럼 보인다. 그 중 shufpsxmm3에서 4 개의 부동 소수점 중 하나를 선택하는 데 사용됩니다 (예 : shufps xmm5, xmm3, 55h은 두 번째 부동 소수점을 선택하고 xmm5에 넣음).

이것은 shufps이 실제로 메모리 액세스 (예 : movss xmm0, dword ptr [rbp-30h], movss dword ptr [rdx], xmm0)보다 빠르기 때문에 컴파일러가 그렇게했는지 궁금합니다.

그래서 두 가지 접근법을 비교하기위한 몇 가지 테스트를 작성했으며 shufps은 항상 여러 메모리 액세스보다 느린 것으로 나타났습니다. 이제 shufps의 사용이 성능과 관련이 없다고 생각합니다. decompilers가 깨끗한 코드를 쉽게 생성 할 수 없도록 (IDA pro로 시도한 결과 실제로 복잡성이 너무 많았습니다) 코드를 난독화할 수 있습니다.

실용적인 프로그램에서 컴파일러가 나보다 똑똑하기 때문에 어쨌든 (예 : _mm_shuffle_ps을 사용하여) shufps을 명시 적으로 사용하지는 않지만, 컴파일 된 컴파일러가 왜 그런 코드를 생성했는지 알고 싶습니다. . 그것은 더 빠르지도 작지도 않습니다. 그것은 말도 안돼.

어쨌든 나는 아래에 쓴 시험을 제공 할 것이다. 테스트에서

#include <Windows.h> 
#include <iostream> 

using namespace std; 

__declspec(noinline) DWORD profile_routine(void (*routine)(void *), void *arg, int iterations = 1) 
{ 
    DWORD startTime = GetTickCount(); 
    while (iterations--) 
    { 
     routine(arg); 
    } 
    DWORD timeElapsed = GetTickCount() - startTime; 
    return timeElapsed; 
} 


struct Struct 
{ 
    float x, y, z, w; 
}; 

__declspec(noinline) Struct shuffle1(float *arr) 
{ 
    float x = arr[3]; 
    float y = arr[2]; 
    float z = arr[0]; 
    float w = arr[1]; 

    return {x, y, z, w}; 
} 


#define SS0  (0x00) 
#define SS1  (0x55) 
#define SS2  (0xAA) 
#define SS3  (0xFF) 
__declspec(noinline) Struct shuffle2(float *arr) 
{ 
    Struct r; 
    __m128 packed = *reinterpret_cast<__m128 *>(arr); 

    __m128 x = _mm_shuffle_ps(packed, packed, SS3); 
    __m128 y = _mm_shuffle_ps(packed, packed, SS2); 
    __m128 z = _mm_shuffle_ps(packed, packed, SS0); 
    __m128 w = _mm_shuffle_ps(packed, packed, SS1); 

    _mm_store_ss(&r.x, x); 
    _mm_store_ss(&r.y, y); 
    _mm_store_ss(&r.z, z); 
    _mm_store_ss(&r.w, w); 

    return r; 
} 



void profile_shuffle_r1(void *arg) 
{ 
    float *arr = static_cast<float *>(arg); 
    Struct q = shuffle1(arr); 
    arr[0] += q.w; 
    arr[1] += q.z; 
    arr[2] += q.y; 
    arr[3] += q.x; 
} 
void profile_shuffle_r2(void *arg) 
{ 
    float *arr = static_cast<float *>(arg); 
    Struct q = shuffle2(arr); 
    arr[0] += q.w; 
    arr[1] += q.z; 
    arr[2] += q.y; 
    arr[3] += q.x; 
} 

int main(int argc, char **argv) 
{ 
    int n = argc + 3; 
    float arr1[4], arr2[4]; 
    for (int i = 0; i < 4; i++) 
    { 
     arr1[i] = static_cast<float>(n + i); 
     arr2[i] = static_cast<float>(n + i); 
    } 

    int iterations = 20000000; 
    DWORD time1 = profile_routine(profile_shuffle_r1, arr1, iterations); 
    cout << "time1 = " << time1 << endl; 
    DWORD time2 = profile_routine(profile_shuffle_r2, arr2, iterations); 
    cout << "time2 = " << time2 << endl; 

    return 0; 
} 

위, 저는 두 셔플 방법 shuffle1과 같은 일을 shuffle2 있습니다. MSVC -O2로 컴파일하면 다음 코드를 생성합니다

shuffle1: 
mov   eax,dword ptr [rdx+0Ch] 
mov   dword ptr [rcx],eax 
mov   eax,dword ptr [rdx+8] 
mov   dword ptr [rcx+4],eax 
mov   eax,dword ptr [rdx] 
mov   dword ptr [rcx+8],eax 
mov   eax,dword ptr [rdx+4] 
mov   dword ptr [rcx+0Ch],eax 
mov   rax,rcx 
ret 
shuffle2: 
movaps  xmm2,xmmword ptr [rdx] 
mov   rax,rcx 
movaps  xmm0,xmm2 
shufps  xmm0,xmm2,0FFh 
movss  dword ptr [rcx],xmm0 
movaps  xmm0,xmm2 
shufps  xmm0,xmm2,0AAh 
movss  dword ptr [rcx+4],xmm0 
movss  dword ptr [rcx+8],xmm2 
shufps  xmm2,xmm2,55h 
movss  dword ptr [rcx+0Ch],xmm2 
ret 

shuffle1 내 컴퓨터에 shuffle2 항상보다 30 % 이상 빠르다. 나는 shuffle2에 두 개의 명령어가 있고 shuffle1은 실제로 xmm0 대신 eax을 사용 했으므로 일부 정크 산술 연산을 추가하면 결과가 달라질 것이라고 생각했습니다.

__declspec(noinline) Struct shuffle1(float *arr) 
{ 
    float x0 = arr[3]; 
    float y0 = arr[2]; 
    float z0 = arr[0]; 
    float w0 = arr[1]; 

    float x = x0 + y0 + z0; 
    float y = y0 + z0 + w0; 
    float z = z0 + w0 + x0; 
    float w = w0 + x0 + y0; 

    return {x, y, z, w}; 
} 


#define SS0  (0x00) 
#define SS1  (0x55) 
#define SS2  (0xAA) 
#define SS3  (0xFF) 
__declspec(noinline) Struct shuffle2(float *arr) 
{ 
    Struct r; 
    __m128 packed = *reinterpret_cast<__m128 *>(arr); 

    __m128 x0 = _mm_shuffle_ps(packed, packed, SS3); 
    __m128 y0 = _mm_shuffle_ps(packed, packed, SS2); 
    __m128 z0 = _mm_shuffle_ps(packed, packed, SS0); 
    __m128 w0 = _mm_shuffle_ps(packed, packed, SS1); 

    __m128 yz = _mm_add_ss(y0, z0); 
    __m128 x = _mm_add_ss(x0, yz); 
    __m128 y = _mm_add_ss(w0, yz); 

    __m128 wx = _mm_add_ss(w0, x0); 
    __m128 z = _mm_add_ss(z0, wx); 
    __m128 w = _mm_add_ss(y0, wx); 

    _mm_store_ss(&r.x, x); 
    _mm_store_ss(&r.y, y); 
    _mm_store_ss(&r.z, z); 
    _mm_store_ss(&r.w, w); 

    return r; 
} 

를 그들이 지침의 같은 번호가 모두가 XMM 레지스터를 사용할 필요로 이제 어셈블리는 좀 더 공정 같습니다

그래서 나는 다음과 같은를 수정했습니다.

shuffle1: 
movss  xmm5,dword ptr [rdx+8] 
mov   rax,rcx 
movss  xmm3,dword ptr [rdx+0Ch] 
movaps  xmm0,xmm5 
movss  xmm2,dword ptr [rdx] 
addss  xmm0,xmm3 
movss  xmm4,dword ptr [rdx+4] 
movaps  xmm1,xmm2 
addss  xmm1,xmm5 
addss  xmm0,xmm2 
addss  xmm1,xmm4 
movss  dword ptr [rcx],xmm0 
movaps  xmm0,xmm4 
addss  xmm0,xmm2 
addss  xmm4,xmm3 
movss  dword ptr [rcx+4],xmm1 
addss  xmm0,xmm3 
addss  xmm4,xmm5 
movss  dword ptr [rcx+8],xmm0 
movss  dword ptr [rcx+0Ch],xmm4 
ret 
shuffle2: 
movaps  xmm4,xmmword ptr [rdx] 
mov   rax,rcx 
movaps  xmm3,xmm4 
movaps  xmm5,xmm4 
shufps  xmm5,xmm4,0AAh 
movaps  xmm2,xmm4 
shufps  xmm2,xmm4,0FFh 
movaps  xmm0,xmm5 
addss  xmm0,xmm3 
shufps  xmm4,xmm4,55h 
movaps  xmm1,xmm4 
addss  xmm1,xmm2 
addss  xmm2,xmm0 
addss  xmm4,xmm0 
addss  xmm3,xmm1 
addss  xmm5,xmm1 
movss  dword ptr [rcx],xmm2 
movss  dword ptr [rcx+4],xmm4 
movss  dword ptr [rcx+8],xmm3 
movss  dword ptr [rcx+0Ch],xmm5 
ret 

그러나 상관 없습니다. shuffle1은 여전히 ​​30 % 빨라졌습니다!

+0

드문 경우이지만 수작업으로 작성한 어셈블리 일 수 있습니다. – tambre

+0

@tambre 예 나는 이것에 대해 생각했지만 그렇게할만한 이유를 생각할 수 없다. 이것은 수억 개의 코드 라인을 가지고있는 거대한 프로그램에서 나온 것입니다. 그들이 복잡성에도 불구하고 프로그램의 특정 부분을 최적화하고 싶다면. 그들이 최적화가 아니라 반대인지 확인해야하는 이유는 무엇입니까? 따라서 나는 컴파일러를 비난했다. – MegaStupidMonkeys

+0

정렬 된 메모리 액세스는 오래된 프로세서에서 상당히 빨랐다. 그래서 컴파일러는 네 개의 4 바이트 정렬되지 않은로드 대신에 한 개의 16 바이트 정렬 된로드를 수행하는 것을 선호했습니다. 또한 컴파일러는 부동 소수점 데이터에 'eax'와 같은 레지스터를 사용할 수 없습니다. 마지막으로, 메모리로드 속도와 셔플 명령어를 비교하는 것이 현명하지 않음에 유의하십시오. 이 두 가지 유형의 명령어는 CPU 내에서 별도의 실행 단위를 사용하기 때문에 병렬로 실행할 수 있습니다. 실제 성능은 여기 병목 현상이 무엇이든간에 정의됩니다 ... – stgatilov

답변

2

넓은 의미의 문맥이 없으면 확실하게 말하기는 어렵지만 새로운 프로세서를 최적화 할 때는 다른 포트의 사용을 고려해야합니다. 여기 Agers를 참조하십시오 : http://www.agner.org/optimize/instruction_tables.pdf

이 경우에는 거의 불가능할 수도 있지만 조립품이 실제로 최적화되었다고 가정 할 경우 몇 가지 가능성이 있습니다.

  1. 이는 아웃 - 오브 - 주문 스케줄러 (예를 들어 하 스웰을 사용하여 다시) 포트 2와 3보다 (예를 들어, 하 스웰에) 포트 5 개를 가지고 어떻게 코드의 스트레칭에 게재 될 수 있습니다 유효한.
  2. # 1과 유사하지만 하이퍼 스레딩 할 때 동일한 효과가 관찰 될 수 있습니다. 이 코드는 형제 하이퍼 스레드에서 읽기 작업을 도용하지 않을 수 있습니다.
  3. 마지막으로, 최적화와 같은 종류의 특정 및 특정 비슷한 사용했습니다. 실행 시간이 100 %에 가까운 분기를 컴파일 할 때가 아니라 예측할 수 있다고 가정 해보십시오. 상상해 봅시다. 분기 직후에는 종종 캐시 미스가있는 읽기가 있습니다. 가능한 빨리 읽으려고합니다. 아웃 오브 오더 스케줄러는 미리 읽기를 수행하고 읽기 포트를 사용하지 않는다면 읽기를 시작합니다. 이것은 shufps 명령을 본질적으로 "자유"로 실행할 수 있습니다.

    MOV ecx, [some computed, mostly constant at run-time global] 
    label loop: 
        ADD rdi, 16 
        ADD rbp, 16 
        CALL shuffle 
        SUB ecx, 1 
        JNE loop 
    
    MOV rax, [rdi] 
    
    ;do a read that could be "predicted" properly 
    MOV rbx, [rax] 
    

솔직히하지만, 그냥 잘못 작성된 어셈블리 또는 잘못 생성 된 기계어 코드처럼 보이는, 그래서 많이로 생각 넣어 않을 것 : 여기에서 그 예이다. 내가주는 예제는 거의 없을 것 같아요.