【问题标题】:Efficient memory bandwidth use for streaming流式传输的有效内存带宽使用
【发布时间】:2010-10-17 02:17:59
【问题描述】:

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

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

计算受内存带宽限制。我可以这么说,因为即使我使用计算量更大的判别函数,速度也没有变化。

构建新流的写入以优化我的内存带宽的最佳方法是什么?我特别认为了解缓存使用和缓存行大小可能在其中发挥重要作用。想象一下最坏的情况,我有 64 个输出流,运气不好,许多都映射到同一个缓存行。然后,当我将接下来的 64 位数据写入流时,CPU 必须将过时的缓存行刷新到主内存,并加载到正确的缓存行中。其中每一个都使用 64 字节的带宽......所以我的带宽受限应用程序可能会浪费 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);
}

【问题讨论】:

  • errr.. 如果有代码?
  • 如所写,该代码无法编译(缺少分号,我已添加),但我怀疑任何为发布而编辑的示例。

标签: optimization streaming cpu-cache memory-bandwidth


【解决方案1】:

这种情况的真正答案是编写几种方法并对其进行计时。你显然已经做到了。像我这样的人所能做的就是建议尝试其他方法。

例如:即使没有缓存抖动(您的输出流映射到相同的缓存行),如果您正在写入 size int,size = 1

您可以通过 (a) 使用 WC 内存(可能很难设置)或 (b) 使用 SSE 流式存储,即 NT(非临时)存储来消除这种不必要的 RFO 读取流量。 MOVNT* - MOVNTQ、MOVNTPS 等(还有一个 MOVNTDQA 流加载,虽然使用起来更痛苦。)

我更喜欢我刚刚通过谷歌搜索http://blogs.fau.de/hager/2008/09/04/a-case-for-the-non-temporal-store/找到的这篇论文

现在:MOVNT* 适用于 WB 内存,但像 WC 内存一样工作,使用少量的写入 cmbining 缓冲区。实际数量因处理器型号而异:第一个 Intel 芯片 P6(又名 Pentium Pro)上只有 4 个。 Ooof... Bulldozer 的 4K WCC (Write Combining Cache) 基本上提供了 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。

但您可以尝试以下方法:实现 write 组合自己。

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

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

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

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

相对于 N 字节缓存未命中读取 + N 字节缓存写入读取。

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

【讨论】:

    【解决方案2】:

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

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

    【讨论】:

      【解决方案3】:

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

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

      就这种机制的缓存行为而言,由于您只在两个数据流中滑动,并且除第一种情况外,写入与读取一样多的数据,硬件将为您提供所有帮助可能希望进行预取、缓存行优化等。

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

      【讨论】:

        【解决方案4】:

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

        您可以考虑升级硬件。对于与您的流应用程序有些相似的流应用程序,我发现通过更改为 i7 处理器可以大大提高速度。此外,在内存密集型工作方面,AMD 处理器据称比 Core 2 更好(尽管我自己最近没有使用它们)。

        您可能考虑的另一种解决方案是使用 CUDA 等语言在显卡上进行处理。显卡经过调整以具有非常高的内存带宽并进行快速浮点数学运算。相对于直接的非优化 C 实现,预计 CUDA 代码的开发时间是 5 到 20 倍。

        【讨论】:

          【解决方案5】:

          当您写入 64 个输出箱时,您将使用许多不同的内存位置。如果这些 bin 基本上是随机填充的,这意味着您有时会有两个 bin 可以共享相同的缓存行。问题不大; Core 2 L1 高速缓存是 8 路关联的。这意味着您只会遇到第 9 个缓存行的问题。任何时候只有 65 次实时内存引用(1 次读取/64 次写入),8 路关联就可以了。

          L2 缓存显然是 12 路关联的(总共 3/6MB,所以 12 并不是一个奇怪的数字)。因此,即使您在 L1 中发生冲突,很有可能您仍然没有到达主内存。

          但是,如果您不喜欢这样,请重新排列内存中的 bin。与其按顺序排列每个 bin,不如将它们交错。对于 bin 0,将块 0-15 存储在偏移量 0-63 处,但将块 16-31 存储在偏移量 8192-8255 处。对于 bin 1,将块 0-15 存储在偏移量 64-127 等处。这只需要一些位移和掩码,但结果是一对 bin 共享 8 个缓存行。

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

          【讨论】:

          • 所以这是试图保持每个输出队列总是映射到同一个缓存箱。然后,每个缓存箱始终具有相同数量的流,从而最大限度地减少驱逐。随机地址可以轻松地将 9+ 个流映射到同一个 bin 并导致驱逐。复杂且依赖于 CPU,但合乎逻辑!谢谢。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2017-06-07
          • 1970-01-01
          • 1970-01-01
          • 2010-11-23
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多