ICC 中的 -O2 搞砸了汇编程序,ICC 中的 -O1 和 GCC / Clang 中的所有优化都很好



我最近开始使用ICC(18.0.1.126)来编译一个代码,该代码在任意优化设置下与GCC和Clang配合良好。该代码包含一个汇编程序例程,该例程使用 AVX2 和 FMA 指令将 4x4 双精度矩阵相乘。经过多次摆弄,事实证明汇编程序例程在使用 -O1 - xcore-avx2 编译时工作正常,但在使用 -O2 - xcore-avx2 编译时给出了错误的数值结果。但是,代码编译时所有优化设置都没有任何错误消息。它运行在2015年初的MacBook Air上,带有Broadwell核心i5。

我还有几个版本的 4x4 矩阵乘法例程,最初是为速度测试而编写的,有/没有 FMA,并使用汇编器/内部函数。这对他们来说都是同样的问题。

我向例程传递一个指向 4x4 双精度数组的第一个元素的指针,该元素创建为 双矩阵假人[4][4]; 并传递给例程作为 (和矩阵假人)[0][0]

汇编程序例程在这里:

//Routine multiplies the 4x4 matrices A * B and store the result in C
inline void RunAssembler_FMA_UnalignedCopy_MultiplyMatrixByMatrix(double *A, double *B, double *C)
{
__asm__ __volatile__  ("vmovupd %0, %%ymm0 nt"
"vmovupd %1, %%ymm1 nt"
"vmovupd %2, %%ymm2 nt"
"vmovupd %3, %%ymm3"
:
:
"m" (B[0]),
"m" (B[4]),
"m" (B[8]),
"m" (B[12])
:
"ymm0", "ymm1", "ymm2", "ymm3");

__asm__ __volatile__ ("vbroadcastsd %1, %%ymm4 nt"
"vbroadcastsd %2, %%ymm5 nt"
"vbroadcastsd %3, %%ymm6 nt"
"vbroadcastsd %4, %%ymm7 nt"
"vmulpd %%ymm4, %%ymm0, %%ymm8 nt"
"vfmadd231PD %%ymm5, %%ymm1, %%ymm8 nt"
"vfmadd231PD %%ymm6, %%ymm2, %%ymm8 nt"
"vfmadd231PD %%ymm7, %%ymm3, %%ymm8 nt"
"vmovupd %%ymm8, %0"
:
"=m" (C[0])
:
"m" (A[0]),
"m" (A[1]),
"m" (A[2]),
"m" (A[3])
:
"ymm4", "ymm5", "ymm6", "ymm7", "ymm8");
__asm__ __volatile__ ("vbroadcastsd %1, %%ymm4 nt"
"vbroadcastsd %2, %%ymm5 nt"
"vbroadcastsd %3, %%ymm6 nt"
"vbroadcastsd %4, %%ymm7 nt"
"vmulpd %%ymm4, %%ymm0, %%ymm8 nt"
"vfmadd231PD %%ymm5, %%ymm1, %%ymm8 nt"
"vfmadd231PD %%ymm6, %%ymm2, %%ymm8 nt"
"vfmadd231PD %%ymm7, %%ymm3, %%ymm8 nt"
"vmovupd %%ymm8, %0"
:
"=m" (C[4])
:
"m" (A[4]),
"m" (A[5]),
"m" (A[6]),
"m" (A[7])
:
"ymm4", "ymm5", "ymm6", "ymm7", "ymm8");
__asm__ __volatile__ ("vbroadcastsd %1, %%ymm4 nt"
"vbroadcastsd %2, %%ymm5 nt"
"vbroadcastsd %3, %%ymm6 nt"
"vbroadcastsd %4, %%ymm7 nt"
"vmulpd %%ymm4, %%ymm0, %%ymm8 nt"
"vfmadd231PD %%ymm5, %%ymm1, %%ymm8 nt"
"vfmadd231PD %%ymm6, %%ymm2, %%ymm8 nt"
"vfmadd231PD %%ymm7, %%ymm3, %%ymm8 nt"
"vmovupd %%ymm8, %0"
:
"=m" (C[8])
:
"m" (A[8]),
"m" (A[9]),
"m" (A[10]),
"m" (A[11])
:
"ymm4", "ymm5", "ymm6", "ymm7", "ymm8");

__asm__ __volatile__ ("vbroadcastsd %1, %%ymm4 nt"
"vbroadcastsd %2, %%ymm5 nt"
"vbroadcastsd %3, %%ymm6 nt"
"vbroadcastsd %4, %%ymm7 nt"
"vmulpd %%ymm4, %%ymm0, %%ymm8 nt"
"vfmadd231PD %%ymm5, %%ymm1, %%ymm8 nt"
"vfmadd231PD %%ymm6, %%ymm2, %%ymm8 nt"
"vfmadd231PD %%ymm7, %%ymm3, %%ymm8 nt"
"vmovupd %%ymm8, %0"
:
"=m" (C[12])
:
"m" (A[12]),
"m" (A[13]),
"m" (A[14]),
"m" (A[15])
:
"ymm4", "ymm5", "ymm6", "ymm7", "ymm8");
}

