当源 = 目标、就地时,AVX512 自动向量化 C++ 矩阵向量函数要慢得多

Lor*_*ran 5 c++ assembly x86-64 auto-vectorization avx512

我尝试编写一些函数来使用单个矩阵和源向量数组来执行矩阵向量乘法。我曾经用 C++ 编写过这些函数,并在 x86 AVX512 汇编中编写过一次,以将性能与英特尔 VTune Profiler 进行比较。当使用源向量数组作为目标数组时,汇编变体的执行速度比 C++ 对应版本快 3.5 倍到 10x\xc2\xa0,但是当使用不同的源和目标数组时,汇编变体的性能几乎不比 C++ 对应版本更好,实现几乎相同的性能...有时甚至更糟。

\n

我无法理解的另一件事是,为什么在使用不同的源和目标数组时,C++ 对应项甚至可以达到与汇编变体接近相同或更好的性能水平,即使汇编代码要短得多并且也根据静态分析工具 uica 和 llvm-mca 速度提高数倍。uica.uops.info

\n

我不想让这篇文章变得太长,所以我只发布执行 mat4-vec4 乘法的函数的代码。

\n

这是汇编变体的代码,它假设矩阵要转置:

\n
alignas(64) uint32_t mat4_mul_vec4_avx512_vpermps_index[64]{    0, 0, 0, 0, 4, 4, 4, 4, 8, 8, 8, 8, 12, 12, 12, 12,\n                                                            1, 1, 1, 1, 5, 5, 5, 5, 9, 9, 9, 9, 13, 13, 13, 13,\n                                                            2, 2, 2, 2, 6, 6, 6, 6, 10, 10, 10, 10, 14, 14, 14, 14,\n                                                            3, 3, 3, 3, 7, 7, 7, 7, 11, 11, 11, 11, 15, 15, 15, 15 };\n\nvoid __declspec(naked, align(64)) mat4_mul_vec4_avx512(vec4f_t* destination, const mat4f_t& src1, const vec4f_t* src2, uint32_t vector_count) {\n__asm {\n    vbroadcastf32x4 zmm16, xmmword ptr[rdx]\n    vbroadcastf32x4 zmm17, xmmword ptr[rdx + 16]\n\n    vbroadcastf32x4 zmm18, xmmword ptr[rdx + 32]\n    vbroadcastf32x4 zmm19, xmmword ptr[rdx + 48]\n\n    vmovups zmm20, zmmword ptr[mat4_mul_vec4_avx512_vpermps_index]\n    vmovups zmm21, zmmword ptr[mat4_mul_vec4_avx512_vpermps_index + 64]\n\n    vmovups zmm22, zmmword ptr[mat4_mul_vec4_avx512_vpermps_index + 128]\n    vmovups zmm23, zmmword ptr[mat4_mul_vec4_avx512_vpermps_index + 192]\n\n    vmovups zmm24, zmmword ptr[r8]\n\n    vpermps zmm25, zmm20, zmm24\n    vpermps zmm26, zmm21, zmm24\n    vpermps zmm27, zmm22, zmm24\n    vpermps zmm28, zmm23, zmm24\n\n    xor eax, eax\n\n    align 32\n    mat4_mul_vec4_avx512_loop:\n\n        vmovups zmm24, zmmword ptr[r8+rax+64]\n\n        vmulps zmm29, zmm16, zmm25\n        vpermps zmm25, zmm20, zmm24\n\n        vfmadd231ps zmm29, zmm17, zmm26\n        vpermps zmm26, zmm21, zmm24\n\n        vfmadd231ps zmm29, zmm18, zmm27\n        vpermps zmm27, zmm22, zmm24\n\n        vfmadd231ps zmm29, zmm19, zmm28\n        vpermps zmm28, zmm23, zmm24\n\n        vmovups zmmword ptr[rcx+rax], zmm29\n\n    add rax, 64\n\n    sub r9, 4\n    jnz mat4_mul_vec4_avx512_loop\n\n    ret\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

这是 C++ 变体,它假设矩阵不被转置:

\n
void mat4_mul_vec4_cpp(vec4f_t* destination, const mat4f_t& src1, const vec4f_t* src2, uint32_t vector_count) {\nfor (uint32_t i0{}; i0 < vector_count; ++i0) {\n    destination[i0].element_[0] = src1.element_[0][0] * src2[i0].element_[0] + src1.element_[0][1] * src2[i0].element_[1] + src1.element_[0][2] * src2[i0].element_[2] + src1.element_[0][3] * src2[i0].element_[3];\n    destination[i0].element_[1] = src1.element_[1][0] * src2[i0].element_[0] + src1.element_[1][1] * src2[i0].element_[1] + src1.element_[1][2] * src2[i0].element_[2] + src1.element_[1][3] * src2[i0].element_[3];\n    destination[i0].element_[2] = src1.element_[2][0] * src2[i0].element_[0] + src1.element_[2][1] * src2[i0].element_[1] + src1.element_[2][2] * src2[i0].element_[2] + src1.element_[2][3] * src2[i0].element_[3];\n    destination[i0].element_[3] = src1.element_[3][0] * src2[i0].element_[0] + src1.element_[3][1] * src2[i0].element_[1] + src1.element_[3][2] * src2[i0].element_[2] + src1.element_[3][3] * src2[i0].element_[3];\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

英特尔 C++ 编译器生成以下汇编代码:

\n
00007FF69F123D50  sub         rsp,38h  \n00007FF69F123D54  vmovaps     xmmword ptr [rsp+20h],xmm8  \n00007FF69F123D5A  vmovaps     xmmword ptr [rsp+10h],xmm7  \n00007FF69F123D60  vmovaps     xmmword ptr [rsp],xmm6  \n        destination[i0].element_[0] = src1.element_[0][0] * src2[i0].element_[0] + src1.element_[0][1] * src2[i0].element_[1] + src1.element_[0][2] * src2[i0].element_[2] + src1.element_[0][3] * src2[i0].element_[3];\n00007FF69F123D65  vmovss      xmm0,dword ptr [rdx]  \n00007FF69F123D69  vmovss      xmm1,dword ptr [rdx+4]  \n00007FF69F123D6E  vmovss      xmm2,dword ptr [rdx+8]  \n00007FF69F123D73  vmovss      xmm3,dword ptr [rdx+0Ch]  \n        destination[i0].element_[1] = src1.element_[1][0] * src2[i0].element_[0] + src1.element_[1][1] * src2[i0].element_[1] + src1.element_[1][2] * src2[i0].element_[2] + src1.element_[1][3] * src2[i0].element_[3];\n00007FF69F123D78  vmovss      xmm4,dword ptr [rdx+10h]  \n00007FF69F123D7D  vmovss      xmm5,dword ptr [rdx+14h]  \n00007FF69F123D82  vmovss      xmm16,dword ptr [rdx+18h]  \n00007FF69F123D89  vmovss      xmm17,dword ptr [rdx+1Ch]  \n        destination[i0].element_[2] = src1.element_[2][0] * src2[i0].element_[0] + src1.element_[2][1] * src2[i0].element_[1] + src1.element_[2][2] * src2[i0].element_[2] + src1.element_[2][3] * src2[i0].element_[3];\n00007FF69F123D90  vmovss      xmm18,dword ptr [rdx+20h]  \n00007FF69F123D97  vmovss      xmm19,dword ptr [rdx+24h]  \n00007FF69F123D9E  vmovss      xmm20,dword ptr [rdx+28h]  \n00007FF69F123DA5  vmovss      xmm21,dword ptr [rdx+2Ch]  \n        destination[i0].element_[3] = src1.element_[3][0] * src2[i0].element_[0] + src1.element_[3][1] * src2[i0].element_[1] + +src1.element_[3][2] * src2[i0].element_[2] + src1.element_[3][3] * src2[i0].element_[3];\n00007FF69F123DAC  vmovss      xmm22,dword ptr [rdx+30h]  \n00007FF69F123DB3  vmovss      xmm23,dword ptr [rdx+34h]  \n00007FF69F123DBA  vmovss      xmm24,dword ptr [rdx+38h]  \n00007FF69F123DC1  vmovss      xmm25,dword ptr [rdx+3Ch]  \n    for (uint32_t i0{}; i0 < vector_count; ++i0) {\n00007FF69F123DC8  lea         rax,[r8+3A9800h]  \n00007FF69F123DCF  cmp         rax,rcx  \n00007FF69F123DD2  jbe         mat4_mul_vec4_cpp+150h (07FF69F123EA0h)  \n00007FF69F123DD8  lea         rax,[rcx+3A9800h]  \n00007FF69F123DDF  cmp         rax,r8  \n00007FF69F123DE2  jbe         mat4_mul_vec4_cpp+150h (07FF69F123EA0h)  \n00007FF69F123DE8  mov         eax,0Ch  \n00007FF69F123DED  nop         dword ptr [rax]  \n        destination[i0].element_[0] = src1.element_[0][0] * src2[i0].element_[0] + src1.element_[0][1] * src2[i0].element_[1] + src1.element_[0][2] * src2[i0].element_[2] + src1.element_[0][3] * src2[i0].element_[3];\n00007FF69F123DF0  vmulss      xmm26,xmm0,dword ptr [r8+rax-0Ch]  \n00007FF69F123DF8  vfmadd231ss xmm26,xmm1,dword ptr [r8+rax-8]  \n00007FF69F123E00  vfmadd231ss xmm26,xmm2,dword ptr [r8+rax-4]  \n00007FF69F123E08  vfmadd231ss xmm26,xmm3,dword ptr [r8+rax]  \n00007FF69F123E0F  vmovss      dword ptr [rcx+rax-0Ch],xmm26  \n        destination[i0].element_[1] = src1.element_[1][0] * src2[i0].element_[0] + src1.element_[1][1] * src2[i0].element_[1] + src1.element_[1][2] * src2[i0].element_[2] + src1.element_[1][3] * src2[i0].element_[3];\n00007FF69F123E17  vmulss      xmm26,xmm4,dword ptr [r8+rax-0Ch]  \n00007FF69F123E1F  vfmadd231ss xmm26,xmm5,dword ptr [r8+rax-8]  \n00007FF69F123E27  vfmadd231ss xmm26,xmm16,dword ptr [r8+rax-4]  \n00007FF69F123E2F  vfmadd231ss xmm26,xmm17,dword ptr [r8+rax]  \n00007FF69F123E36  vmovss      dword ptr [rcx+rax-8],xmm26  \n        destination[i0].element_[2] = src1.element_[2][0] * src2[i0].element_[0] + src1.element_[2][1] * src2[i0].element_[1] + src1.element_[2][2] * src2[i0].element_[2] + src1.element_[2][3] * src2[i0].element_[3];\n00007FF69F123E3E  vmulss      xmm26,xmm18,dword ptr [r8+rax-0Ch]  \n00007FF69F123E46  vfmadd231ss xmm26,xmm19,dword ptr [r8+rax-8]  \n00007FF69F123E4E  vfmadd231ss xmm26,xmm20,dword ptr [r8+rax-4]  \n00007FF69F123E56  vfmadd231ss xmm26,xmm21,dword ptr [r8+rax]  \n00007FF69F123E5D  vmovss      dword ptr [rcx+rax-4],xmm26  \n        destination[i0].element_[3] = src1.element_[3][0] * src2[i0].element_[0] + src1.element_[3][1] * src2[i0].element_[1] + +src1.element_[3][2] * src2[i0].element_[2] + src1.element_[3][3] * src2[i0].element_[3];\n00007FF69F123E65  vmulss      xmm26,xmm22,dword ptr [r8+rax-0Ch]  \n00007FF69F123E6D  vfmadd231ss xmm26,xmm23,dword ptr [r8+rax-8]  \n00007FF69F123E75  vfmadd231ss xmm26,xmm24,dword ptr [r8+rax-4]  \n00007FF69F123E7D  vfmadd231ss xmm26,xmm25,dword ptr [r8+rax]  \n00007FF69F123E84  vmovss      dword ptr [rcx+rax],xmm26  \n    for (uint32_t i0{}; i0 < vector_count; ++i0) {\n00007FF69F123E8B  add         rax,10h  \n00007FF69F123E8F  cmp         rax,3A980Ch  \n00007FF69F123E95  jne         mat4_mul_vec4_cpp+0A0h (07FF69F123DF0h)  \n00007FF69F123E9B  jmp         mat4_mul_vec4_cpp+2FEh (07FF69F12404Eh)  \n00007FF69F123EA0  vbroadcastss ymm0,xmm0  \n00007FF69F123EA5  vbroadcastss ymm1,xmm1  \n00007FF69F123EAA  vbroadcastss ymm2,xmm2  \n00007FF69F123EAF  vbroadcastss ymm3,xmm3  \n00007FF69F123EB4  vbroadcastss ymm4,xmm4  \n00007FF69F123EB9  vbroadcastss ymm5,xmm5  \n00007FF69F123EBE  vbroadcastss ymm16,xmm16  \n00007FF69F123EC4  vbroadcastss ymm17,xmm17  \n00007FF69F123ECA  vbroadcastss ymm18,xmm18  \n00007FF69F123ED0  vbroadcastss ymm19,xmm19  \n00007FF69F123ED6  vbroadcastss ymm20,xmm20  \n00007FF69F123EDC  vbroadcastss ymm21,xmm21  \n00007FF69F123EE2  vbroadcastss ymm22,xmm22  \n00007FF69F123EE8  vbroadcastss ymm23,xmm23  \n00007FF69F123EEE  vbroadcastss ymm24,xmm24  \n00007FF69F123EF4  vbroadcastss ymm25,xmm25  \n00007FF69F123EFA  xor         eax,eax  \n        destination[i0].element_[0] = src1.element_[0][0] * src2[i0].element_[0] + src1.element_[0][1] * src2[i0].element_[1] + src1.element_[0][2] * src2[i0].element_[2] + src1.element_[0][3] * src2[i0].element_[3];\n00007FF69F123EFC  vmovups     xmm26,xmmword ptr [r8+rax]  \n00007FF69F123F03  vmovups     xmm27,xmmword ptr [r8+rax+10h]  \n00007FF69F123F0B  vmovups     xmm28,xmmword ptr [r8+rax+20h]  \n00007FF69F123F13  vmovups     xmm29,xmmword ptr [r8+rax+30h]  \n00007FF69F123F1B  vinsertf32x4 ymm26,ymm26,xmmword ptr [r8+rax+40h],1  \n00007FF69F123F24  vinsertf32x4 ymm27,ymm27,xmmword ptr [r8+rax+50h],1  \n00007FF69F123F2D  vinsertf32x4 ymm28,ymm28,xmmword ptr [r8+rax+60h],1  \n00007FF69F123F36  vinsertf32x4 ymm29,ymm29,xmmword ptr [r8+rax+70h],1  \n00007FF69F123F3F  vshufps     ymm30,ymm26,ymm27,14h  \n00007FF69F123F46  vshufps     ymm31,ymm29,ymm28,41h  \n00007FF69F123F4D  vshufps     ymm6,ymm30,ymm31,6Ch  \n00007FF69F123F54  vmulps      ymm7,ymm6,ymm0  \n        destination[i0].element_[1] = src1.element_[1][0] * src2[i0].element_[0] + src1.element_[1][1] * src2[i0].element_[1] + src1.element_[1][2] * src2[i0].element_[2] + src1.element_[1][3] * src2[i0].element_[3];\n00007FF69F123F58  vmulps      ymm8,ymm6,ymm4  \n        destination[i0].element_[0] = src1.element_[0][0] * src2[i0].element_[0] + src1.element_[0][1] * src2[i0].element_[1] + src1.element_[0][2] * src2[i0].element_[2] + src1.element_[0][3] * src2[i0].element_[3];\n00007FF69F123F5C  vshufps     ymm30,ymm30,ymm31,39h  \n        destination[i0].element_[2] = src1.element_[2][0] * src2[i0].element_[0] + src1.element_[2][1] * src2[i0].element_[1] + src1.element_[2][2] * src2[i0].element_[2] + src1.element_[2][3] * src2[i0].element_[3];\n00007FF69F123F63  vmulps      ymm31,ymm6,ymm18  \n        destination[i0].element_[3] = src1.element_[3][0] * src2[i0].element_[0] + src1.element_[3][1] * src2[i0].element_[1] + +src1.element_[3][2] * src2[i0].element_[2] + src1.element_[3][3] * src2[i0].element_[3];\n00007FF69F123F69  vmulps      ymm6,ymm6,ymm22  \n        destination[i0].element_[0] = src1.element_[0][0] * src2[i0].element_[0] + src1.element_[0][1] * src2[i0].element_[1] + src1.element_[0][2] * src2[i0].element_[2] + src1.element_[0][3] * src2[i0].element_[3];\n00007FF69F123F6F  vfmadd231ps ymm7,ymm30,ymm1  \n        destination[i0].element_[1] = src1.element_[1][0] * src2[i0].element_[0] + src1.element_[1][1] * src2[i0].element_[1] + src1.element_[1][2] * src2[i0].element_[2] + src1.element_[1][3] * src2[i0].element_[3];\n00007FF69F123F75  vfmadd231ps ymm8,ymm30,ymm5  \n        destination[i0].element_[2] = src1.element_[2][0] * src2[i0].element_[0] + src1.element_[2][1] * src2[i0].element_[1] + src1.element_[2][2] * src2[i0].element_[2] + src1.element_[2][3] * src2[i0].element_[3];\n00007FF69F123F7B  vfmadd231ps ymm31,ymm30,ymm19  \n        destination[i0].element_[3] = src1.element_[3][0] * src2[i0].element_[0] + src1.element_[3][1] * src2[i0].element_[1] + +src1.element_[3][2] * src2[i0].element_[2] + src1.element_[3][3] * src2[i0].element_[3];\n00007FF69F123F81  vfmadd231ps ymm6,ymm23,ymm30  \n        destination[i0].element_[0] = src1.element_[0][0] * src2[i0].element_[0] + src1.element_[0][1] * src2[i0].element_[1] + src1.element_[0][2] * src2[i0].element_[2] + src1.element_[0][3] * src2[i0].element_[3];\n00007FF69F123F87  vshufps     ymm26,ymm26,ymm27,0BEh  \n00007FF69F123F8E  vshufps     ymm27,ymm29,ymm28,0EBh  \n00007FF69F123F95  vshufps     ymm28,ymm26,ymm27,6Ch  \n00007FF69F123F9C  vfmadd231ps ymm7,ymm28,ymm2  \n        destination[i0].element_[1] = src1.element_[1][0] * src2[i0].element_[0] + src1.element_[1][1] * src2[i0].element_[1] + src1.element_[1][2] * src2[i0].element_[2] + src1.element_[1][3] * src2[i0].element_[3];\n00007FF69F123FA2  vfmadd231ps ymm8,ymm28,ymm16  \n        destination[i0].element_[2] = src1.element_[2][0] * src2[i0].element_[0] + src1.element_[2][1] * src2[i0].element_[1] + src1.element_[2][2] * src2[i0].element_[2] + src1.element_[2][3] * src2[i0].element_[3];\n00007FF69F123FA8  vfmadd231ps ymm31,ymm28,ymm20  \n        destination[i0].element_[3] = src1.element_[3][0] * src2[i0].element_[0] + src1.element_[3][1] * src2[i0].element_[1] + +src1.element_[3][2] * src2[i0].element_[2] + src1.element_[3][3] * src2[i0].element_[3];\n00007FF69F123FAE  vfmadd231ps ymm6,ymm24,ymm28  \n        destination[i0].element_[0] = src1.element_[0][0] * src2[i0].element_[0] + src1.element_[0][1] * src2[i0].element_[1] + src1.element_[0][2] * src2[i0].element_[2] + src1.element_[0][3] * src2[i0].element_[3];\n00007FF69F123FB4  vshufps     ymm26,ymm26,ymm27,39h  \n00007FF69F123FBB  vfmadd231ps ymm7,ymm26,ymm3  \n        destination[i0].element_[1] = src1.element_[1][0] * src2[i0].element_[0] + src1.element_[1][1] * src2[i0].element_[1] + src1.element_[1][2] * src2[i0].element_[2] + src1.element_[1][3] * src2[i0].element_[3];\n00007FF69F123FC1  vfmadd231ps ymm8,ymm26,ymm17  \n        destination[i0].element_[2] = src1.element_[2][0] * src2[i0].element_[0] + src1.element_[2][1] * src2[i0].element_[1] + src1.element_[2][2] * src2[i0].element_[2] + src1.element_[2][3] * src2[i0].element_[3];\n00007FF69F123FC7  vfmadd231ps ymm31,ymm26,ymm21  \n        destination[i0].element_[3] = src1.element_[3][0] * src2[i0].element_[0] + src1.element_[3][1] * src2[i0].element_[1] + +src1.element_[3][2] * src2[i0].element_[2] + src1.element_[3][3] * src2[i0].element_[3];\n00007FF69F123FCD  vfmadd231ps ymm6,ymm25,ymm26  \n00007FF69F123FD3  vpunpckldq  ymm26,ymm7,ymm8  \n00007FF69F123FD9  vpunpckldq  ymm27,ymm31,ymm6  \n00007FF69F123FDF  vpunpckhdq  ymm28,ymm7,ymm8  \n00007FF69F123FE5  vpunpckhdq  ymm29,ymm31,ymm6  \n00007FF69F123FEB  vpunpcklqdq ymm30,ymm26,ymm27  \n00007FF69F123FF1  vpunpckhqdq ymm26,ymm26,ymm27  \n00007FF69F123FF7  vpunpcklqdq ymm27,ymm28,ymm29  \n00007FF69F123FFD  vpunpckhqdq ymm28,ymm28,ymm29  \n00007FF69F124003  vinsertf32x4 ymm29,ymm30,xmm26,1  \n00007FF69F12400A  vmovups     ymmword ptr [rcx+rax],ymm29  \n00007FF69F124011  vinsertf32x4 ymm29,ymm27,xmm28,1  \n00007FF69F124018  vmovups     ymmword ptr [rcx+rax+20h],ymm29  \n00007FF69F124020  vshuff64x2  ymm26,ymm30,ymm26,3  \n00007FF69F124027  vmovupd     ymmword ptr [rcx+rax+40h],ymm26  \n00007FF69F12402F  vshuff64x2  ymm26,ymm27,ymm28,3  \n00007FF69F124036  vmovupd     ymmword ptr [rcx+rax+60h],ymm26  \n    for (uint32_t i0{}; i0 < vector_count; ++i0) {\n00007FF69F12403E  sub         rax,0FFFFFFFFFFFFFF80h  \n00007FF69F124042  cmp         rax,3A9800h  \n00007FF69F124048  jne         mat4_mul_vec4_cpp+1ACh (07FF69F123EFCh)  \n    }\n}\n00007FF69F12404E  vmovaps     xmm6,xmmword ptr [rsp]  \n00007FF69F124053  vmovaps     xmm7,xmmword ptr [rsp+10h]  \n00007FF69F124059  vmovaps     xmm8,xmmword ptr [rsp+20h]  \n00007FF69F12405F  add         rsp,38h  \n00007FF69F124063  vzeroupper  \n00007FF69F124066  ret  \n
Run Code Online (Sandbox Code Playgroud)\n

这是使用相同的源和目标向量数组时基准测试的调用代码:

\n
int main() {\n    SetPriorityClass(GetCurrentProcess(), REALTIME_PRIORITY_CLASS);\n\n    SetThreadPriorityBoost(GetCurrentThread(), false);\n    SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL);\n\n    mat4f_t matrix{ {0, 1, 2, 3,\n                    4, 5, 6, 7,\n                    8, 9, 10, 11,\n                    12, 13, 14, 15} };\n\n    vec4f_t* dst_vector = new vec4f_t[1\'000\'000]{};\n    vec4f_t* src_vector = new vec4f_t[1\'000\'000]{};\n\n    vec3f_t* dst_vector0 = new vec3f_t[1\'000\'000]{};\n    vec3f_t* src_vector0 = new vec3f_t[1\'000\'000]{};\n\n    for (uint32_t i0{}; i0 < 1000000; ++i0) {\n        src_vector[i0] = vec4f_t{ (float)i0, (float)i0, (float)i0, (float)i0 };\n        src_vector0[i0] = vec3f_t{ (float)i0, (float)i0, (float)i0 };\n    }\n\n    set_mxcsr0(0b1111\'1111\'1100\'0000);\n\n    for (uint64_t i0{}; i0 < 30000; ++i0) {\n        for (uint32_t i1{}; i1 < 16; ++i1) {\n            reinterpret_cast<float*>(matrix.element_)[i1] = 1. / i0;\n        }\n        mat4_mul_vec4_avx512(src_vector, matrix, src_vector, 240000);\n        mat4_mul_vec4_cpp(src_vector, matrix, src_vector, 240000);\n        mat4_mul_vec3_avx512(src_vector0, matrix, src_vector0, 240000);\n        mat4_mul_vec3_cpp(src_vector0, matrix, src_vector0, 240000);\n        fps();\n    }\n\n    for (uint32_t i0{}; i0 < 1000000; ++i0) {\n        std::cout << src_vector0[i0] << std::endl;\n        std::cout << src_vector[i0] << std::endl;\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

当使用不同的源和目标数组进行测试时,src_vector/src_vector0 作为第一个参数将被 dst_vector/dst_vector0 替换。

\n

这些是使用相同源和目标数组时的基准测试结果:

\n

相同的源/目标数组,Assembly 性能更好

\n

这些是使用不同源阵列和目标阵列时的基准测试结果:

\n

不同的源/目标数组,Assembly 的性能几乎与 C++ 相同

\n

这些基准测试是在配备第 11 代 i7-11850H Tiger Lake CPU 的 Windows 11 计算机上使用 Intel C++ Compiler 2024 以及如前所述的 Intel VTune Profiler 创建的。

\n

有几件事我不明白:

\n
    \n
  1. 为什么使用不同/相同的源阵列和目标阵列时速度会有所不同?\n在这种情况下与缓存有什么关系吗?

    \n
  2. \n
  3. 为什么在使用不同的源数组和目标数组时,C++ 对应版本甚至能在汇编变体附近达到如此好的性能?

    \n
  4. \n
\n

感谢您的帮助。我真的很感激。

\n

Pet*_*des 6

您的第一个 VTune 屏幕截图(源 = 目标版本)显示了运行 100% 标量运算的 cpp 版本(最右列),以及运行 100% 打包 SIMD FP 运算的单独目标情况。

自动矢量化通常会检查重叠,如果输出与任何输入重叠,则返回到标量循环,而不是制作专门用于 dst=src 的第三个版本(例如,如果仍然与 C++ 语义匹配,则可能首先加载所有内容)那种情况)。 因此它可能使用标量回退,因为它的检查检测到重叠。 (在总是具有不相交的 dst 和 src 的代码中,您可以使用__restrict承诺并优化这些检查。它可能会编译为在完美重叠 dst=src 情况下工作的 asm,但这仍然是 UB 所以不要这样做,它可能会在将来中断或使用不同的周围代码来内联。)

如果您的源特殊情况 dst=src 情况,则可能会自动矢量化。


还根据静态分析工具 uica 和 llvm-mca 速度快了数倍。uica.uops.info

事情并没有那么极端;uiCA 预测您的 asm 与 Tiger Lake 上的 LLVM 相比,速度可提高 1.75 倍。

  • 用于 C++ 自动向量化内部循环的 uiCA - 使用 YMM 向量预测每次迭代 14.14 个周期(128 字节数据 = 8x float4)。通过完美的调度,后端端口吞吐量瓶颈为 13.33 个周期,但显然它预测太多的洗牌将被调度到端口 1 并从 MUL/FMA 窃取周期。
    54 个前端 uops / 迭代 = 6.75 个float4
  • 用于 asm 循环的 uiCA - 预测每次迭代 4.02 个周期(64 字节 = 4x float4)。正如预期的那样,如果加载/存储不是瓶颈,则无序执行可以毫无问题地跨迭代重叠工作,并且使用 FMA 使端口 0 饱和,使用洗牌使端口 5 饱和。索引寻址模式不是问题,因为您不将其用作 ALU 指令的内存操作数,仅用于纯加载/纯存储。
    每个 iter 总共 12 个(融合域)uops = 3 per float4,因此,如果另一个逻辑核心有一些可以在端口 1 和 6 上运行的标量整数工作,或者当该线程在加载/存储带宽上停止时,则对超线程更加友好。

因此,在最好的无失速情况下,14.14/2 / 4.02 = 1.756速度比以周期/ 为单位float4。也许您错过了每次迭代因素的工作(查看指针增量和寻址模式偏移),或者您复制/粘贴了整个函数而不仅仅是内部循环?uiCA 将 asm 视为循环体,无论它是否以分支结尾,假设不采用任何较早的分支。

内存/缓存带宽瓶颈可能是限制因素,导致两者运行速度大致相同? 对于等效代码(与使用 256 位向量进行大量额外洗牌的情况不同),未对齐的数据可能会使使用 512 位向量的 Skylake Xeon 上的 DRAM 带宽比使用 256 位向量时差 15%(其中只有每隔一个负载/ store 是缓存行分割。)

源 = 目标更适合缓存占用空间,因为您只接触一半的数据。对于内存带宽来说更好:如果没有 NT 存储,如果数据在缓存中还不是热的,则写入成本为读+写,包括读取所有权以获取缓存行的旧值和独占所有权。(我不确定整个 64 字节缓存行的对齐存储是否有任何优化,例如是否可以在不执行 RFO 的情况下使其他副本无效,但不会像 NT 存储那样绕过/逐出缓存,而是像如何rep movsb/rep stosb微码有效。)无论如何,如果 DRAM 带宽是一个瓶颈,您会期望就地速度比复制快 1.5 倍,并以其他因素(例如混合读+写)为 DRAM 本身带来一些开销。

因此,就地情况可能会使您的汇编摆脱带宽瓶颈的束缚,并导致 C++ 在运行标量代码时面对植物,从而放大差异。


看起来您的代码每次都使用相同的矩阵,但使用不同的向量。然后将 4 个float4向量加载为一个 ZMM 向量。

通过在 MUL+FMA 之前进行加载+洗牌的软件流水线(实际上没有必要),所有具有 AVX-512 的 CPU 都具有相当深的乱序执行缓冲区,以找到跨迭代的指令级并行性。这是一个相当短的依赖链。

另外,您加载 + 洗牌最终向量,但不进行 FMA + 存储它。您可以从最后一次迭代的末尾剥离最后一组 MUL/FMA + 存储,并让循环计数器少执行一次迭代。

对于 Zen 4,您可以在cmp/jne指针上使用 on,而不是在单独的计数器上使用sub/ jnz。Zen 可以宏融合,cmp/jcc但不能sub/jcc


看起来 ICX 展开后每次迭代处理 128 字节的数据,但它对每个输入向量执行大量shuffle uops。每个乘法一个vpermps(或者vpermt2ps如果它需要从 2x YMM 而不是默认的 1x ZMM 中提取数据-mprefer-vector-width=256)应该更好,但它并没有发明除立即数之外的洗牌常量vshufps

看起来它使用 16xvbroadcastss指令将矩阵的每个标量元素广播到单独的 YMM 寄存器。所以这需要洗牌来排列结果。


即使“客户端”芯片具有 1/时钟 ZMM FMA/mul/add 但 2/时钟 YMM,由于前端和后端执行端口吞吐量瓶颈,我预计您的 asm 会更快洗牌。即使您的数据未按 64 对齐,因此每次加载和存储都是缓存行拆分。(512 位向量使得对齐数据比 256 位向量更重要。)即使使用 512 位向量可能会降低时钟速度。

您可能不需要asm 中编写此内容。_mm512_loadu_ps您可能会得到与和 等内在函数相同的汇编_mm512_fmadd_psasm{}确实可以让您align 32针对该特定循环来帮助 uop 缓存,但 Tiger Lake 有一个有效的 LSD(循环缓冲区)。(Skylake-X 只有 uop 缓存,没有 LSD,而且它的 JCC 勘误表为循环底部创建了更多代码对齐坑洞。)