受最近关于 SO 的问题和给出的答案的启发,这让我感到非常无知,我决定花一些时间来了解更多关于CPU 缓存的知识,并编写了一个小程序来验证我是否正确地完成了这件事(大多数恐怕不会)。我将首先写下构成我期望的假设,因此如果这些假设是错误的,您可能会在这里阻止我。根据我所读到的,一般来说:
- 一个
n
-way关联缓存被分成s
集合,每一个包含n
行,每行有一个固定的大小L
; - 每个主存地址
A
都可以映射到一组缓存线中的任何一个;n
- 地址映射到的集合
A
可以通过将地址空间划分为每个缓存行大小的槽,然后计算A
's slot (I = A / L
) 的索引,最后执行模运算将索引映射到目标设置T
(T = I % s
); - 高速缓存读取未命中导致比高速缓存写入未命中更高的延迟,因为 CPU 在等待获取主内存行时不太可能停止并保持空闲。
我的第一个问题是:这些假设是否正确?
假设它们是,我尝试对这些概念进行一些尝试,以便我可以真正看到它们对程序产生具体影响。我写了一个简单的测试,它分配一个B
字节的内存缓冲区,并从缓冲区的开头以给定步长的固定增量重复访问该缓冲区的位置(这意味着如果是 14 并且步长是 3,我只重复访问位置 0 , 3, 6, 9, 和 12 - 如果是 13, 14, 或 15, 也是如此): B
B
int index = 0;
for (int i = 0; i < REPS; i++)
{
index += STEP;
if (index >= B) { index = 0; }
buffer[index] = ...; // Do something here!
}
由于上述假设,我的期望是:
- 当设置
STEP
等于临界步长(即高速缓存行的大小乘以高速缓存中的集合数,或)时L * s
,性能应该比STEP
设置为(例如L * s) + 1
,因为我们将只访问内存映射到同一集合中的位置,迫使缓存行更频繁地从该集合中逐出并导致更高的缓存未命中率; - 当
STEP
等于临界步长时,性能不应该受缓冲区大小B
的影响,只要它不是太小(否则会访问的位置太少,缓存未命中率会更少);否则,性能应该受 影响,B
因为使用更大的缓冲区,我们更有可能访问映射到不同集合的位置(尤其是如果STEP
不是 2 的倍数); - 读取和写入每个缓冲区位置时的性能损失应该比只写入这些位置时更糟:写入内存位置不应该需要等待相应的行被获取,因此访问映射到的内存位置的事实相同的集合(同样,通过使用临界步幅 as
STEP
)应该有轻微的影响。
所以我使用RightMark Memory Analyzer找出我的 L1 CPU 数据缓存的参数,调整我的程序的大小,并尝试了一下。这就是我编写主循环的方式(onlyWriteToCache
是一个可以从命令行设置的标志):
...
for (int i = 0; i < REPS; i++)
{
...
if (onlyWriteToCache)
{
buffer[index] = (char)(index % 255);
}
else
{
buffer[index] = (char)(buffer[index] % 255);
}
}
结果简而言之:
- 预期 1) 和 2) 得到证实;
- 预期 3)未得到证实。
这个事实让我印象深刻,让我觉得有些事情我做得不太对。当B
是 256 MB 并且STEP
等于临界步长时,测试(在 GCC 4.7.1 上使用 -O3 编译)表明:
- 该周期的只写版本遭受平均约 6 倍的性能损失(6.234 秒对 1.078 秒);
- 周期的读写版本平均遭受约 1.3 倍的性能损失(6.671 秒对 5.25 秒)。
所以我的第二个问题是:为什么会有这种差异?我预计读写时的性能损失会比只写时更高。
为了完整起见,下面是我编写的用于测试的程序,其中的常量反映了我机器的硬件参数:L1 8 路关联数据缓存的大小为 32 KB L
,每个缓存行的大小为64 字节,总共 64 组(CPU 有一个单独的 L1 8 路指令缓存,大小相同,行大小相同)。
#include <iostream>
#include <ctime>
#include <cstdlib>
#include <iterator>
#include <algorithm>
using namespace std;
// Auxiliary functions
constexpr int pow(int base, int exp)
{
return ((exp == 0) ? 1 : base * pow(base, exp - 1));
}
int main(int argc, char* argv[])
{
//======================================================================
// Define behavior from command-line arguments
//======================================================================
bool useCriticalStep = false;
bool onlyWriteToCache = true;
size_t BUFFER_SIZE = pow(2, 28);
size_t REPS = pow(2, 27);
if (argc > 0)
{
for (int i = 1; i < argc; i++)
{
string option = argv[i];
if (option == "-c")
{
useCriticalStep = true;
}
else if (option == "-r")
{
onlyWriteToCache = false;
}
else if (option[1] == 's')
{
string encodedSizeInMB = option.substr(2);
size_t sizeInMB = atoi(encodedSizeInMB.c_str());
BUFFER_SIZE = sizeInMB * pow(2, 20);
}
else if (option[1] == 'f')
{
string encodedNumOfReps = option.substr(2);
size_t millionsOfReps = atoi(encodedNumOfReps.c_str());
REPS = millionsOfReps * pow(10, 6);
}
}
}
//======================================================================
// Machine parameters
//======================================================================
constexpr int CACHE_SIZE = pow(2, 15);
constexpr int CACHE_LINE_SIZE = 64;
constexpr int CACHE_LINES_PER_SET = 8;
constexpr int SET_SIZE = CACHE_LINE_SIZE * CACHE_LINES_PER_SET;
constexpr int NUM_OF_SETS = CACHE_SIZE / SET_SIZE;
//======================================================================
// Print out the machine parameters
//======================================================================
cout << "CACHE SIZE: " << CACHE_SIZE / 1024 << " KB" << endl;
cout << "CACHE LINE SIZE: " << CACHE_LINE_SIZE << " bytes" << endl;
cout << "CACHE LINES PER SET: " << CACHE_LINES_PER_SET << endl;
cout << "SET SIZE: " << SET_SIZE << " bytes" << endl;
cout << "NUMBER OF SETS: " << NUM_OF_SETS << endl;
fill_n(ostream_iterator<char>(cout), 30, '='); cout << endl;
//======================================================================
// Test parameters
//======================================================================
const int STEP = NUM_OF_SETS * CACHE_LINE_SIZE + (useCriticalStep ? 0 : 1);
//======================================================================
// Print out the machine parameters
//======================================================================
cout << "BUFFER SIZE: " << BUFFER_SIZE / pow(2, 20) << " MB" << endl;
cout << "STEP SIZE: " << STEP << " bytes" << endl;
cout << "NUMBER OF REPS: " << REPS << endl;
fill_n(ostream_iterator<char>(cout), 30, '='); cout << endl;
//======================================================================
// Start the test
//======================================================================
char* buffer = new char[BUFFER_SIZE];
clock_t t1 = clock();
int index = 0;
for (size_t i = 0; i < REPS; i++)
{
index += STEP;
if (index >= BUFFER_SIZE)
{
index = 0;
}
if (onlyWriteToCache)
{
buffer[index] = (char)(index % 255);
}
else
{
buffer[index] = (char)(buffer[index] % 255);
}
}
clock_t t2 = clock();
//======================================================================
// Print the execution time (in clock ticks) and cleanup resources
//======================================================================
float executionTime = (float)(t2 - t1) / CLOCKS_PER_SEC;
cout << "EXECUTION TIME: " << executionTime << "s" << endl;
delete[] buffer;
}
如果您设法阅读了这个长长的问题,请提前感谢您。