可以在0的1上更快地求和吗?

时间:2012-03-31 21:01:13

标签: c parallel-processing bit-manipulation

我有一个非常大的数组(例如1000万个元素),它只包含1和0。我还有一堆并行线程(例如10个),我想将这个大型数组块分成不同的线程,并使每个线程对它们负责的部分求和。

我在C&使用“+”运算符进行pthreads。但是,由于数组只包含1和0,我想知道有没有更快的方法来实现这个求和? (通过按位运算符,移位等?)因为我正在处理非常大的数组,所以天真的求和会破坏性能。

7 个答案:

答案 0 :(得分:6)

你在现代CPU上添加2个1000万个元素的数组......每秒可以执行大约3亿个指令(3GHz)。

即使必须单独添加每个元素,也可以在0.003秒内添加两个完整的数组。 (这确实是最糟糕的情况。在64位计算机上,你应该一次能够添加64个元素)

除非在内部循环中发生这种情况,否则这不应该会破坏性能。

考虑更全面地描述您的问题,并展示您当前的实施。

答案 1 :(得分:1)

首先,转换为执行SIMD向量和,并将向量寄存器的元素在循环外的末尾减少为单个和。这应该会在1/4的操作中得到相同的结果。然后展开该向量化循环,每个展开的迭代在单独的向量中求和,以暴露更大的指令级并行性,并在末尾组合部分和。有了它,你应该很容易地最大化内存带宽。

答案 2 :(得分:1)

如果您可以使用所有位而不是每个int使用1,那么性能至少可以提高;)

还使用SSE__m128i _mm_add_epi32registers等等(eka)进行了测试,但未获得任何明显的提升。 (很可能我没有正确地做到这一点。)。

一切都很大程度上取决于环境如何创建数组,如何在其他地方使用等等。人们可以查看GPU处理,但这又变得专业化,并且可能更好地用于更重的计算然后+

无论如何,这是我在P4 2.8GHz上使用2G SDRAM进行的粗略样本结果;使用正常 1增量循环,展开2和8(在一个数字pr.int上),以及从CountBitsSetParallel结合展开的第二位旋转。既有线程也没有。如果您决定将它与线程结合使用,那么小心点错。

./bcn -z330000000 -s3 -i1
sz_i      : 330000000 * 4 = 1320000000 (bytes int array)
sz_bi     :  10312500 * 4 =   41250000 (bytes bit array)
set every :         3 (+ 1 controll-bit)
iterations:         1

Allocated 1320000000 bytes for ari    (0x68cff008 - 0xb77d8a08)
            1289062 KiB
               1258 MiB
                  1 GiB
Allocated  41250000 bytes for arbi   (0x665a8008 - 0x68cfecd8)
              40283 KiB
                 39 MiB
Setting values ...
--START--
    1 iteration over 330,000,000 values
Running TEST_00 Int Normal    ; sum = 110000001 ... time: 0.618463440
Running TEST_01 Int Unroll 2  ; sum = 110000001 ... time: 0.443277919
Running TEST_02 Int Unroll 8  ; sum = 110000001 ... time: 0.425574923
Running TEST_03 Int Bit Calc  ; sum = 110000001 ... time: 0.068396207
Running TEST_04 Int Bit Table ; sum = 110000001 ... time: 0.056727713

...

    1 iteration over 200,000,000
Running TEST_00 Int Normal    ; sum = 66666668 ... time: 0.339017852
Running TEST_01 Int Unroll 2  ; sum = 66666668 ... time: 0.273805886
Running TEST_02 Int Unroll 8  ; sum = 66666668 ... time: 0.264436688
Running TEST_03 Int Bit Calc  ; sum = 66666668 ... time: 0.032404574
Running TEST_04 Int Bit Table ; sum = 66666668 ... time: 0.034900498

...

  100 iterations over 2,000,000 values
Running TEST_00 Int Normal    ; sum = 666668 ... time: 0.373892700
Running TEST_01 Int Unroll 2  ; sum = 666668 ... time: 0.270294678
Running TEST_02 Int Unroll 8  ; sum = 666668 ... time: 0.260143237
Running TEST_03 Int Bit Calc  ; sum = 666668 ... time: 0.031871318
Running TEST_04 Int Bit Table ; sum = 666668 ... time: 0.035358995

...

    1 iteration over 10,000,000 values
Running TEST_00 Int Normal    ; sum = 3333335 ... time: 0.023332354
Running TEST_01 Int Unroll 2  ; sum = 3333335 ... time: 0.011932137
Running TEST_02 Int Unroll 8  ; sum = 3333335 ... time: 0.013220130
Running TEST_03 Int Bit Calc  ; sum = 3333335 ... time: 0.002068979
Running TEST_04 Int Bit Table ; sum = 3333335 ... time: 0.001758484

主题......

 4 threads, 1 iteration pr. thread over 200,000,000 values
Running TEST_00 Int Normal    ; sum = 66666668 ... time: 0.285753177
Running TEST_01 Int Unroll 2  ; sum = 66666668 ... time: 0.263798773
Running TEST_02 Int Unroll 8  ; sum = 66666668 ... time: 0.254483912
Running TEST_03 Int Bit Calc  ; sum = 66666668 ... time: 0.031457365
Running TEST_04 Int Bit Table ; sum = 66666668 ... time: 0.036319760

Snip(对不起,短命名):

