受最近在SO上提出的问题和给出的答案的启发,这让我感到非常无知,我决定花些时间学习更多关于CPU缓存的知识,并编写了一个小程序来验证我是否全都理解正确(很可能不是,我很担心)。首先,我会记录下我期望的假设,如果这些假设是错误的,您可以在此处阻止我。根据我所读的,一般而言:
An
每个n路组相联缓存被划分为s个集合,每个集合包含n条线路,每条线路具有固定的大小L;
Each main memory address
每个主内存地址A可以映射到一个集合中任意n条缓存线路之一;
The set into which address
可以通过将地址空间分割成每个缓存线路大小的槽,然后计算A所在槽的索引(I=A/L),最后执行模运算将索引映射到目标集合T(T=I%s)来找到A所映射的集合;
A cache read miss causes a higher delay than a cache write miss, because the CPU is less likely to stall and stay idle while waiting for the main memory line to be fetched.
与缓存写失效相比,缓存读失效会导致更高的延迟,因为CPU在等待主内存线路被获取时不太可能停顿和空闲。
我的第一个问题是:这些假设正确吗?
假设它们是,我尝试对这些概念进行一些实际操作,以便我能够看到它们对程序产生具体影响。我编写了一个简单的测试,分配了一个大小为B字节的内存缓冲区,并重复访问该缓冲区的位置,步长为给定的step,从缓冲区的开头开始(这意味着如果B为14且步长为3,则我只会重复访问0、3、6、9和12位置——如果B为13、14或15,则情况相同)。
由于以上假设,我的期望是:
简而言之,结果如下:
1. 确认了期望1和2; 2. 未能确认期望3; 3. 在测试中发现读写版本的性能损失比只写版本低。
对于第三点,测试结果表明,当B为256MB且STEP等于关键步长时,只写版本的循环平均性能损失约为6倍(6.234秒 vs 1.078秒),而读写版本的循环平均性能损失约为1.3倍(6.671秒 vs 5.25秒)。因此,作者想知道为什么读写版本的性能损失比只写版本低。
为了完整起见,以下是我编写用于进行测试的程序,其中常量反映了我的机器的硬件参数:L1 8路关联数据缓存的大小为32 KB,每个缓存行的大小为64字节,这样总共有64组(CPU还有一个大小和具有相同线路大小的相同大小的单独的L1 8路指令高速缓存)。
An
n
-way associative cache is divided into s
sets, each containing n
lines, each line having a fixed size L
;每个n路组相联缓存被划分为s个集合,每个集合包含n条线路,每条线路具有固定的大小L;
Each main memory address
A
can be mapped into any of the n
cache lines of one set;每个主内存地址A可以映射到一个集合中任意n条缓存线路之一;
The set into which address
A
is mapped can be found by splitting the address space into slots each of the size of one cache line, then computing the index of A
's slot (I = A / L
), and finally performing a modulo operation to map the index into the target set T
(T = I % s
);可以通过将地址空间分割成每个缓存线路大小的槽,然后计算A所在槽的索引(I=A/L),最后执行模运算将索引映射到目标集合T(T=I%s)来找到A所映射的集合;
A cache read miss causes a higher delay than a cache write miss, because the CPU is less likely to stall and stay idle while waiting for the main memory line to be fetched.
与缓存写失效相比,缓存读失效会导致更高的延迟,因为CPU在等待主内存线路被获取时不太可能停顿和空闲。
我的第一个问题是:这些假设正确吗?
假设它们是,我尝试对这些概念进行一些实际操作,以便我能够看到它们对程序产生具体影响。我编写了一个简单的测试,分配了一个大小为B字节的内存缓冲区,并重复访问该缓冲区的位置,步长为给定的step,从缓冲区的开头开始(这意味着如果B为14且步长为3,则我只会重复访问0、3、6、9和12位置——如果B为13、14或15,则情况相同)。
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的倍数); - 读取和写入每个缓冲区位置时的性能损失应该比仅写入这些位置时更严重:写入内存位置不应需要等待相应的行被获取,因此访问被映射到同一集合的内存位置(再次使用关键步长作为
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. 确认了期望1和2; 2. 未能确认期望3; 3. 在测试中发现读写版本的性能损失比只写版本低。
对于第三点,测试结果表明,当B为256MB且STEP等于关键步长时,只写版本的循环平均性能损失约为6倍(6.234秒 vs 1.078秒),而读写版本的循环平均性能损失约为1.3倍(6.671秒 vs 5.25秒)。因此,作者想知道为什么读写版本的性能损失比只写版本低。
为了完整起见,以下是我编写用于进行测试的程序,其中常量反映了我的机器的硬件参数:L1 8路关联数据缓存的大小为32 KB,每个缓存行的大小为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;
}
如果您能够阅读完这个长问题,我提前感谢您。