有效地获取C中整数向量的绝对值

时间:2016-07-15 05:49:28

标签: c optimization vectorization premature-optimization

任务是将C整数数组的每个元素设置为其绝对值。我试图尽可能高效地做到这一点。以下是我所做的一系列优化。请告诉我这些是否真的是优化,如果还有更多的话!

函数的第一个参数是整数数组,第二个参数是该数组的整数。

以下是标准实施:

void absolute (int array[], int n){
  for(int i = 0; i < n; i++)
    if(array[i] < 0)
      array[i] = - array[i];
}

很多东西可以满足任何入门编程课程教授的要求,但是我想再多玩一遍,也许还要学习一些优化方法。

基于https://stackoverflow.com/a/2074403,无分支绝对值:

void absolute (int array[], int n){
  for(int i = 0; i < n; i++){
    uint32_t temp = array[i] >> 31;     // make a mask of the sign bit
    array[i] ^= temp;                   // toggle the bits if value is negative
    array[i] += temp & 1;               // add one if value was negative
  }
}

基于对零的比较更高效,并且不需要额外的变量:

void absolute (int array[], int n){
  for(n--; n >= 0;){
    uint32_t temp = array[n] >> 31;
    array[n] ^= temp;
    array[n] += temp & 1;
  }
}

(虽然这会向量化吗?)

就我而言。可以做更多的工作来优化这个功能吗?

2 个答案:

答案 0 :(得分:4)

就个人而言,我更喜欢这个问题。这些问题让你想知道是否有办法让自己的代码变得更好。

您的最终优化不正确,因为它初始化n--,但是n永远不会再次递减。要更正此问题,您需要for(n--; n >= 0; n--)。虽然我发现减少或递增你的for循环的结果没有明显的优势。

如果数组的值不是真正随机分布的,我发现第一个实现中使用的简单if(array[i] < 0)实际上要快得多。

以下是我用来进行基准测试的代码:

#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#include <stdint.h>
#ifdef _OPT3
#include <emmintrin.h>
#include <tmmintrin.h>
#endif

int main(int argc, char **argv)
{
        int *array;
        struct timespec tsstart, tsend;
        int ncount = 500000000;
        int i;

        array = malloc(sizeof(int) * ncount);

        for(i = 0; i < ncount; i++)
        {
                array[i] = rand();
#ifdef _DIST
                if(rand() % 100 == 0) // make the values less likely to be negative.
#else
                if(rand() % 2 == 0) // the values are equeally likely to be negaitve as positive.
#endif
                        array[i] = -rand();
        }

        clock_gettime(CLOCK_MONOTONIC, &tsstart);

#ifdef _OPT1
        for(i = 0; i < ncount; i++)
        {
                uint32_t ntemp = array[i] >> 31;
                array[i] ^= ntemp;
                array[i] += ntemp & 1;
        }
#elif _OPT2
        for(ncount--; ncount >= 0; ncount--)
        {
                uint32_t ntemp = array[ncount] >> 31;
                array[ncount] ^= ntemp;
                array[ncount] += ntemp & 1;
        }
#elif _OPT3
        for(i = 0; i < ncount; i+=4)
        {
                __m128i a3_a2_a1_a0 = _mm_loadu_si128((__m128i*)&array[i]);         //Load 4 int32 elements from array.
                a3_a2_a1_a0 = _mm_abs_epi32(a3_a2_a1_a0);                           //Set absolute of 4 int32 elements in single instruction.
                _mm_storeu_si128((__m128i*)(&array[i]), a3_a2_a1_a0);               //Store 4 int32 elements of array.
        }
#elif _OPT4
        for(i = 0; i < ncount; i++)
        {
                array[i] = abs(array[i]); // abs() is actually an intrinsic on gcc and msvc
        }
#else
        for(i = 0; i < ncount; i++)
        {
                if(array[i] < 0)
                {
                        array[i] = -array[i];
                }
        }
#endif

        clock_gettime(CLOCK_MONOTONIC, &tsend);

        printf("start: %ld.%09ld\n", tsstart.tv_sec, tsstart.tv_nsec);
        printf("end: %ld.%09ld\n", tsend.tv_sec, tsend.tv_nsec);

        tsend.tv_sec -= tsstart.tv_sec;
        tsend.tv_nsec -= tsstart.tv_nsec;
        if(tsend.tv_nsec < 0)
        {
                tsend.tv_sec--;
                tsend.tv_nsec = 1000000000 + tsend.tv_nsec;
        }
        printf("diff: %ld.%09ld\n", tsend.tv_sec, tsend.tv_nsec);

        free(array);

        return 0;
}

测试结果

这是我的结果(时间以秒为单位)。这些测试在Intel(R)Xeon(R)CPU W3580 @ 3.33GHz上运行。 gcc(Debian 4.9.2-10)4.9.2

// Implimentation One (No Optimizations)
$ gcc -O3 -march=native test.c
$ ./a.out
start: 9221396.418007954
end: 9221398.103490309
diff: 1.685482355

// Implimentation One Non Random Distrubution
$ gcc -D_DIST -O3 -march=native test.c
$ ./a.out
start: 9221515.889463124
end: 9221516.255742919
diff: 0.366279795