作为比较,以下代码应该执行完全相同的操作,并且使用所有编译器/优化设置执行此操作。由于如果我使用此例程而不是汇编程序例程,则一切正常,因此我希望错误必须出在ICC如何处理具有-O2优化的汇编程序例程中。

inline void Run3ForLoops_MultiplyMatrixByMatrix_OutputTo3(double *A, double *B, double *C){
int i, j, k;
double dummy[4][4];
for(j=0; j<4; j++) {
for(k=0; k<4; k++) {
dummy[j][k] = 0.0;
for(i=0; I<4; i++) {
dummy[j][k] += *(A+j*4+i)*(*(B+i*4+k));
}
}
}
for(j=0; j<4; j++) {
for(k=0; k<4; k++) {
*(C+j*4+k) = dummy[j][k];
}
}

}

有什么想法吗?我真的很困惑。

代码的核心问题是假设如果将值写入寄存器,该值仍将存在于下一条语句中。 这种假设是错误的。 在asm语句之间,编译器可以根据需要使用任何寄存器。 例如,它可能决定使用ymm0将变量从语句之间的一个位置复制到另一个位置,从而破坏其之前的内容。

进行内联组装的正确方法是,如果没有充分的理由,永远不要直接引用寄存器。 要在程序集语句之间保留的每个值都需要使用适当的操作数放置在变量中。 手册对此非常清楚。

例如,让我重写您的代码以使用正确的内联程序集:

#include <immintrin.h>
inline void RunAssembler_FMA_UnalignedCopy_MultiplyMatrixByMatrix(double *A, double *B, double *C)
{
size_t i;
/* the registers you use */
__m256 a0, a1, a2, a3, b0, b1, b2, b3, sum;
__m256 *B256 = (__m256 *)B, *C256 = (__m256 *)C;
/* load values from B */
asm ("vmovupd %1, %0" : "=x"(b0) : "m"(B256[0]));
asm ("vmovupd %1, %0" : "=x"(b1) : "m"(B256[1]));
asm ("vmovupd %1, %0" : "=x"(b2) : "m"(B256[2]));
asm ("vmovupd %1, %0" : "=x"(b3) : "m"(B256[3]));
for (i = 0; i < 4; i++) {
/* load values from A */
asm ("vbroadcastsd %1, %0" : "=x"(a0) : "m"(A[4 * i + 0]));
asm ("vbroadcastsd %1, %0" : "=x"(a1) : "m"(A[4 * i + 1]));
asm ("vbroadcastsd %1, %0" : "=x"(a2) : "m"(A[4 * i + 2]));
asm ("vbroadcastsd %1, %0" : "=x"(a3) : "m"(A[4 * i + 3]));
asm ("vmulpd %2, %1, %0"      : "=x"(sum) : "x"(a0), "x"(b0));
asm ("vfmadd231pd %2, %1, %0" : "+x"(sum) : "x"(a1), "x"(b1));
asm ("vfmadd231pd %2, %1, %0" : "+x"(sum) : "x"(a2), "x"(b2));
asm ("vfmadd231pd %2, %1, %0" : "+x"(sum) : "x"(a3), "x"(b3));
asm ("vmovupd %1, %0" : "=m"(C256[i]) : "x"(sum));
}
}

您应该立即注意到很多事情:

  • 我们使用的每个寄存器都通过ASM操作数进行抽象描述
  • 我们保存的所有值都与局部变量相关联,因此编译器可以跟踪哪些寄存器正在使用中,哪些寄存器可以被破坏
  • 由于 ASM 语句的所有依赖关系和副作用都通过操作数显式描述,因此不需要volatile限定符,编译器可以更好地优化代码

不过,您确实应该考虑使用内部函数,因为编译器可以使用内部函数进行比使用内联程序集更多的优化。 这是因为编译器在某种程度上了解内联函数的作用,并可以使用这些知识来生成更好的代码。

最新更新