ICC 中的 -O2 搞乱了汇编程序,ICC 中的 -O1 和 GCC / Clang 中的所有优化都很好
-O2 in ICC messes up assembler, fine with -O1 in ICC and all optimizations in GCC / Clang
我最近开始使用 ICC (18.0.1.126) 来编译一段代码,该代码可以在任意优化设置上与 GCC 和 Clang 一起正常工作。该代码包含一个汇编例程,该例程使用 AVX2 和 FMA 指令将 4x4 双精度矩阵相乘。经过多次调整后发现,汇编器例程在使用 -O1 - xcore-avx2 编译时工作正常,但在使用 -O2 - xcore-avx2 编译时给出错误的数值结果。然而,代码编译时所有优化设置都没有任何错误消息。它在配备 Broadwell 核心 i5 的 2015 年初 MacBook Air 上运行。
我还有几个版本的 4x4 矩阵乘法例程,最初是为速度测试而编写的,有/没有 FMA,并使用汇编程序/内在函数。这对他们所有人来说都是同样的问题。
我将指针传递给例程,该指针指向创建为的 4x4 双精度数组的第一个元素
双 MatrixDummy[4][4];
并作为
(&MatrixDummy)[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 \n\t"
"vmovupd %1, %%ymm1 \n\t"
"vmovupd %2, %%ymm2 \n\t"
"vmovupd %3, %%ymm3"
:
:
"m" (B[0]),
"m" (B[4]),
"m" (B[8]),
"m" (B[12])
:
"ymm0", "ymm1", "ymm2", "ymm3");
__asm__ __volatile__ ("vbroadcastsd %1, %%ymm4 \n\t"
"vbroadcastsd %2, %%ymm5 \n\t"
"vbroadcastsd %3, %%ymm6 \n\t"
"vbroadcastsd %4, %%ymm7 \n\t"
"vmulpd %%ymm4, %%ymm0, %%ymm8 \n\t"
"vfmadd231PD %%ymm5, %%ymm1, %%ymm8 \n\t"
"vfmadd231PD %%ymm6, %%ymm2, %%ymm8 \n\t"
"vfmadd231PD %%ymm7, %%ymm3, %%ymm8 \n\t"
"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 \n\t"
"vbroadcastsd %2, %%ymm5 \n\t"
"vbroadcastsd %3, %%ymm6 \n\t"
"vbroadcastsd %4, %%ymm7 \n\t"
"vmulpd %%ymm4, %%ymm0, %%ymm8 \n\t"
"vfmadd231PD %%ymm5, %%ymm1, %%ymm8 \n\t"
"vfmadd231PD %%ymm6, %%ymm2, %%ymm8 \n\t"
"vfmadd231PD %%ymm7, %%ymm3, %%ymm8 \n\t"
"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 \n\t"
"vbroadcastsd %2, %%ymm5 \n\t"
"vbroadcastsd %3, %%ymm6 \n\t"
"vbroadcastsd %4, %%ymm7 \n\t"
"vmulpd %%ymm4, %%ymm0, %%ymm8 \n\t"
"vfmadd231PD %%ymm5, %%ymm1, %%ymm8 \n\t"
"vfmadd231PD %%ymm6, %%ymm2, %%ymm8 \n\t"
"vfmadd231PD %%ymm7, %%ymm3, %%ymm8 \n\t"
"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 \n\t"
"vbroadcastsd %2, %%ymm5 \n\t"
"vbroadcastsd %3, %%ymm6 \n\t"
"vbroadcastsd %4, %%ymm7 \n\t"
"vmulpd %%ymm4, %%ymm0, %%ymm8 \n\t"
"vfmadd231PD %%ymm5, %%ymm1, %%ymm8 \n\t"
"vfmadd231PD %%ymm6, %%ymm2, %%ymm8 \n\t"
"vfmadd231PD %%ymm7, %%ymm3, %%ymm8 \n\t"
"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
在您的语句之间将变量从一个位置复制到另一个位置,从而破坏其先前的内容。
进行内联汇编的正确方法是,如果没有充分的理由,永远不要直接引用寄存器。您希望在汇编语句之间保留的每个值都需要使用适当的操作数放置在变量中。 manual 对此很清楚。
例如,让我重写您的代码以使用正确的内联汇编:
#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
限定符,编译器可以更好地优化代码
不过,您确实应该考虑使用内部函数,因为编译器可以使用内部函数进行比内联汇编更多的优化。这是因为编译器在某种程度上了解内部函数的作用,并可以使用这些知识生成更好的代码。
我最近开始使用 ICC (18.0.1.126) 来编译一段代码,该代码可以在任意优化设置上与 GCC 和 Clang 一起正常工作。该代码包含一个汇编例程,该例程使用 AVX2 和 FMA 指令将 4x4 双精度矩阵相乘。经过多次调整后发现,汇编器例程在使用 -O1 - xcore-avx2 编译时工作正常,但在使用 -O2 - xcore-avx2 编译时给出错误的数值结果。然而,代码编译时所有优化设置都没有任何错误消息。它在配备 Broadwell 核心 i5 的 2015 年初 MacBook Air 上运行。
我还有几个版本的 4x4 矩阵乘法例程,最初是为速度测试而编写的,有/没有 FMA,并使用汇编程序/内在函数。这对他们所有人来说都是同样的问题。
我将指针传递给例程,该指针指向创建为的 4x4 双精度数组的第一个元素 双 MatrixDummy[4][4]; 并作为 (&MatrixDummy)[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 \n\t"
"vmovupd %1, %%ymm1 \n\t"
"vmovupd %2, %%ymm2 \n\t"
"vmovupd %3, %%ymm3"
:
:
"m" (B[0]),
"m" (B[4]),
"m" (B[8]),
"m" (B[12])
:
"ymm0", "ymm1", "ymm2", "ymm3");
__asm__ __volatile__ ("vbroadcastsd %1, %%ymm4 \n\t"
"vbroadcastsd %2, %%ymm5 \n\t"
"vbroadcastsd %3, %%ymm6 \n\t"
"vbroadcastsd %4, %%ymm7 \n\t"
"vmulpd %%ymm4, %%ymm0, %%ymm8 \n\t"
"vfmadd231PD %%ymm5, %%ymm1, %%ymm8 \n\t"
"vfmadd231PD %%ymm6, %%ymm2, %%ymm8 \n\t"
"vfmadd231PD %%ymm7, %%ymm3, %%ymm8 \n\t"
"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 \n\t"
"vbroadcastsd %2, %%ymm5 \n\t"
"vbroadcastsd %3, %%ymm6 \n\t"
"vbroadcastsd %4, %%ymm7 \n\t"
"vmulpd %%ymm4, %%ymm0, %%ymm8 \n\t"
"vfmadd231PD %%ymm5, %%ymm1, %%ymm8 \n\t"
"vfmadd231PD %%ymm6, %%ymm2, %%ymm8 \n\t"
"vfmadd231PD %%ymm7, %%ymm3, %%ymm8 \n\t"
"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 \n\t"
"vbroadcastsd %2, %%ymm5 \n\t"
"vbroadcastsd %3, %%ymm6 \n\t"
"vbroadcastsd %4, %%ymm7 \n\t"
"vmulpd %%ymm4, %%ymm0, %%ymm8 \n\t"
"vfmadd231PD %%ymm5, %%ymm1, %%ymm8 \n\t"
"vfmadd231PD %%ymm6, %%ymm2, %%ymm8 \n\t"
"vfmadd231PD %%ymm7, %%ymm3, %%ymm8 \n\t"
"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 \n\t"
"vbroadcastsd %2, %%ymm5 \n\t"
"vbroadcastsd %3, %%ymm6 \n\t"
"vbroadcastsd %4, %%ymm7 \n\t"
"vmulpd %%ymm4, %%ymm0, %%ymm8 \n\t"
"vfmadd231PD %%ymm5, %%ymm1, %%ymm8 \n\t"
"vfmadd231PD %%ymm6, %%ymm2, %%ymm8 \n\t"
"vfmadd231PD %%ymm7, %%ymm3, %%ymm8 \n\t"
"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
在您的语句之间将变量从一个位置复制到另一个位置,从而破坏其先前的内容。
进行内联汇编的正确方法是,如果没有充分的理由,永远不要直接引用寄存器。您希望在汇编语句之间保留的每个值都需要使用适当的操作数放置在变量中。 manual 对此很清楚。
例如,让我重写您的代码以使用正确的内联汇编:
#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
限定符,编译器可以更好地优化代码
不过,您确实应该考虑使用内部函数,因为编译器可以使用内部函数进行比内联汇编更多的优化。这是因为编译器在某种程度上了解内部函数的作用,并可以使用这些知识生成更好的代码。