page cache局部性
行优先VS列优先
代码
int array[n][n];
//行优先
for (int i = 0; i < n; ++i)
for (int j = 0; j < n; ++j)
array[i][j] += j;
//列优先
for (int i = 0; i < n; ++i)
for (int j = 0; j < n; ++j)
array[j][i] += j;
//乱序访问
for (int i = 0; i < n; ++i)
for (int j = 0; j < n; ++j)
array[rand()%n][rand()%n] += j;
行优先
列优先
乱序访问(链表)
性能差异
分析结论
L1 cache的cache line大小是64 bytes = 64 * 8 bits。每次cpu加载一个变量时,会一次性的加载一个cache line大小的内存到L1 cache。
行优先遍历数组,由于每次加载的cache line数据都能命中缓存,因此效率高
而列优先遍历,由于每次读完L1 cache变量后,读下一个变量有可能发现cache miss,需要重新从内存中加载,导致消息低
乱序访问是每次读去都会cache miss
访问数据的顺序
当我们有三个变量,cpu访问的顺序有可能有下面两种
第一种是不断的轮训访问三个变量,但是当再次访问变量时,有可能cache就会被踢走,需要重新加载变量到cache中。
比较高效的方法是,有序的访问三个变量,这样就能一直保持变量在cache中,直到处理完为止
伪共享与对其cache line
多线程共享一个原子变量
代码
void work(std::atomic& a) {
for(int i = 0; i < 100000; ++i) {
a++;
}
}
void directSharing() {
// Create a single atomic integer
std::atomic a;
a = 0;
// Four threads all sharing one atomic
std::thread t1([&]() {work(a)});
std::thread t2([&]() {work(a)});
std::thread t3([&]() {work(a)});
std::thread t4([&]() {work(a)});
// Join the 4 threads
t1.join();
t2.join();
t3.join();
t4.join();
}
性能
可以看到随着线程数的增加,性能会出现下降
原子变量在不同core之间的流转过程
初始状态
Core1 core2读取原子变量a,并加载到cache
Core1 修改a变量,并另core2的变量失效
core2重新加载a变量
分析与结论
原子变量在不同core之间,由于需要不断的令cache失效,又重新加载修改,造成随着分享的core越多,开销也越大
多线程发生伪共享
代码
void work(std::atomic& a) {
for(int i = 0; i < 100000; ++i) {
a++;
}
}
void falseSharing() {
// Create a single atomic integer
std::atomic a;
a = 0;
std::atomic b;
b = 0;
std::atomic c;
c = 0;
std::atomic d;
d = 0;
// Four threads each with their own atomic
std::thread t1([&]() {work(a)});
std::thread t2([&]() {work(b)});
std::thread t3([&]() {work(c)});
std::thread t4([&]() {work(d)});
// Join the 4 threads
t1.join();
t2.join();
t3.join();
t4.join();
}
性能
可以看到效果跟共享一个原子变量没有什么差异
分析与结论
这是因为变量的失效是以cache line为单位的,
变量a b c d,他们都位于同一个cache linezhong,所以只要有一个失效,其他变量也跟着失效
解决
对齐cache line
struct alignas(64) AlignedType {
AlignedType() { val = 0; }
std::atomic val;
};
void noSharing() {
AlignedType a{};
AlignedType b{};
AlignedType c{};
AlignedType d{};
// Launch the four threads now using our aligned data
std::thread t1([&]() { work(a.val); });
std::thread t2([&]() { work(b.val); });
std::thread t3([&]() { work(c.val); });
std::thread t4([&]() { work(d.val); });
// Join the threads
t1.join();
t2.join();
t3.join();
t4.join();
}
性能对比如下
stl库性能分析
Vector , List, deque性能对比
尾插入
线性搜索
析够
分析与结论
可以看到list由于元素内存的不连续性,导致小对象不能很好的利用page cache,导致性能比不上vecotr之类的容器
list对大对象性能是可观的,因为元素足够大,完全占满了cache line;
在我们使用的过程中,考虑到一般我们都是使用小对象为主,为了更好的利用page cache,应该避免使用链表,rb-tree, unordered_map的容器
寻求对缓存更友好的容器来做高性能开发