제목은 넌센스로 보일 수 있지만 설명하겠습니다. 나는 다음과 같은 어셈블리 코드가 발생했을 때 다른 일 프로그램을 공부 :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]에서 대부분 그것을 단지 복사처럼 보인다. 그 중 shufps
은 xmm3
에서 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 % 빨라졌습니다!
드문 경우이지만 수작업으로 작성한 어셈블리 일 수 있습니다. – tambre
@tambre 예 나는 이것에 대해 생각했지만 그렇게할만한 이유를 생각할 수 없다. 이것은 수억 개의 코드 라인을 가지고있는 거대한 프로그램에서 나온 것입니다. 그들이 복잡성에도 불구하고 프로그램의 특정 부분을 최적화하고 싶다면. 그들이 최적화가 아니라 반대인지 확인해야하는 이유는 무엇입니까? 따라서 나는 컴파일러를 비난했다. – MegaStupidMonkeys
정렬 된 메모리 액세스는 오래된 프로세서에서 상당히 빨랐다. 그래서 컴파일러는 네 개의 4 바이트 정렬되지 않은로드 대신에 한 개의 16 바이트 정렬 된로드를 수행하는 것을 선호했습니다. 또한 컴파일러는 부동 소수점 데이터에 'eax'와 같은 레지스터를 사용할 수 없습니다. 마지막으로, 메모리로드 속도와 셔플 명령어를 비교하는 것이 현명하지 않음에 유의하십시오. 이 두 가지 유형의 명령어는 CPU 내에서 별도의 실행 단위를 사용하기 때문에 병렬로 실행할 수 있습니다. 실제 성능은 여기 병목 현상이 무엇이든간에 정의됩니다 ... – stgatilov