用于流式传输的高效内存带宽

时间:2009-04-02 11:17:40

标签: optimization streaming cpu-cache memory-bandwidth

我有一个应用程序,它通过250 MB的数据流,将简单快速的神经网络阈值函数应用于数据块(每个只有2个32位字)。基于(非常简单的)计算的结果,块被不可预测地推入64个箱中的一个。所以它是一个大流和64个较短(可变长度)的流。

使用不同的检测功能重复多次。

计算是内存带宽有限。我可以这样说,因为即使我使用的计算密集程度更高的判别函数也没有速度变化。

构建新流写入以优化内存带宽的最佳方法是什么?我特别认为理解缓存使用和缓存行大小可能会在此中发挥重要作用。想象一下最糟糕的情况,我有64个输出流,运气不好,许多映射到同一个缓存行。然后,当我将下一个64位数据写入流时,CPU必须将过时的高速缓存行清除到主存储器,然后加载到正确的高速缓存行中。每个都使用64 BYTES的带宽...所以我带宽有限的应用程序可能会浪费95%的内存带宽(尽管在这个假设的最坏情况下)。

甚至很难测量效果,因此围绕它设计方法更加模糊。或者我甚至追逐一个幽灵瓶颈,不知怎么硬件优化得比我更好?

如果有任何不同,我正在使用Core II x86处理器。

编辑:这是一些示例代码。它通过一个数组流,并将其元素复制到伪随机选择的各种输出数组。使用不同数量的目标bin运行相同的程序会产生不同的运行时,即使完成了相同数量的计算和内存读写操作:

2输出流:13秒
8输出流:13秒
32输出流:19秒
128输出流:29秒
512输出流:47秒

使用512与2输出流之间的差异是高速缓存行驱逐开销引起的4倍(可能是??)。

#include <stdio.h>
#include <stdlib.h>
#include <ctime>

int main()
{
  const int size=1<<19;
  int streambits=3;
  int streamcount=1UL<<streambits; // # of output bins
  int *instore=(int *)malloc(size*sizeof(int));
  int **outstore=(int **)malloc(streamcount*sizeof(int *));
  int **out=(int **)malloc(streamcount*sizeof(int));
  unsigned int seed=0;

  for (int j=0; j<size; j++) instore[j]=j;

  for (int i=0; i< streamcount; ++i) 
    outstore[i]=(int *)malloc(size*sizeof(int));

  int startTime=time(NULL);
  for (int k=0; k<10000; k++) {
    for (int i=0; i<streamcount; i++) out[i]=outstore[i];
    int *in=instore;

    for (int j=0; j<size/2; j++) {
      seed=seed*0x1234567+0x7162521;
      int bin=seed>>(32-streambits); // pseudorandom destination bin
      *(out[bin]++)=*(in++);
      *(out[bin]++)=*(in++);
    }

  }
  int endTime=time(NULL);
  printf("Eval time=%ld\n", endTime-startTime);
}

5 个答案:

答案 0 :(得分:4)

在写入64个输出接收器时,您将使用许多不同的存储器位置。如果垃圾箱基本上随机填充,则意味着您有时会有两个垃圾箱共享同一个缓存行。不是一个大问题; Core 2 L1缓存是8路关联的。这意味着只有第9个缓存行才会出现问题。任何时候只有65个实时内存引用(1个读取/ 64个写入),8向关联是可以的。

L2缓存显然是12路关联的(总共3 / 6MB,因此12不是那么奇怪的数字)。因此,即使你在L1中发生了碰撞,很可能你仍然没有击中主存。

但是,如果你不喜欢这样,请在内存中重新安排垃圾箱。不是按顺序对每个bin进行遍历,而是将它们交错。对于bin 0,在偏移0-63处存储块0-15,但是将块16-31存储在偏移8192-8255处。对于bin 1,将块0-15存储在偏移64-127等处。这只需要几位移位和掩码,但结果是一对箱共享8个高速缓存行。

在这种情况下加速代码的另一种可能方法是SSE4,尤其是在x64模式下。您将获得16个寄存器x 128位,您可以优化读取(MOVNTDQA)以限制缓存污染。我不确定这对读取速度是否会有很大帮助 - 我希望Core2预取器能够抓住它。读取顺序整数是最简单的访问方式,任何预取器都应该优化它。

答案 1 :(得分:3)

您是否可以选择将输出流编写为具有内联元数据的单个流,以识别每个“块”?如果您要读取一个“块”,请在其上运行阈值函数,然后将其写入特定的输出流而不是将其写入所属的流(1个字节),然后是原始数据,您将认真对待减少你的颠簸。

