手动优化嵌套循环

时间:2011-05-10 15:36:17

标签: c optimization

我正在做家庭作业,我必须手动优化嵌套循环(我的程序将在禁用优化的情况下编译)。分配的目标是在不到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;

 }

5 个答案:

答案 0 :(得分:3)

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

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

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

答案 1 :(得分:2)

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

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

答案 2 :(得分:1)

  • 尝试在页面边界上对齐数组(即4K)

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

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

  • 我不希望允许以下建议,但无论如何我都提到了

    • 多线程
    • 专门的CPU指令,即SSE

答案 3 :(得分:0)

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

答案 4 :(得分:-1)

一些不错的优化技巧:

  • 使您的循环从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