/* I used an array named "ari" for integer 1 value based array, and
   "arbi" for integer array with bits set to 0 or 1.

   #define SZ_I : number of elements (int based)
   #define SZ_BI: number of elements (bit based) on number of SZ_I, or
      as I did also by user input (argv)
 */

#define INT_BIT     (CHAR_BIT * sizeof(int))

#define SZ_I    (100000000U)
#define SZ_BI   ((SZ_I / INT_BIT ) + (SZ_I / INT_BIT  * INT_BIT  != SZ_I))

static unsigned int sz_i  = SZ_I;
static unsigned int sz_bi = SZ_BI;

static unsigned int   *ari;
static unsigned int   *arbi;

/* (if value (sz_i) from argv ) */
sz_bi = sz_i  / INT_BIT + (sz_i / INT_BIT  * INT_BIT  != sz_i);

...
#define UNROLL  8


static __inline__ unsigned int bitcnt(unsigned int v)
{
    v = v - ((v >> 1) & 0x55555555);
    v = (v & 0x33333333) + ((v >> 2) & 0x33333333);
    return (((v + (v >> 4)) & 0xF0F0F0F) * 0x1010101) >> 24;
}

unsigned int test_03(void)
{
    unsigned int i   = 0;
    unsigned int sum = 0;
    unsigned int rep = (sz_bi / UNROLL);
    unsigned int rst = (sz_bi % UNROLL);

    while (rep-- > 0) {
        sum += bitcnt(arbi[i]);
        sum += bitcnt(arbi[i+1]);
        sum += bitcnt(arbi[i+2]);
        sum += bitcnt(arbi[i+3]);
        sum += bitcnt(arbi[i+4]);
        sum += bitcnt(arbi[i+5]);
        sum += bitcnt(arbi[i+6]);
        sum += bitcnt(arbi[i+7]);
        i += UNROLL;
    }

    switch (rst) {
    case 7: sum += bitcnt(arbi[i+6]);
    case 6: sum += bitcnt(arbi[i+5]);
    case 5: sum += bitcnt(arbi[i+4]);
    case 4: sum += bitcnt(arbi[i+3]);
    case 3: sum += bitcnt(arbi[i+2]);
    case 2: sum += bitcnt(arbi[i+1]);
    case 1: sum += bitcnt(arbi[i]);
    case 0:;
    }

    return sum;
}

答案 3 :(得分:0)

你提到了一系列的int,所以像这样: int array [...];

如果您使用64位os + cpu,您可能希望将其强制转换为long long(或__int64,具体取决于您的平台) - 基本上是8位整数。 所以你这样做:

int array[...];
...
unsigned long long *longArray;
unsigned long long sum;
for (longArray = &array[number of elements in array]; longArray != array;)
{
    --longArray;
    sum += *longArray;
}

if (the number of elements in original array % 2 == 1)
    sum += array[number of elements in original array - 1];
sum = (sum >> 32) + (sum & 0xFFFFFFFF); // basically add up the first 4 bytes and second 4 bytes
return sum;

但试试看,我并不完全确定它会更快。

答案 4 :(得分:0)

如果通过求和每个值的总和,基本上,计算有多少1,那么我认为唯一的方法是从数组/块中添加每个值...用位运算符重新实现add指令,我认为它可能比使用cpu的添加速度慢;但也许它可能取决于cpu。

另外,跳过0并不比添加它们快(跳跃很慢)......

我想到的唯一可能加快速度的事情就是以一种可以利用目标CPU特殊指令的方式打包(从头开始)数据。有些CPU有一些指令可以很容易(而且我认为很快)获得寄存器的总数。 32位寄存器可以容纳32位(数组的32个元素),你可以"求和"它们只有一条指令(特定的CPU ......)。那么你当然必须将结果总结为"全局部分"线程的结果;无论如何这样你减少了添加指令的数量(32加成为1单指令)。这应该与Novelocrat的答案完全一致(例如,向量寄存器的元素是人口计数的结果)。

最近" x86" cpus有人口统计指令,请参阅this link on wikipedia

答案 5 :(得分:0)

在大多数处理器上,add指令是最快的指令之一。计算元素地址,获取元素,根据需要加宽元素等的逻辑将使实际的加法值淹没4-10倍(如果编译器插入数组边界检查,中断点等,甚至更多)。

当然,首先要做的是将数组索引转换为指针增量。接下来,可以将循环展开20次。一个好的优化编译器可能会做同等的事情,但在这种情况下,你可能(或可能不)能够做得更好。

另一个技巧,特别是如果你有一个字节数组,就是做一些类似于Novelocrat建议的东西(显然是Alex建议的) - 强制指向long的指针的数组指针并获取多个数组元素一次,然后在一次操作的同时添加多个元素(在字节的情况下为4)。当然,对于字节,你必须至少每255次迭代停止并分解,以防止一个字节溢出到下一个字节。

你需要注意在一个线程中“在空中”保留太多的值。只有很多处理器寄存器,并且您希望在寄存器中保留所有值(元素指针,迭代计数器,累加器,抓取元素的临时寄存器等)。

但很快存储访问将成为瓶颈。

答案 6 :(得分:0)

您可能会发现数组索引生成的代码比指针索引更好。看看编译器生成的汇编程序是否确定。使用gcc,这是-S选项。在我的iMac上使用gcc v4.2.1,我看到索引生成更短的代码,虽然我不知道x86汇编程序我不能说它是否实际上更快。

BTW,是由硬件或外部约束强制要求的int数组吗?