嵌套循环遍历数组

时间:2012-11-10 23:30:13

标签: c performance optimization assembly

有2个非常大的系列元素,第二个比第一个大100倍。对于第一个系列的每个元素,第二个系列中有0个或更多元素。这可以通过2个嵌套循环遍历和处理。但是第一个阵列中每个成员的匹配元素数量的不可预测性使得事情非常非常缓慢。

第二系列元素的实际处理涉及逻辑和(&)以及人口数。

我找不到使用C的好的优化,但我正在考虑使用内联asm,为第一个系列的每个元素执行rep * mov *或类似操作,然后对第二个系列的匹配字节进行批处理,也许在缓冲区1MB或其他东西。但代码会变得非常混乱。

有人知道更好的方法吗? C首选,但x86 ASM也行。非常感谢!

简化问题的示例/演示代码,第一个系列是“人”,第二个系列是“事件”,为了清楚起见。 (最初的问题实际上是100米和10,000米的条目!)

#include <stdio.h>
#include <stdint.h>

#define PEOPLE 1000000    //   1m
struct Person {
    uint8_t age;   // Filtering condition
    uint8_t cnt;   // Number of events for this person in E
} P[PEOPLE]; // Each has 0 or more bytes with bit flags

#define EVENTS 100000000  // 100m
uint8_t P1[EVENTS]; // Property 1 flags
uint8_t P2[EVENTS]; // Property 2 flags

void init_arrays() {
    for (int i = 0; i < PEOPLE; i++) { // just some stuff
        P[i].age = i & 0x07;
        P[i].cnt = i % 220; // assert( sum < EVENTS );
    }
    for (int i = 0; i < EVENTS; i++) {
        P1[i]    = i % 7;  // just some stuff
        P2[i]    = i % 9;  // just some other stuff
    }
}

int main(int argc, char *argv[])
{
    uint64_t   sum = 0, fcur = 0;

    int age_filter = 7; // just some

    init_arrays();      // Init P, P1, P2

    for (int64_t p = 0; p < PEOPLE ; p++)
        if (P[p].age < age_filter)
            for (int64_t e = 0; e < P[p].cnt ; e++, fcur++)
                sum += __builtin_popcount( P1[fcur] & P2[fcur] );
        else
            fcur += P[p].cnt; // skip this person's events

    printf("(dummy %ld %ld)\n", sum, fcur );

    return 0;
}

gcc -O5 -march=native -std=c99 test.c -o test

7 个答案:

答案 0 :(得分:4)

由于平均每人可获得100件物品,因此您可以通过一次处理多个字节来加快速度。我稍微重新安排了代码,以便使用指针而不是索引,并将一个循环替换为两个循环:

uint8_t *p1 = P1, *p2 = P2;
for (int64_t p = 0; p < PEOPLE ; p++) {
    if (P[p].age < age_filter) {
        int64_t e = P[p].cnt;
        for ( ; e >= 8 ; e -= 8) {
            sum += __builtin_popcountll( *((long long*)p1) & *((long long*)p2) );
            p1 += 8;
            p2 += 8;
        }
        for ( ; e ; e--) {
            sum += __builtin_popcount( *p1++ & *p2++ );
        }
    } else {
        p1 += P[p].cnt;
        p2 += P[p].cnt;
    }
}

在我的测试中,这会将您的代码从1.515升级到0.855秒。

答案 1 :(得分:2)

Neil的回答不需要按年龄排序,这可能是一个好主意 -

如果第二个循环有孔(请更正原始源代码以支持该想法),常见的解决方案是cumsum[n+1]=cumsum[n]+__popcount(P[n]&P2[n]);
然后为每个人   sum+=cumsum[fcur + P[p].cnt] - cumsum[fcur];

无论如何,似乎计算负担仅仅是订单事件,而不是EVENTS * PEOPLE。无论如何,可以通过为符合条件的所有连续人员调用内循环来进行一些优化。

如果确实存在最多8个谓词,那么预先计算所有的谓词是有意义的 sums (_popcounts(predicate[0..255]))让每个人进入单独的数组C [256] [PEOPLE]。这只是内存需求的两倍(在磁盘上?),但将搜索从10GB + 10GB + ... + 10GB(8个谓词)本地化为一个200MB的流(假设16位条目)。

取决于p的概率(P [i] .age&lt; condition&amp;&amp; P [i] .height&lt; cond2),计算累积和可能不再有意义。也许,也许不是。更可能的是,一次只有一些SSE并行性8或16个人会这样做。

答案 2 :(得分:2)

一种全新的方法可能是使用ROBDDs来编码每个人/每个事件的真值表。首先,如果事件表不是非常随机或者它们不包含病理函数,例如bignum乘法的真值表,则第一个可以实现函数的压缩,其次,真值表的算术运算可以以压缩形式计算。每个子树可以在用户之间共享,并且两个相同子树的每个算术运算只需要计算一次。

答案 3 :(得分:1)

我不知道您的示例代码是否准确反映了您的问题,但可以像下面这样重写:

for (int64_t p = 0; p < PEOPLE ; p++)
    if (P[p].age < age_filter)
        fcur += P[p].cnt;

for (int64_t e = 0; e < fcur ; e++)
    sum += __builtin_popcount( P1[e] & P2[e] );

答案 4 :(得分:0)

我不知道gcc -O5(这里似乎没有记录)并且似乎与我的gcc 4.5.4生成与gcc -O3完全相同的代码(但是,仅在相对较小的代码示例上测试)

取决于您想要达到的目标,-O3可能比-O2

与您的问题一样,我建议您考虑更多关于数据结构而不是实际算法。 只要您的数据没有以方便的方式表达,您就不应该专注于通过适当的算法/代码优化来解决问题。

如果您想根据单个条件快速剪切大量数据(此处为示例中的年龄),我建议您使用已排序树的变体。

答案 5 :(得分:0)

如果您的实际数据(年龄,计数等)确实是8位,则计算中可能存在大量冗余。在这种情况下,您可以用查找表替换处理 - 对于每个8位值,您将有256个可能的输出,而不是计算,可以从表中读取计算数据。

答案 6 :(得分:0)

要解决分支错误预测(在其他答案中缺失),代码可以执行以下操作:

#ifdef MISPREDICTIONS
if (cond)
    sum += value
#else
mask = - (cond == 0);  // cond: 0 then -0, binary 00..; cond: 1 then -1, binary 11..
sum += (value & mask); // if mask is 0 sum value, else sums 0
#endif

由于存在数据依赖性(想想超标量cpu),它并不是完全免费的。但对于大多数不可预测的情况,它通常会提高10倍。