我有2d数组乘法的两个函数。其中一个有SSE。另一个功能没有任何优化。这两个功能都很好。但结果略有不同。例如20.333334和20.333332。
你能解释为什么结果不同吗?我可以用函数来做同样的结果吗?
使用SSE功能
float** sse_multiplication(float** array1, float** array2, float** arraycheck)
{
int i, j, k;
float *ms1, *ms2, result;
float *end_loop;
for( i = 0; i < rows1; i++)
{
for( j = 0; j < columns2; j++)
{
result = 0;
ms1 = array1[i];
ms2 = array2[j];
end_loop = &array1[i][columns1];
__asm{
mov rax, ms1
mov rbx, ms2
mov rdx, end_loop
xorps xmm2, xmm2
loop:
movups xmm0, [rax]
movups xmm1, [rbx]
movups xmm3, [rax+16]
movups xmm4, [rbx+16]
mulps xmm0, xmm1
mulps xmm3, xmm4
addps xmm2, xmm0
add rax, 32
add rbx, 32
cmp rdx, rax
jne loop
haddps xmm2, xmm2
haddps xmm2, xmm2
movups result, xmm2
}
arraycheck[i][j] = result;
}
}
return arraycheck;
}
没有任何优化的功能
float** multiplication(float** array1, float** array2, float** arraycheck)
{
for (int i = 0; i < rows1; i++)
for (int j = 0; j < columns2; j++)
for (int k = 0; k < rows1; k++)
arraycheck[i][j] += array1[i][k] * array2[k][j];
return arraycheck;
}
答案 0 :(得分:2)
您的C按顺序汇总元素。 (除非您使用-ffast-math
允许编译器做出相同的假设,因此FP操作足够接近关联)。
你的asm总结了4个不同偏移处的每4个元素,然后水平地对它们求和。每个向量元素的总和在每个点都有不同的舍入。
您的矢量化版本似乎与C版本不匹配。索引看起来不同。 AFAICT,向量化arraycheck[i][j] += array1[i][k] * array2[k][j];
的唯一理智方式是j
。循环k
将需要来自array2
的跨步加载,并且i
上的循环将需要来自array1
的跨步加载。
我错过了关于你的asm的一些事情吗?它从两个数组加载连续值。 它还会在mulps
的每次迭代中将xmm3
结果丢弃loop
,所以我认为它只是错误。
由于在内部向量循环中循环j
并不会更改array1[i][k]
,因此只需在循环外(_mm256_set1_ps
)广播加载一次。
但是,这意味着对每个不同的arraycheck[i][j]
值执行j
的读取 - 修改 - 写入。即ac[i][j + 0..3] = fma(a1[i][k], a2[k][j + 0..3], ac[i][j + 0..3])
。为避免这种情况,您必须首先转置其中一个阵列。 (但对于NxN矩阵来说,这是O(N ^ 2),它仍然比乘法更便宜。)
这种方式不使用horizontal sums,但如果您想要更好的代码,请查看该链接。
它以与标量C相同的顺序执行操作,因此结果应完全匹配。
另请注意,您需要使用多个累加器来使CPU的执行单元饱和。我建议8,使每个0.5c吞吐量addps
饱和Skylake的4c潜伏期。 Haswell有3c延迟,每1c {1}}一个,但Skylake放弃了单独的FP添加单元并在FMA单元中执行。 (请参阅x86代码wiki,尤其是Agner Fog's guides)
实际上,由于我建议的更改根本不使用单个累加器,因此每次循环迭代都会访问独立的内存。您需要一些循环展开来使FP执行单元用两个加载饱和并存储在循环中(即使您只需要两个指针,因为存储返回到与其中一个加载相同的位置)。但无论如何,如果你的数据适合L1缓存,乱序执行应该保持执行单元很好地提供单独迭代的工作。
如果您真的关心性能,那么您将制作FMA版本,也许是Sandybridge的AVX-without-FMA版本。您可以在每个时钟执行两个256b FMA,而不是每个时钟一个128b add和mul。 (当然,你甚至没有得到它,因为你的延迟瓶颈,除非循环足够短,无序窗口可以看到下一次迭代的独立指令。)
您将需要&#34;循环平铺&#34;,又名&#34;缓存阻止&#34;使这个问题无法解决大问题。这是一个矩阵乘法,对吗?有很好的库,可以根据缓存大小进行调整,并且可以通过这样的简单尝试来打败裤子。例如我上次检查时ATLAS很好,但那是几年前的事了。
使用内在函数,除非您在asm中编写整个函数。编译器&#34;了解&#34;他们做了什么,所以可以做出很好的优化,比如在适当时循环展开。
答案 1 :(得分:1)
根据IEEE standard Formats,32位浮点数只能保证6-7位精度。你的错误是如此微不足道,以至于无法对编译器的机制做出合理的声明。如果你想获得更好的精度,最好选择64位双精度(guarentees 15位精度)或者像java那样实现你自己的BigDecimal类。