使用SIMD指令将代码变成代码
Turn the code into a code using SIMD instructions
我正在准备考试,正在做一些不熟练的练习。所以我一直在给出这段代码,想知道我是否已经把代码变成了 SIMD 指令。
代码
int A[100000];
int B[100000];
int C=0;
for int(i=0; i < 100000; i++)
C += A[i] * B[i];
既然没有余数,就不用管了。我们还假设它是一个128位的寄存器,因此可以计算4个单精度浮点数。
我的结果 - 使用 SIMD
int A[100000];
int B[100000];
int C=0;
for int(i=0; i < 100000/4; i += 4)
C += A[i] * B[i];
C += A[i+1] * B[i+1];
C += A[i+2] * B[i+2];
C += A[i+3] * B[i+3];
您认为使用 SIMD 指令而不是编写多线程程序有什么优势?
是的,所提供的代码应该可以编译成具有能力的 CPU 和编译器的 SIMD 指令。
在支持矢量的处理器上,SIMD 公开了硬件功能,可以大大加速相同的并行计算。例如,由于流式 RAM 访问,SIMD 通常会更好地利用单个内核上的缓存,假设正在处理的数据位于内存的连续区域中。使用多处理、缓存竞争和其他同步开销实际上可能会降低性能,因为各个内核会尝试同时写入数据。这是对冯诺依曼机器的内在提升的补充,因为它只需从共享系统内存中读取一条而不是四条单独的指令。
并行执行这些算术运算的逻辑始终存在,但需要使用特定的 SIMD 指令。因此,SIMD 倾向于在热循环中使用,在这些循环中,手动调整对整体优化有意义。
假设你的第二个循环中省略的花括号只是一个拼写错误,for 循环中的拼写错误,以及你询问乘法浮点数但你的代码显示整数数组的事实,这不会得到很好的矢量化即使编译器看到它。虽然编译器可能将 A 和 B 中的 4 个值分别作为一条指令加载,并在一条指令中进行 4 次乘法运算,但您的代码会强制编译器提取 4 个产品中的每一个并按顺序对它们求和,并分别获得SIMD 寄存器中的值通常非常慢。
如果另一方面你这样做了
float A[100000];
float B[100000];
float C0=0, C1=0, C2=0, C3=0;
for (size_t i=0; i < 100000/4; i += 4)
{
C0 += A[i+0] * B[i+0];
C1 += A[i+1] * B[i+1];
C2 += A[i+2] * B[i+2];
C3 += A[i+3] * B[i+3];
}
float C = (C0 + C1) + (C2 + C3);
然后一个好的编译器可以将其矢量化,因为现在它看到在每个循环中它加载两个 SIMD 寄存器,将它们相乘,然后它可以将结果添加到总和的 SIMD 寄存器,并且只提取这 4 个总和和最后总结一下。
矢量化编译可以使用 SIMD 执行此操作,并且不会更改单个和的评估顺序(FP 数学不是关联的)。由于这个原因,编译器通常不允许更改 FP 数学的顺序(并非没有一些额外的标志允许它在技术上违反语言标准),所以上面的代码可以用 SIMD 指令精确表示,并且将 运行 快得多(事实上,我会进一步展开循环,因为乘法将成为目前的瓶颈)。
这有点像 SIMD 的技巧,你必须理解并思考如何用向量指令最好地实现操作,然后编写你的代码来执行相同的操作序列,希望编译器能发现你做了什么。
或者您可以使用内在函数自己编写向量指令,或者使用 OpenMP 或类似工具更明确地告诉编译器要做什么。
对于此类操作,SIMD 优于线程的优势之一是您可以在单个内核中使用更多的硅……因此您不会阻止另一个线程获得周期。在我们的计算网格上,我们通常在任何一台机器上 运行 许多单线程进程,以保持所有内核始终忙碌......在这种情况下,使用更多内核进行此求和是一种错误的经济,你只需正在窃取另一个线程可以有用地 运行 另一项工作的周期。
我正在准备考试,正在做一些不熟练的练习。所以我一直在给出这段代码,想知道我是否已经把代码变成了 SIMD 指令。
代码
int A[100000];
int B[100000];
int C=0;
for int(i=0; i < 100000; i++)
C += A[i] * B[i];
既然没有余数,就不用管了。我们还假设它是一个128位的寄存器,因此可以计算4个单精度浮点数。
我的结果 - 使用 SIMD
int A[100000];
int B[100000];
int C=0;
for int(i=0; i < 100000/4; i += 4)
C += A[i] * B[i];
C += A[i+1] * B[i+1];
C += A[i+2] * B[i+2];
C += A[i+3] * B[i+3];
您认为使用 SIMD 指令而不是编写多线程程序有什么优势?
是的,所提供的代码应该可以编译成具有能力的 CPU 和编译器的 SIMD 指令。
在支持矢量的处理器上,SIMD 公开了硬件功能,可以大大加速相同的并行计算。例如,由于流式 RAM 访问,SIMD 通常会更好地利用单个内核上的缓存,假设正在处理的数据位于内存的连续区域中。使用多处理、缓存竞争和其他同步开销实际上可能会降低性能,因为各个内核会尝试同时写入数据。这是对冯诺依曼机器的内在提升的补充,因为它只需从共享系统内存中读取一条而不是四条单独的指令。
并行执行这些算术运算的逻辑始终存在,但需要使用特定的 SIMD 指令。因此,SIMD 倾向于在热循环中使用,在这些循环中,手动调整对整体优化有意义。
假设你的第二个循环中省略的花括号只是一个拼写错误,for 循环中的拼写错误,以及你询问乘法浮点数但你的代码显示整数数组的事实,这不会得到很好的矢量化即使编译器看到它。虽然编译器可能将 A 和 B 中的 4 个值分别作为一条指令加载,并在一条指令中进行 4 次乘法运算,但您的代码会强制编译器提取 4 个产品中的每一个并按顺序对它们求和,并分别获得SIMD 寄存器中的值通常非常慢。
如果另一方面你这样做了
float A[100000];
float B[100000];
float C0=0, C1=0, C2=0, C3=0;
for (size_t i=0; i < 100000/4; i += 4)
{
C0 += A[i+0] * B[i+0];
C1 += A[i+1] * B[i+1];
C2 += A[i+2] * B[i+2];
C3 += A[i+3] * B[i+3];
}
float C = (C0 + C1) + (C2 + C3);
然后一个好的编译器可以将其矢量化,因为现在它看到在每个循环中它加载两个 SIMD 寄存器,将它们相乘,然后它可以将结果添加到总和的 SIMD 寄存器,并且只提取这 4 个总和和最后总结一下。
矢量化编译可以使用 SIMD 执行此操作,并且不会更改单个和的评估顺序(FP 数学不是关联的)。由于这个原因,编译器通常不允许更改 FP 数学的顺序(并非没有一些额外的标志允许它在技术上违反语言标准),所以上面的代码可以用 SIMD 指令精确表示,并且将 运行 快得多(事实上,我会进一步展开循环,因为乘法将成为目前的瓶颈)。
这有点像 SIMD 的技巧,你必须理解并思考如何用向量指令最好地实现操作,然后编写你的代码来执行相同的操作序列,希望编译器能发现你做了什么。
或者您可以使用内在函数自己编写向量指令,或者使用 OpenMP 或类似工具更明确地告诉编译器要做什么。
对于此类操作,SIMD 优于线程的优势之一是您可以在单个内核中使用更多的硅……因此您不会阻止另一个线程获得周期。在我们的计算网格上,我们通常在任何一台机器上 运行 许多单线程进程,以保持所有内核始终忙碌......在这种情况下,使用更多内核进行此求和是一种错误的经济,你只需正在窃取另一个线程可以有用地 运行 另一项工作的周期。