我正在测试读取多个数据流如何影响 CPU 缓存性能。我正在使用以下代码对此进行基准测试。基准测试读取顺序存储在内存中的整数并顺序写回部分和。从中读取的顺序块的数量是变化的。以循环方式读取来自块的整数。
#include <iostream>
#include <vector>
#include <chrono>
using std::vector;
void test_with_split(int num_arrays) {
int num_values = 100000000;
// Fix up the number of values. The effect of this should be insignificant.
num_values -= (num_values % num_arrays);
int num_values_per_array = num_values / num_arrays;
// Initialize data to process
auto results = vector<int>(num_values);
auto arrays = vector<vector<int>>(num_arrays);
for (int i = 0; i < num_arrays; ++i) {
arrays.emplace_back(num_values_per_array);
}
for (int i = 0; i < num_values; ++i) {
arrays[i%num_arrays].emplace_back(i);
results.emplace_back(0);
}
// Try to clear the cache
const int size = 20*1024*1024; // Allocate 20M. Set much larger then L2
char *c = (char *)malloc(size);
for (int i = 0; i < 100; i++)
for (int j = 0; j < size; j++)
c[j] = i*j;
free(c);
auto start = std::chrono::high_resolution_clock::now();
// Do the processing
int sum = 0;
for (int i = 0; i < num_values; ++i) {
sum += arrays[i%num_arrays][i/num_arrays];
results[i] = sum;
}
std::cout << "Time with " << num_arrays << " arrays: " << std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() - start).count() << " ms\n";
}
int main() {
int num_arrays = 1;
while (true) {
test_with_split(num_arrays++);
}
}
以下是在 Intel Core 2 Quad CPU Q9550 @ 2.83GHz 上拆分 1-80 路的时间:
8 个流之后的速度提升对我来说是有意义的,因为处理器具有 8 路关联 L1 缓存。24 路关联 L2 缓存反过来解释了 24 个流的颠簸。如果我得到的效果与为什么一个循环比两个循环慢这么多?,其中多个大分配总是在同一个关联集中结束。为了比较,我在最后一个大块中完成分配的时间包括在内。
但是,我并不完全理解从一个流到两个流的颠簸。我自己的猜测是它与预取到 L1 缓存有关。阅读Intel 64 and IA-32 Architectures Optimization Reference Manual似乎 L2 流式预取器支持跟踪多达 32 个数据流,但没有为 L1 流式预取器提供此类信息。L1 预取器是否无法跟踪多个流,或者这里还有其他东西在起作用?
背景
我正在对此进行调查,因为我想了解将游戏引擎中的实体组织为数组结构样式中的组件如何影响性能。目前看来,转换所需的数据在两个组件中而不是在 8-10 个组件中对于现代 CPU 来说并不重要。但是,上面的测试表明,有时避免将某些数据拆分为多个组件可能是有意义的,如果这将允许“瓶颈”转换仅使用一个组件,即使这意味着某些其他转换必须读取它是不感兴趣。
在一个块中分配
如果改为分配多个数据块,则仅以跨步方式分配和访问一个数据块时的时间如下。这不会将凹凸从一个流变为两个,但为了完整起见,我将其包括在内。
这是修改后的代码:
void test_with_split(int num_arrays) {
int num_values = 100000000;
num_values -= (num_values % num_arrays);
int num_values_per_array = num_values / num_arrays;
// Initialize data to process
auto results = vector<int>(num_values);
auto array = vector<int>(num_values);
for (int i = 0; i < num_values; ++i) {
array.emplace_back(i);
results.emplace_back(0);
}
// Try to clear the cache
const int size = 20*1024*1024; // Allocate 20M. Set much larger then L2
char *c = (char *)malloc(size);
for (int i = 0; i < 100; i++)
for (int j = 0; j < size; j++)
c[j] = i*j;
free(c);
auto start = std::chrono::high_resolution_clock::now();
// Do the processing
int sum = 0;
for (int i = 0; i < num_values; ++i) {
sum += array[(i%num_arrays)*num_values_per_array+i/num_arrays];
results[i] = sum;
}
std::cout << "Time with " << num_arrays << " arrays: " << std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() - start).count() << " ms\n";
}
编辑 1
我确保 1 对 2 拆分的差异不是由于编译器展开循环并以不同方式优化第一次迭代。使用__attribute__ ((noinline))
我确保工作函数没有内联到主函数中。我通过查看生成的程序集验证了它没有发生。这些改变之后的时间是一样的。