// Implementation Two (Branchless)
$ gcc -D_OPT1 -O3 -march=native test.c
$ ./a.out
start: 9221472.539690988
end: 9221472.787347636
diff: 0.247656648

// Implementation Three (Branchless Decrement)
$ gcc -D_OPT2 -O3 -march=native test.c
$ ./a.out
start: 9221930.068693139
end: 9221930.334575475
diff: 0.265882336

// Rotem's Implementation (SIMD)
$ gcc -D_OPT3 -O3 -march=native test.c
$ ./a.out
start: 9222076.001094679
end: 9222076.230432423
diff: 0.229337744

// Inuitive abs() Implementation
$ gcc -D_OPT4 -O3 -march=native test.c
$ ./a.out
start: 9222112.523690484
end: 9222112.754820240
diff: 0.231129756
// Inuitive abs() Implementation Without native
$ gcc -D_OPT4 -O3 test.c
$ ./a.out
start: 9223301.744006196
end: 9223301.974097927
diff: 0.230091731

结论

我对此的看法是,处理分支预测的硬件优化可能比任何基于软件的优化都能显着加快代码执行速度并提高速度。通过尝试优化分支,您创建的代码无论正在处理的数据如何都执行相同的步骤。因此,当它在恒定时间内执行时,如果数据不是完美随机分布的,那么实际上可能会使执行速度变慢。

更新:我在打开编译器优化的情况下进行了一些测试,发现了不完全支持我之前得出的结论的不同结果。

根据我的经验,我发现如果您只需编写更少的代码,那通常是最佳的优化方式。看起来指令越少,无论硬件功能如何,执行的速度就越快。

我期待阅读有关此练习的任何评论。

更新

我已经添加了Rotem实现的结果。此代码超快,证明您的指令越少,执行时间越快。干得好Rotem!

更新2

我今天进行了一些广泛的测试,发现在开启编译器优化(如gcc -O3)时,微调优化(如改变for循环计数的方式)绝对没有效果。编译器最终生成组件,在数组指针上执行指针比较,以测试我们是否已到达结束。

当编译器与gcc -O3一起运行时,优化Rotem提供的SSE代码也没有区别,因为它正确地对齐16字节边界上的内存,从而删除了_mm_loadu_si128() / _mm_storeu_si128()必要性。

最终更新

我添加了另一个使用简单直观的abs()函数的实现。结果是gcc上的abs(),而MSVC实际上是编译器内在的。我只是使用gcc -O3优化来重新编写所有测试结果。

正如您所看到的,Rotem的SIMD实现和abs()实现速度最快,其次是两个XOR实现,最后是分支实现。

在两个XOR实现中,递减for循环的实际上稍慢,因为它的循环包含14条指令,而增量循环只包含13条。

Rotem的SIMD实现和abs()实现实际上都依赖于PABSD指令,并且两者都有包含7条指令的循环。然而,速度的微小差异(SIMD稍微快一点)来自于优化的SIMD实现假定存储器将始终包含4个整数(128位)的倍数,而abs()实现需要额外的指令来测试其中的情况内存不包含4个整数的倍数。

令人惊奇的是,通过简单地使用abs(),我们可以实现与SIMD几乎完全相同的速度,并且调用C库函数非常简单。不使用abs()的{​​{1}}循环只需要4个指令,而不是-march=native,它使用PABSDPSRADPXOR指令。

为什么可移植PSUBD比XOR实现更快?

事实证明,可移植(或非本机)abs()程序集几乎与XOR实现完全相同。

这是abs()

abs()

这是异或:

psrad   $31, %xmm0
pxor    %xmm0, %xmm1
psubd   %xmm0, %xmm1

现在让我们将它们转换回C代码:

这是psrad $31, %xmm1 movdqa %xmm1, %xmm2 pxor %xmm1, %xmm0 pand %xmm3, %xmm2 paddd %xmm2, %xmm0

abs()

这是异或:

int ntemp = array[i] >> 31;
array[i] ^= ntemp;
array[i] -= ntemp;

不同之处在于我们在原始XOR实现中有一个额外的按位AND运算。

最终结论

使用uint32_t ntemp = array[i] >> 31; array[i] ^= ntemp; array[i] += ntemp & 1; ! :d

答案 1 :(得分:3)

为了获得最佳性能,我建议您使用SIMD说明 不同的处理器支持不同的SIMD指令集。

使用手动SIMD指令优化的常用方法是使用C intrinsic函数。

以下示例使用SSE内在函数:

#include <intrin.h>

//Limitations:
//1. n must be a multiple of 4.
void absolute(const int array[], int n)
{
    int x;

    //Process 4 elements per iteration.
    for (x = 0; x < n; x += 4)
    {  
        __m128i a3_a2_a1_a0 = _mm_loadu_si128((__m128i*)&array[x]);     //Load 4 int32 elements from array.

        a3_a2_a1_a0 = _mm_abs_epi32(a3_a2_a1_a0);                       //Set absolute of 4 int32 elements in single instruction.

        _mm_storeu_si128((__m128i*)(&array[x]), a3_a2_a1_a0);           //Store 4 int32 elements of array.
    }
}

考虑一下:这只是一个例子(不是最好的表现)。

感谢 Brandon 测量我的代码示例。