【问题标题】:Manually optimize a nested loop手动优化嵌套循环
【发布时间】:2011-05-10 15:36:16
【问题描述】:

我正在做一个家庭作业,我必须手动优化嵌套循环(我的程序将在禁用优化的情况下编译)。作业的目标是在 6 秒内运行整个程序(额外得分少于 4.5 秒)。

我只允许修改一小段代码,起点是这样的:

    for (j=0; j < ARRAY_SIZE; j++) {
        sum += array[j];
    }

其中ARRAY_SIZE 是 9973。此循环包含在另一个循环中,该循环运行了 200,000 次。这个特定版本在 16 秒内运行。

到目前为止,我所做的是更改实现以展开循环并使用指针作为我的迭代器:

(这些声明不会循环超过 200,000 次)

 register int unroll_length = 16;
 register int *unroll_end = array + (ARRAY_SIZE - (ARRAY_SIZE % unroll_length));
 register int *end = array + (ARRAY_SIZE -1);
 register int *curr_end;

curr_end = end;

while (unroll_end != curr_end) {
 sum += *curr_end;
 curr_end--;
}

do {
 sum += *curr_end + *(curr_end-1) + *(curr_end-2) + *(curr_end-3) +
  *(curr_end-4) + *(curr_end-5) + *(curr_end-6) + *(curr_end-7) +
  *(curr_end-8) + *(curr_end-9) + *(curr_end-10) + *(curr_end-11) +
  *(curr_end-12) + *(curr_end-13) + *(curr_end-14) + *(curr_end-15);
}
while ((curr_end -=  unroll_length) != array);

sum += *curr_end;

使用这些技术,我能够将执行时间缩短到 5.5 秒,这将是我的功劳。然而;我确实想获得额外的积分,但我也很好奇我可以做哪些额外的优化而我可能会忽略?

编辑#1(添加外循环)

 srand(time(NULL));
 for(j = 0; j < ARRAY_SIZE; j++) {
  x = rand() / (int)(((unsigned)RAND_MAX + 1) / 14);
  array[j] = x;
  checksum += x;
 }

 for (i = 0; i < N_TIMES; i++) {

  // inner loop goes here

  if (sum != checksum)
   printf("Checksum error!\n");

  sum = 0;

 }

【问题讨论】:

  • 被求和的数组每次都会改变吗?看看外循环也会很有帮助。
  • 数组没有变化。我已将外部循环添加到问题中。
  • 按 4 或 8 步进行。尝试查看分项是否有帮助。查看生成的程序集,调试版本确实喜欢将值写回堆栈,只是为了提供更好的调试器反馈。
  • 预计算你可以的东西:(ARRAY_SIZE - (ARRAY_SIZE % 10)),取模可能很昂贵。
  • ……当然,Duff’s device 对于展开内部循环很有用。

标签: c optimization


【解决方案1】:

您可以尝试将变量存储在 CPU 寄存器中:

register int *unroll_limit = array + (ARRAY_SIZE - (ARRAY_SIZE % 10));
register int *end = array + ARRAY_SIZE;
register int *curr;

并尝试使用不同大小的手动循环来检查何时最大化缓存使用率。

【讨论】:

  • 大多数编译器都会忽略 register 关键字。
  • @Jasper:由于作业评分显然是基于在特定系统上使用特定编译器运行的,因此值得一试。它可能有效,也可能无效,但不会受到伤害。
【解决方案2】:

