为什么缓存读取未命中比写入未命中更快?

时间:2015-03-12 14:39:29

标签: c++ performance caching cpu-cache

我需要使用另一个数组(readArray)来计算数组(writeArray),但问题是数组之间的索引映射是不一样的(writeArray的索引x处的值必须使用readArray的索引y处的值计算)所以它不是非常缓存友好。

但是我可以选择循环浏览readArray还是顺序浏览writeArray。

所以这是一个简化的代码:

int *readArray = new int[ARRAY_SIZE];       // Array to read
int *writeArray = new int[ARRAY_SIZE];      // Array to write
int *refArray = new int[ARRAY_SIZE];        // Index mapping between read and write, could be also array of pointers instead indexes

// Code not showed here : Initialization of readArray with values, writeArray with zeroes and refArray with random indexes for mapping between readArray and writeArray (values of indexes between 0 and ARRAY_SIZE - 1)

// Version 1: Random read (browse writeArray/refArray sequentially)
for (int n = 0; n < ARRAY_SIZE; ++n) {
    writeArray[n] = readArray[refArray[n]];
}

// Version 2: Random write (browse readArray/refArray sequentially)
for (int n = 0; n < ARRAY_SIZE; ++n) {
    writeArray[refArray[n]] = readArray[n];
}

我认为缓存读取丢失比写入错误更慢(因为如果下一条指令依赖于读取数据,CPU需要在读取完成之前等待,但是为了写入它不需要等待处理下一条指令)通过分析,似乎版本1比版本2快(版本2比版本1慢大约50%)。

我也尝试了这个:

// Version 3: Same as version 2 but without polluting cache
for (int n = 0; n < ARRAY_SIZE; ++n) {
    _mm_stream_si32(&writeArray[refArray[n]], readArray[n]);
}

因为我不需要读取writeArray的值,所以没有理由用写入的值污染缓存,但是这个版本比其他版本慢得多(比版本1慢6700%)。

为什么写错过比读错慢? 为什么绕过缓存进行写入比使用它更慢,即使我们之后没有读取这些写入的数据?

1 个答案:

答案 0 :(得分:4)

让我们从最后一个版本开始 - 您所做的是使用流式存储来实现非顺序(非流)访问模式。您随机访问整数,这意味着您正在对完整缓存行进行部分写入(int size)。在正常写入时,这不重要,因为核心将线路拉入缓存,并且只需修改必要的块(稍后在需要存储其他东西时将其写回),但是因为你要求为了避免缓存,你实际上必须在内存中进行这种非常昂贵和阻塞的部分合并。 只有当您保证修改整行时(例如,通过顺序遍历数组),流式存储才有用。

至于第二个版本 - 你的假设是正确的,如果通过负载存在数据依赖性,你将不得不等待它们,但这里没有真正的依赖链。您只有一组具有2级依赖关系的加载,但它们之间没有相互依赖关系导致迭代之间的任何序列化(即迭代n == 2和n == 3可能在n == 1获得第一次加载之前开始)。 实际上,假设您的CPU可以支持N个未完成的访问(取决于所涉及的大小和缓存级别),您将并行启动对refArray的前N个引用(假设索引计算速度很快),然后是前N个引用readArray,然后是下一批等等。

现在,由于没有数据依赖性,因此它成为带宽问题。在这种情况下,一般来说,处理器由于它们的无序性而更容易负载 - 一旦知道地址(仅取决于快速索引计算),您可以并行启动它们并且乱序。另一方面,商店需要按程序顺序观察(以保持内存一致性),几乎将它们序列化(那里有一些可能的CPU技巧,取决于你的确切微架构,但它不会改变大图)。

编辑:版本2中添加的另一个约束(我认为更为关键)是内存消歧。处理器必须计算负载并存储地址,以便知道是否存在任何冲突(我们知道没有,但处理器没有...)。如果负载依赖于商店,则必须阻止它,以防必须转发新数据。 现在,由于负载是在早期的OOO机器上启动的,因此尽早知道所有商店的地址以避免碰撞(或者更糟糕的是 - 失败并导致大量冲洗的推测)变得至关重要