5

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

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

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

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

甚至很难衡量效果,因此围绕它设计的方法更加模糊。还是我什至在追逐一个幽灵瓶颈,以某种方式硬件优化得比我好?

如果这有什么不同,我正在使用 Core II x86 处理器。

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

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);
}
4

5 回答 5

4

当您写入 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 预取器能够捕捉到这一点。读取顺序整数是最简单的访问方式,任何预取器都应该优化它。

于 2009-04-02T14:52:10.040 回答
3

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

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

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

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

于 2009-04-06T21:01:09.770 回答
2

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

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

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

于 2009-04-06T20:42:27.460 回答
1

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

有像 ACE ( http://www.cs.wustl.edu/~schmidt/ACE.html ) 或 Boost ( http://www.boost.org ) 这样的框架,它们允许您编写在平台无关的方式。

于 2009-04-06T20:25:32.040 回答
1

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

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

您可以通过 (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 内存一样工作,使用少量的写入组合缓冲区。实际数量因处理器型号而异:第一个 Intel 芯片 P6(又名 Pentium Pro)上只有 4 个。Ooof... Bulldozer 的 4K WCC(​​写入组合缓存)基本上提供了 64 个写入组合缓冲区,根据http://semiaAccurate.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 字节的缓存未命中读取可能比弥补额外开销更重要。

于 2012-11-06T06:10:09.573 回答