有和没有SSE的不同结果(浮点数乘法)

时间:2016-04-09 12:35:59

标签: c++ arrays floating-point sse

我有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;
}

2 个答案:

答案 0 :(得分:2)

FP添加不是完全关联的,因此不同的操作顺序会产生略微不同的舍入误差。

您的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单元中执行。 (请参阅代码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类。