循环平铺。如何选择块大小?

时间:2013-12-04 05:02:58

标签: c performance loops optimization cpu-cache

我正在尝试学习循环优化。我发现循环平铺有助于使数组循环更快。我尝试使用下面给出的两个代码块,有或没有循环阻塞,并测量两者的时间。我大部分时间都没有发现明显的差异。我测试了不同的块大小,但我不知道如何选择块大小。如果我的方向错了,请帮助我。实际上我发现没有块的循环可以多次运行。

一个。阻止

int max = 1000000;
int B = 100;
for (i = 0; i < max; i += B)
{
     for (j = i; j < (i + B); j++)
     {
         array[j] = 0;
     }
 }

湾不阻止

for (i = 0; i < max; i++)
{
    array[i] = 0;
 }

所花时间: 阻塞:经过时间 - 6997000 Nano Secs

没有阻止经过的时间 - 6097000 Nano Secs

1 个答案:

答案 0 :(得分:25)

正如这里所指出的,平铺是一种技术,用于在您处理缓存时将工作集保留在缓存中,以便享受内存延迟。如果您在从未点击缓存后看到没有任何好处的流式传输数据,那么平铺将没有用处。

你的示例循环以相同的顺序执行完全相同的工作,除了添加另一个分支并使分支模式稍微复杂一点(大多数预测器能够应对它,它只是没有任何帮助)。 / p>

考虑以下示例 -

#include "stdlib.h"
#include "stdio.h"
#include <time.h>

#define MAX (1024*1024*32)
#define REP 100
#define B  (16*1024)
int main() { 
    int i,j,r;
    char array[MAX];

    for (i = 0; i < MAX; i++) {  // warmup to make things equal if array happens to fit in your L3
       array[i] = 0;
    }

    clock_t t1 = clock();

    // Tiled loop
    for (i = 0; i < MAX; i += B) {
        for (r = 0; r < REP; r++) {
             for (j = i; j < (i + B); j+=64) {
                 array[j] = r;
             }
        }
    }
    clock_t t2 = clock();

    // un-tiled loop
    for (r = 0; r < REP; r++) {
        for (i = 0; i < MAX; i+=64) {
             array[i] = r;
        }
    }

    clock_t t3 = clock();
    printf ("Tiled: %f sec\n", (double)(t2 - t1) / CLOCKS_PER_SEC);
    printf ("Untiled: %f sec\n", (double)(t3 - t2) / CLOCKS_PER_SEC);
    printf ("array[0] = %d\n", array[0]);    // to prevent optimizing out all the writes
}

两个循环都在进行相同的访问(64byte跳转是通过使用每个缓存行一次来强调缓存,并防止IPC和指令调度成为瓶颈)。

平铺版本以块的形式重新排序这些访问,以便重复单个块可以重复访问缓存。由于块大小设置为16k,因此它可能适合大多数L1缓存并获得非常好的延迟。对于外循环的每次迭代,您将有1次迭代,其中您错过了所有缓存并转到内存(如果L3大于32M,只需拨打MAX甚至更高以确保),{{1}从L1飞过的迭代。

Untiled版本总共会重复REP-1次,但每次重复都会从缓存中剔除所有数据,使所有访问都进入内存,累积到更高的整体延迟。

使用gcc 4.8.1(-O3)进行编译,使我获得Xeon 5670 @ 2.9GHz -

REP

超过4倍:)

请注意,untiled版本仍然有一个好处 - 只有一个有序的流,因此HW预取器可以有效地为您提取数据,有点减轻内存延迟效应。但是,如果添加以下内容,则可以帮助CPU在库存版本中执行类似的操作:

Tiled: 0.110000 sec
Untiled: 0.450000 sec
array[0] = 99

告诉CPU在完成当前块之前略微引入下一个块。对于条件分支的价格(每个块有一些错误预测),您减少了下一个块上第一次迭代的执行时间 - 我从那里进一步减少到:

for (i = 0; i < MAX; i += B) {
    for (r = 0; r < REP; r++) {
         for (j = i; j < (i + B); j+=64) {
             array[j] = r;
             if (r == REP - 2)                         // SW prefetching
                 __builtin_prefetch(&array[j+B], 0);    
         }
    }
}