【问题标题】:Nested loop traversing arrays嵌套循环遍历数组
【发布时间】:2012-11-10 23:30:12
【问题描述】:

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

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

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

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

为清楚起见,带有简化问题的示例/演示代码,第一个系列是“人”,第二个系列是“事件”。 (原来的问题其实是100m和10000m条目!)

#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

【问题讨论】:

  • 你可能会更受记忆的束缚...
  • 是的。有固定尺寸,是的,这就是我的目标。但是对于可变大小的匹配,由于内部循环(根据 valgrind),它会慢得多。条件上也存在分支错误预测,但我认为这更容易摆脱。
  • 看看Loop Tiling。这可能会给你带来最大的进步。
  • 只是一个猜测,但您可以尝试在循环之前进行 popcount,因为看起来每个元素对 P1[i] 和 P2[i] 都已完成。这将允许一次完成所有弹出计数,这可能会给您一些时间收益
  • @alecco 您确定示例代码不需要else 分支来处理此人未通过年龄过滤器的情况吗?不应该有fcur += P[p].cnt吗?否则,内部循环将消耗其他人的事件...

标签: c performance optimization assembly


【解决方案1】:

由于您平均每人获得 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 倍的改进。虽然它不应该是__builtin_popcountll 吗?我也会考虑使用 128 位寄存器。
  • @VladLazarenko 啊,您对__builtin_popcountll 的看法是完全正确的!非常感谢,这个问题已经解决了。
  • 好一个。遗憾的是,分布不正常,中位数可能远低于 64 位。类似的优化是将每人“四舍五入”到 16、32 或 64 位块,但这是一个巨大的内存权衡,因为有这么多人只有很少的事件(例如 0 或 1)支持 +赏金跨度>
  • 这是最接近答案的。谢谢。
【解决方案2】:

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

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

无论如何,计算负担似乎只是事件的顺序,而不是事件*人。无论如何,都可以通过为满足条件的所有连续人员调用内部循环来进行一些优化。

如果确实有最多 8 个谓词,则可以将每个人的所有
sums (_popcounts(predicate[0..255])) 预先计算到单独的数组 C[256][PEOPLE] 中。这几乎使内存需求翻了一番(在磁盘上?),但是将搜索从 10GB+10GB+...+10GB(8 个谓词)本地化到一个 200MB 的流(假设 16 位条目)。

根据 p(P[i].age

【讨论】:

  • 实际上性能是主要驱动力,它必须适合 RAM。而“&”是一种简化。它可以是 !(a) & (b) 或其他组合。我已经用宏和函数指针表解决了这个问题,我宁愿拥有 100k 的函数而不是 1000GB 的存储空间 :) 但是感谢您周到的回答,请点赞!
  • 您的系统至少有 80GB 内存?
【解决方案3】:

一种全新的方法是使用ROBDDs 对每个人/每个事件的真值表进行编码。首先,如果事件表不是很随机,或者不是由病态函数组成,比如大数乘法的真值表,那么第一个可以实现函数的压缩,第二个真值表的算术运算可以压缩形式计算.每个子树可以在用户之间共享,两个相同子树的每个算术运算只需要计算一次。

【讨论】:

    【解决方案4】:

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

    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] );
    

    【讨论】:

    • 也许我不清楚。第一个系列(“人”)不是按“年龄”排序的,实际上有多个属性(例如“身高”)。所以在运行代码的时候,在第二个系列上会出现空隙,属于第一个系列中不符合搜索条件的元素。但是感谢您的回答,无论如何都要投赞成票。
    【解决方案5】:

    我不知道 gcc -O5(这里似乎没有记录),并且似乎在我的 gcc 4.5.4 中产生了与 gcc -O3 完全相同的代码(虽然,只在相对较小的代码示例上进行了测试)

    根据您想要实现的目标,-O3 可能比 -O2 慢

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

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

    【讨论】:

    • -O3 以上的所有优化选项都等效于 GCC 中的 -O3(在其他编译器中可能不是)。
    • O5 失误了,是的,很抱歉。然而,它是以提供一切帮助的名义。
    【解决方案6】:

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

    【讨论】:

    • 查询可以是多列的,例如超过 21 且高于 6 英尺。
    【解决方案7】:

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

    #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 倍的提升。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-12-25
      • 1970-01-01
      • 1970-01-01
      • 2021-05-31
      • 2013-05-08
      相关资源
      最近更新 更多