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

Posted

技术标签:

【中文标题】循环平铺。如何选择块大小?【英文标题】:loop tiling. how to choose block size? 【发布时间】:2013-12-20 11:02:42 【问题描述】:

我正在尝试学习循环优化。我发现循环平铺有助于使数组循环更快。我尝试使用下面给出的两个代码块,有和没有循环阻塞,并测量两者所花费的时间。大多数时候我没有发现显着差异。我测试了不同的块大小,但我不确定如何选择块大小。如果我的方向错误,请帮助我。事实上,我发现没有块的循环工作得更快很多次。

一个。带屏蔽

int max = 1000000;
int B = 100;
for (i = 0; i < max; i += B)

     for (j = i; j < (i + B); j++)
     
         array[j] = 0;
     
 

b.不阻塞

for (i = 0; i < max; i++)

    array[i] = 0;
 

花费时间: 有阻塞:经过时间 - 6997000 纳秒

没有阻塞经过的时间 - 6097000 纳秒

【问题讨论】:

我不清楚你的问题。为什么您认为这种屏蔽很重要? 我发现这使得循环高效en.wikipedia.org/wiki/Loop_blocking 循环阻塞仅在您对同一数据进行多次传递时才有用。但是您的示例是对数据进行单次传递。所以循环阻塞在这个例子中没有帮助。单循环已经快到可以达到的速度了。 (无论如何编译器可能会将其变成memset()...) 感谢神秘主义者。在我的应用程序中,我有多维数组。我不清楚同一数据的多次传递。请简述。 【参考方案1】:

正如这里所指出的,平铺是一种技术,旨在在您处理工作集时将其保留在缓存中,以便享受内存延迟。如果您流式传输您的数据一次,您将看不到任何好处,因为您从未访问过缓存,因此平铺将没有用处。

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

考虑以下示例 -

#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

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

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

Untiled 版本也会总共重复自己REP 次,但每次重复都会破坏缓存中的所有数据,使所有访问都进入内存,累积到更高的总体延迟。

使用 gcc 4.8.1 (-O3) 编译让我在 Xeon 5670 @ 2.9GHz -

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

超过 4 倍 :)

请注意,untiled 版本仍然有一个好处 - 有一个单一的有序流,因此硬件预取器可以提前运行以有效地为您提取数据,从而在一定程度上减轻内存延迟效应。但是,如果您添加以下内容,您可以帮助 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);    
         
    

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

Tiled: 0.070000 sec

【讨论】:

以上是关于循环平铺。如何选择块大小?的主要内容,如果未能解决你的问题,请参考以下文章

如何设置平铺drawable的大小

如何以编程方式水平平铺某些窗口?

CUDA:关于活动扭曲(活动块)以及如何选择块大小的问题

如何才可以在PDF文件添加文字水印?

如何在 MATLAB 中以(恒定)任意步幅将图像平铺成(恒定)任意大小的补丁?

求助:获取嵌套块选择集lisp