SIMD 性能减慢两倍,无需额外复制

问题描述 投票:0回答:1

我一直在优化一些代码,并偶然发现了一些特殊的情况。 这是两个汇编代码:

; 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 上重现。

assembly x86-64 simd amd
1个回答
0
投票

感谢@chtz和@fuz!

事实证明,这条额外的指令复制了乘法的结果,其中第四个分量是普通浮点数。如果没有这条额外的指令,向量的第四个分量不会被初始化,并且是一个分母浮点数,这会导致计算速度变慢。

如果您手动将第四个分量设置为浮点分值,则每个

mulps
操作都会慢 20% 左右,而用零初始化第四个分量将消除该开销。 在 Intel CPU 上,数字是范数还是分母并不重要,它不会影响计算速度。

这个额外的指令很可能是 MSVC 优化器的错误,因为它不应该存在,但意外地加速了代码。

© www.soinside.com 2019 - 2024. All rights reserved.