我假设您使用的是 x86,如果您不是,大部分内容仍然适用,但细节有所不同。

  1. 使用 SIMD/SSE,这将使您毫不费力地提高 4 倍的速度,它需要 16 字节对齐的数据,您可以使用 _aligned_malloc 或常规 malloc + 手动对齐获得。除此之外,在这种情况下,您只需要_mm_add_epi32 即可同时进行四次加法。 (不同的架构有不同的 SIMD 单元,请检查您的)。
  2. 在这种情况下使用多线程/多核,最简单的方法是让每个线程将数组的一半加到一个临时变量中,并在完成后将这两个结果相加。这将在可用内核数量上线性扩展。
  3. 预取到 L1 缓存;这仅在您拥有一个庞大的阵列并且确保能够在至少约 200 个周期内对 CPU 施加压力(例如,往返于主 RAM)时才有效。
  4. 完全不遗余力地优化它并使用基于 GPU 的方法。这将要求您设置 CUDA 或 OpenCL 环境并将阵列上传到 GPU。这大约是 ~400 LoC,不包括计算内核。但是,如果您有一个小数据集(例如,设置/拆除的开销太大)或者如果您有一个 巨大 不断变化的数据集(例如,花太多时间流式传输到GPU)。
  5. 对齐页面边界以防止在通常为 4K 大小的窗口上出现页面错误(昂贵)。
  6. 在考虑双重发布指令和指令延迟的同时手动展开循环。此信息可从您的 CPU 制造商处获得(英特尔也提供这些信息)。但在 x86 上,这并不是真正有用,因为它的 CPU 无序执行。
  7. 根据您的平台,实际将数据传送到 CPU 进行处理是最慢的部分(这主要适用于最近的控制台和 PS,我从未为小型嵌入式设备开发过),因此您需要为此进行优化.当循环是瓶颈时,在 6502 上向后迭代之类的技巧很不错,但现在您需要线性访问 RAM。
  8. 如果您碰巧使用的是具有快速 RAM 的机器(例如,不是 PC/控制台),那么从普通数组转换为更花哨的数据结构(例如,执行更多指针追踪的数据结构)可能完全值得它。

总而言之,我认为 1 和 2 是最简单和最可行的,并且会为您带来足够多的性能(例如,Core 2 Duo 上的 8 倍)。然而,这一切都归结为了解您的硬件和编程 PIC 将需要与一般 PC 完全不同的优化(例如指令级手动流水线)。

【讨论】:

  • 不错的建议,尽管您实际上应该能够在 cuda 中获得一个大约 120loc(带有 cmets 和空行)的优化优化的 reduce 程序。而且我知道,因为编写 reduce 函数可能是每个人使用 cuda 做的第一件事;)
  • @Voo 我做了一个光线追踪器,但是对于设置部分来说,它比 400 更接近 120loc。我想我会编辑那个。 code.google.com/p/basic-opencl-raytracer
【解决方案3】:
  • 尝试在页面边界(即 4K)上对齐数组

  • 尝试使用更广泛的数据类型进行计算,即 64 位而不是 32 位整数。这样,您可以一次添加 2 个数字。作为最后一步,将两半相加。

  • 将部分数组或计算转换为浮点数,以便您可以并行使用 FPU 和 CPU

  • 我不希望允许以下建议,但我还是会提到它们

    • 多线程
    • 专用 CPU 指令,即 SSE

【讨论】:

  • 第二个建议+1(使用较大的类型求和,然后折叠总和)
  • (顺便说一句,TCP/IP csums 通常是使用 32/64 位算法计算的,然后将结果折叠起来)。
【解决方案4】:

如果数组值没有变化,您可以记住总和(即在第一次运行时计算它,并在后续运行中使用计算的总和)。

【讨论】:

    【解决方案5】:

    一些不错的优化技巧:

    • 使您的循环计数从 ARRAY_SIZE 倒数为 0,这样您就可以从代码中删除比较。较少的比较会加快程序的运行速度。
    • 此外,现在 x86 已针对短循环进行了优化,它们可以“预加载”以比正常运行更快。
    • 尽可能使用寄存器
    • 使用指针代替数组索引

    因此,如果您要使用数组,请尝试使用:

    register int idx = ARRAY_SIZE - 1;
    register int sum = 0;
    do {
        sum += array[idx];
    } while (idx-- % 10 != 0);
    
    do {
        sum += array[idx] + array[idx - 1] + array[idx - 2] + array[idx - 3] + array[idx - 4] + array[idx - 5] + array[idx - 6] + array[idx - 7] + array[idx - 8] + array[idx - 9];
    } while (idx -= 10);
    // now we don't use a comparison and the ZERO flag will be set in FLAG
    // register on which we can conditional jump. With a comparison you do VALUE - VALUE
    // and then check if the ZERO flag is set or the NEGATIVE flag or whatever you are testing on
    

    【讨论】:

    • 您也可以使用array[idx--] + array[idx--] + array[idx--] 等并将idx -= 10 替换为idx 以删除449 个虚假计算。
    • 您不能在一个表达式中多次递减同一个变量。这会导致未定义的行为。
    • 还有什么时候用括号括起来所有项目?然后您需要进行更多输入并将它们全部放在单独的行上:) 此外,您还需要在最后一个循环之后放置最后一个 sum += array[idx] 以添加第一个元素。
    猜你喜欢
    • 2021-01-21
    • 1970-01-01
    • 1970-01-01
    • 2012-01-27
    • 2015-05-29
    • 2020-05-18
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多