我一直在优化一些代码,并偶然发现了一些特殊的情况。 这是两个汇编代码:
; FAST
lea rcx,[rsp+50h]
call qword ptr [Random_get_float3] ;this function only writes 3 components
movaps xmm0,xmmword ptr [rsp+50h]
lea rbx,[rbx+0Ch]
mulps xmm0,xmm6
movlps qword ptr [rbx-0Ch],xmm0
movaps xmmword ptr [rsp+50h],xmm0
extractps eax,xmm0,2
mov dword ptr [rbx-4],eax
; SLOW
lea rcx,[rsp+50h]
call qword ptr [Random_get_float3] ;this function only writes 3 components
movaps xmm0,xmmword ptr [rsp+50h]
lea rbx,[rbx+0Ch]
mulps xmm0,xmm6
movlps qword ptr [rbx-0Ch],xmm0
extractps eax,xmm0,2
mov dword ptr [rbx-4],eax
两个版本都在紧密循环中执行 10000 次(省略相同的循环代码)。正如您所看到的,除了快速版本中多了一条
movaps xmmword ptr [rsp+50h],xmm0
指令之外,程序集完全相同。
实际上这是一个空操作,因为 rsp+50h 将在下一次迭代中被覆盖:
lea rcx,[rsp+50h]
call qword ptr [Random_get_float3]
此代码中有趣的是,慢速版本比快速版本慢两倍,同时缺少一条额外的无用指令。
有人能解释一下为什么吗?
C++ 代码(使用 MSVC v140 和 VS 2022 编译):
#include <immintrin.h>
#include <cstdlib>
__declspec(noinline) void random_get_float3(float* vec3) {
int v = rand();
vec3[0] = *(float*)&v;
v = rand();
vec3[1] = *(float*)&v;
v = rand();
vec3[2] = *(float*)&v;
vec3[0] = powf(vec3[0], 1.0f / 3.0f);
vec3[1] = powf(vec3[1], 1.0f / 3.0f);
vec3[2] = powf(vec3[2], 1.0f / 3.0f);
}
void* randomGetFuncPtr = &random_get_float3;
// Not aligned by 16.
struct Vector3 {
float x, y, z;
};
struct Vector3Array {
size_t length;
Vector3* m_Items;
};
static bool inited = false;
Vector3 scaledRandomPosExtern = Vector3{ 0.5f, 0.5f, 0.5f };
Vector3Array randomPositions;
#define __SLOW // comment to enable fast version.
int numObjectsExtern = 10000;
void TestFunc()
{
int numObjects = numObjectsExtern;
if (!inited) {
randomPositions = {
10000,
new Vector3[10000]
};
inited = true;
}
typedef void (*Random_get_float3_fptr) (__m128* __restrict);
Random_get_float3_fptr _il2cpp_icall_func = (Random_get_float3_fptr)randomGetFuncPtr;
Vector3 scaledRandomPos = scaledRandomPosExtern;
__m128 scaledRandomPosVec = _mm_setr_ps(scaledRandomPos.x, scaledRandomPos.y, scaledRandomPos.z, 0.0f);
Vector3Array* outputArray = &randomPositions;
int* items = (int*)&outputArray->m_Items[0];
for (int i = 0; i < numObjects; i++) {
__m128 v1;
_il2cpp_icall_func(&v1);
#ifdef __SLOW
__m128 v3;
v3 = _mm_mul_ps(v1, scaledRandomPosVec);
#define RESVEC v3
#else
v1 = _mm_mul_ps(v1, scaledRandomPosVec);
#define RESVEC v1
#endif
_mm_storel_pi((__m64*)(items), RESVEC);
items[2] = _mm_extract_ps(RESVEC, 2);
items += 3;
}
}
可重现
中央处理器:
AMD 锐龙 7 3700x Windows 10 19045.3930
其他 Ryzen CPU
无法在 Intel CPU 上重现。
感谢@chtz和@fuz!
事实证明,这条额外的指令复制了乘法的结果,其中第四个分量是普通浮点数。如果没有这条额外的指令,向量的第四个分量不会被初始化,并且是一个分母浮点数,这会导致计算速度变慢。
如果您手动将第四个分量设置为浮点分值,则每个
mulps
操作都会慢 20% 左右,而用零初始化第四个分量将消除该开销。
在 Intel CPU 上,数字是范数还是分母并不重要,它不会影响计算速度。
这个额外的指令很可能是 MSVC 优化器的错误,因为它不应该存在,但意外地加速了代码。