我不建议这样做,除非您说过必须多次处理这些数据。在每次连续运行时,您都会读取输入流以获取bin编号(1个字节),然后在接下来的8个字节中为该bin执行任何操作。

就这种机制的缓存行为而言,由于你只是滑过两个数据流,除了第一种情况之外,在你正在阅读时写入尽可能多的数据,硬件将为你提供所有帮助可能希望就预取,缓存行优化等而言。

如果每次处理数据时都必须添加额外的字节,那么最糟糕的缓存行为就是平均情况。如果你能负担得起存储空间,那对我来说似乎是一个胜利。

答案 2 :(得分:2)

如果你真的感到绝望,这里有一些想法......

您可以考虑升级硬件。对于有点类似于你的流媒体应用程序,我发现通过改用i7处理器我获得了很大的速度提升。另外,对于内存限制的工作来说,AMD处理器应该比Core 2更好(虽然我最近没有使用它们)。

您可能考虑的另一个解决方案是使用CUDA等语言在显卡上进行处理。显卡被调整为具有非常高的存储器带宽并且可以进行快速浮点数学运算。与直接的非优化C实现相比,期望将CUDA代码的开发时间花费5倍到20倍。

答案 3 :(得分:1)

您可能希望探索将文件映射到内存中。这样内核就可以为您处理内存管理。内核通常最了解如何处理页面缓存。 如果您的应用程序需要在多个平台上运行,尤其如此,因为不同的Oses以不同的方式处理内存管理。

有ACE(http://www.cs.wustl.edu/~schmidt/ACE.html)或Boost(http://www.boost.org等框架,允许您编写以独立于平台的方式执行内存映射的代码。

答案 4 :(得分:1)

这种情况的真正答案是编写几种方法并为它们计时。你明显做了什么。像我这样的所有人都可以建议尝试其他方法。

例如:即使没有缓存抖动(您的输出流映射到相同的缓存行),如果您正在编写size int,size = 1&lt;&lt; 19&sizeof(int)= 4,32-比特 - 即如果你正在写8MB的数据,你实际上是在读8MB,然后写8MB。因为如果您的数据位于x86处理器上的普通WB(WriteBack)内存中,要写入一行,您首先必须读取该行的旧副本 - 即使您要将读取的数据丢弃。

您可以通过以下方式消除这种不必要的RFO读取流量(a)使用WC内存(可能很难设置)或(b)使用SSE流媒体商店,即NT(非时间)商店。 MOVNT * - MOVNTQ,MOVNTPS等(还有一个MOVNTDQA流媒体负载,虽然使用起来比较痛苦。)

我更喜欢通过Google搜索http://blogs.fau.de/hager/2008/09/04/a-case-for-the-non-temporal-store/

找到的这篇论文

现在:MOVNT *适用于WB内存但与WC内存一样,使用少量写入cmbining缓冲区。实际数量因处理器型号而异:第一块英特尔芯片上只有4个,P6(又名Pentium Pro)。 Ooof ... Bulldozer的4K WCC(​​写入组合缓存)基本上提供64个写入组合缓冲区,每http://semiaccurate.com/forums/showthread.php?t=6145&page=40,尽管只有4个经典WC缓冲区。但是http://www.intel.com/content/dam/doc/manual/64-ia-32-architectures-optimization-manual.pdf说有些处理器有6个WC缓冲区,有些8个。无论如何......有一些,但不是很多。通常不是64。

但是你可以尝试一下:实现自己组合写。

a)写入一组64个(#streams)缓冲区,每个缓冲区大小为64B(缓存行大小),或者可能是128或256B。让这些缓冲区在普通的WB存储器中。您可以使用普通商店访问它们,但如果您可以使用MOVNT *,那就很好。

当其中一个缓冲区变满时,将其作为一个突发复制到内存中真正应该流的位置。使用MOVNT *流媒体商店。

这将最终做到 * N个字节存储到临时缓冲区,命中L1缓存 *读取64 * 64字节以填充临时缓冲区 * N个字节从临时缓冲区读取,命中L1缓存。 *通过流媒体商店写入的N个字节 - 基本上直接进入内存。

即N字节缓存命中读取+ N字节缓存命中写入+ N字节缓存未命中

与N字节高速缓存未命中读取+ N字节高速缓存写入读取。

减少N个字节的高速缓存未命中读取可能比弥补额外的开销要小。