引言
在许多文章中指出了这些缓存的架构,速度差异等。纸上得来终觉浅,今天想实际写代码简单测试一下。
背景
现代计算机系统中,CPU缓存(L1、L2、L3)和主内存(RAM)之间的读取速度有着显著的差异。缓存的主要目的是提高数据访问的速度,从而提升整体的系统性能。本篇文章将通过一系列的测试来探索不同大小内存块的读写性能,从而揭示缓存和内存之间读取速度的变化规律。
测试思路
我们通过改变内存块的大小来观察缓存命中和未命中的情况。具体而言,我们会从小块内存(128字节)开始测试,逐渐增加到较大的内存块(10GB),以此来观察性能曲线的变化。
测试代码
以下是用于测试的C++代码:
#include <iostream>
#include <vector>
#include <chrono>
#include <cstdlib>
// 记录读写操作的时间
void testMemoryAccessSpeed(size_t blockSize) {
size_t totalBytes = 1024ull * 1024 * 1024 * 100; // 总共读写100GB的数据
size_t iterations = totalBytes / blockSize;
std::vector<size_t> buffer(blockSize / sizeof(size_t));
// 初始化计时器
auto start = std::chrono::high_resolution_clock::now();
// 执行读写操作
for (size_t i = 0; i < iterations; ++i) {
memset(buffer.data(), 551546, buffer.size() * sizeof(size_t));
}
// 记录结束时间
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
// 输出结果
if (blockSize > 1024 * 1024) {
std::cout << "Block Size (MB): " << blockSize / 1024 / 1024 << ", Time (ms): " << duration.count() << " Loops:"<< iterations << std::endl;
}
else if (blockSize > 1024) {
std::cout << "Block Size (KB): " << blockSize / 1024 << ", Time (ms): " << duration.count() << " Loops:" << iterations << std::endl;
}
else {
std::cout << "Block Size ( B): " << blockSize << ", Time (ms): " << duration.count() << " Loops:" << iterations << std::endl;
}
}
int main() {
// 测试不同大小的内存块
std::vector<size_t> blockSizeVec = {
128, 512, 1024, 2 * 1024, //128B ~ 2KB
4 * 1024, 64 * 1024, 512 * 1024, 4 * 1024 * 1024, // 4KB ~ 4 MB
16 * 1024 * 1024, 64 * 1024 * 1024, 256 * 1024 * 1024, 512 * 1024 * 1024, // 16M ~ 512M
1024 * 1024 * 1024, 2ull * 1024 * 1024 * 1024, 5ull * 1024 * 1024 * 1024, 10ull * 1024 * 1024 * 1024 // 1G ~ 10G
};
const size_t maxBlockSize = 128 * 1024 * 1024; // 最大到128MB
for (auto it : blockSizeVec) {
testMemoryAccessSpeed(it);
}
return 0;
}
测试的硬件平台为:
测试结果
以下是测试代码的输出:
结果粗略评估和解释
-
超小块内存(小于1KB):
- 当内存块较小(如128字节)时,需要执行的循环次数成倍增加,整体的耗时更多在循环指令和函数调用(memset)上,而非数据读取。
- 随着内存块大小的增加,循环次数减少,内存读写开始成为瓶颈,数据开始有意义。
-
小块内存(1KB至4KB):
- 当内存块较小时,读写操作主要发生在L1缓存中,因此时间较长。
- 随着内存块大小的增加,L1缓存的利用率下降,但L2缓存开始发挥作用,因此时间逐渐增加。
-
中等大小内存块(4KB至4MB):
- 在这个范围内,L2缓存成为主要存储介质,性能较高。
- 当内存块大小增加到一定程度,L2缓存开始饱和,性能下降。
-
大块内存(4MB至30MB):
- L3缓存成为主要存储介质,性能再次提高。
- 当内存块进一步增加到几十MB时,L3缓存也逐渐饱和,性能趋于稳定。
-
超大块内存(30MB及以上):
- 超过L3缓存的容量后,性能受内存读取速度的限制,因此时间较为稳定。
excel 作图分析
在上图中,红色参考线是根据CPU-Z给出的单核心最大的L1~L3的缓存大小参考。注意,横坐标的BlockSize的增长不是线性增长的,但纵坐标轴Time是线性的。
从上图可以根据Time来粗略评估L1~L3再到内存的性能,我们选取了这几个样本点作为各个缓存性能的代表:
我的测试结论
根据选取的样本点,即可得出不同存储类型的性能参考:
上面的L1数据不是特别严谨,因为在块较小时,在循环和函数调用上会有额外的耗时,因此L1的性能应该比测试数据更高。具体高多少,可以根据前面的超小块内存测试结果进行评估。
网上其他相关测试的结论
总结
综合网上的资料和我的实践结论,基本可以得出如下概念:
- L1的速度是L2的2~5倍,我的测试结果是3.8倍。
- L2的速度是L3的2~3倍,我的测试结果是1.8倍。
- L3的速度是内存的3~7倍,我的测试结果是2.4倍(DDR5@4000M)。
这里的性能测试结果仅仅是上面的代码的运行耗时结果,不代表在大部分场景下L3的性能比内存高3~7倍。CPU访问内存需要经过复杂的内存控制器,主板的内存总线,再到内存控制器等复杂路径,每次读写也要操作一系列内存寄存器,从程序的角度来看,读写内存的延时要远远大于L3。从上图AIDA的测试结果中也能看出,内存的延时是L3的十倍。
对于实际程序运行的情况,内存延时带来的耗时增加是更严重的,这也是现代CPU把L3缓存做到30MB这么大的重要原因。