《代码优化:有效使用内存》读书笔记(三)——优化技巧之数据并行处理

来源:互联网 发布:族谱家谱制作软件 编辑:程序博客网 时间:2024/05/16 14:55
从上一篇可以看出,处理无关数据比相关数据快得多。但是到底能有多快呢?在大多数情况下,并发读取两个相邻单元的企图会向内存子系统发出一个请求(而不是想象中的两个)。字节并不是与存储器进行数据交换的基本单位,整个数据包才是基本的交换单位。根据处理器类型不同,这种包的长度取值从32字节到128字节不等。
可见,线性读取无关数据并不能确保对该内容进行并行处理。
我们举一个简单的例子,
有以下代码:

for (a = 0; a < BLOCK_SIZE; a+=32)

{

x+=*(int *) ((int)p1+a+0);
y+=*(int *) ((int)p1+a+4);
x+=*(int *) ((int)p1+a+8);
y+=*(int *) ((int)p1+a+12);
x+=*(int *) ((int)p1+a+16);
y+=*(int *) ((int)p1+a+20);
x+=*(int *) ((int)p1+a+24);
y+=*(int *) ((int)p1+a+28);
}

不妨假设处理器需要获取后面这个单元的内容:*(int *)((int)p1+a)。处理器先将查询公式化,然后发送给芯片组。同时,它开始处理下一条指令:x+=*(int *)((int)p1+a+4)。CPU发现数据没有相关性,这很好。然而,这个单元已经在前一个请求块中,所以CPU没有必要再发送一个请求(芯片组的操作将不会加快),而是等待处理完前一条指令。这样,处理器只能在芯片组返回内存查询之后才能累加数据。
终于,处理器会遇到一条指令,该指令是访问紧接在上一个请求块末尾的那个单元,也就是CPU应该为这个单元发送一条请求。遗憾的是,如果处理器更早遇到这个单元,它会向芯片组发送两个并行请求。
有一种更为高效的方法是:在第一趟处理期间,内存是按每次递增32个字节(如果程序是专门为Athlon或者P4优化的,则递增64或者128个字节)进行读取的。这使得处理器在本次循环内每次访问内存时,都会向芯片组发送一个请求。于是,总线上总是存在几个重叠的查询与相应,并且它们几乎是并行地进行的。在第二趟循环中,处理器读取所有余下单元的地址。当第一趟完成时,它们都已经处在高速缓存中了,因此,存取这些单元将不会引起重大的延迟。如下图


下面看看根据以上思想,将之前的代码改进之后的版本:

#define BLOCK_SIZE (32*M)          // 处理(内存)块的大小
#define STEP_SIZE L1_CACHE_SIZE    // 处理块的步长

for (b=0; b < BLOCK_SIZE; b += STEP_SIZE)
{
for (a = b; a < (b + STEP_SIZE) ; a += 128)
// 第一趟循环,其间完成并行数据加载
{
    x += *(int *)((int)p + a + 0);
  // 加载的第一个单元
    // 由于不在高速缓存中,CPU向芯片组发出读取请求
    x += *(int *)((int)p + a + 32);
  // 加载的下一个单元
// 由于不存在数据相关性,处理器可以不等待前一条指令的结果
// 就执行这条指令。然而,处理器发现这个单元会连同它刚请求
// 的块一同返回。因此,它不等待前一条指令执行完就向芯片组发出另外一个请求

  x += *(int *)((int)p + a + 64);
    // 现在总线上有三个查询

    x += *(int *)((int)p + a + 96);

    // 第四个查询发向总线,此时第一个查询可能还没有完成。

}   

for (a = b; a < (b + STEP_SIZE); a += 32)

    {
    // 没有必要读取另一个单元,因为它是在第一个循环中读取的
      // x += *(int *)((int)p + a + 0);

      x += *(int *)((int)p + a + 4);
      x += *(int *)((int)p + a + 8);
      x += *(int *)((int)p + a + 12);
      x += *(int *)((int)p + a + 16);
      x += *(int *)((int)p + a + 20);
      x += *(int *)((int)p + a + 24);
      x += *(int *)((int)p + a + 28);
      // 这些单元将出现在高速缓存中。因此,它们可以十分快速地进行加载。
    }
}
笔者经过测试,用该种方法读取数据,比线性读取算法快31%,芯片组等待时间几乎被抵消。这种方法原理简单,思路清晰,而且效果非常明显,非常值得大家一学、一试。
原创粉丝点击