为什么遍历 `std::vector` 比遍历 `std::array` 更快?
Why is iterating though `std::vector` faster than iterating though `std::array`?
我最近问了这个问题:
Why is iterating an std::array much faster than iterating an std::vector?
正如人们很快指出的那样,我的基准测试有很多缺陷。所以当我试图修复我的基准时,我注意到 std::vector
并不比 std::array
慢,事实上,它恰恰相反。
#include <vector>
#include <array>
#include <stdio.h>
#include <chrono>
using namespace std;
constexpr int n = 100'000'000;
vector<int> v(n);
//array<int, n> v;
int main()
{
int res = 0;
auto start = chrono::steady_clock::now();
for(int x : v)
res += x;
auto end = chrono::steady_clock::now();
auto diff = end - start;
double elapsed =
std::chrono::duration_cast<
std::chrono::duration<double, std::milli>
>(end - start).count();
printf("result: %d\ntime: %f\n", res, elapsed);
}
我在之前的基准测试中尝试改进的地方:
- 确保我使用的是结果,所以整个循环没有被优化掉
- 为速度使用
-O3
标志
- 使用
std::chrono
而不是 time
命令。这样我们就可以隔离我们想要测量的部分(只是 for 循环)。变量的静态初始化之类的东西不会被测量。
实测次数:
数组:
$ g++ arrVsVec.cpp -O3
$ ./a.out
result: 0
time: 99.554109
矢量:
$ g++ arrVsVec.cpp -O3
$ ./a.out
result: 0
time: 30.734491
我只是想知道这次我做错了什么。
差异是由于 array
的内存页面不驻留在进程地址 space 中(全局范围数组存储在 .bss
section of the executable that hasn't been paged in, it is zero-initialized 中)。而 vector
刚刚被分配并且 zero-filled,所以它的内存页面已经存在。
如果你添加
std::fill_n(v.data(), n, 1); // included in <algorithm>
作为main
的第一行将页面引入(pre-fault),这使得array
时间与vector
.[=21的时间相同=]
在 Linux 上,您可以 mlock(v.data(), v.size() * sizeof(v[0]));
将页面放入地址 space 而不是那样。有关详细信息,请参阅 man mlock
。
内存mapping/allocating是惰性的:第一次访问页面会导致缺页异常(#PF
on x86)。这包括 BSS,以及 file-backed 映射,例如您的 executable 的文本段。这些页面错误是“有效的”,因此它们不会导致传递 SIGSEGV;相反,内核会在必要时分配一个物理页面并连接硬件页面 tables,以便加载或存储可以重新 运行 而不会在第二次出现故障。
这是昂贵的,特别是如果内核不“fault-around”并在一个页面错误期间准备多个页面。 (特别是启用 Spectre + Meltdown 缓解措施后,用户 <-> 内核往返在当前 x86-64 硬件上的成本更高。)
您让 std:vector
的构造函数在动态分配后将零写入数组 1。 std::vector
在您的定时循环之外执行所有 page-faulting。 这发生在 main 之前,而实现是静态对象的 运行ning 构造函数。
但是数组是 zero-initialized 所以它被放置在 BSS 中。触摸它的第一件事是你的循环。 您的 array<>
循环为计时区域内的所有页面错误支付费用。
如果您使用 new int[n]
动态分配但 未 初始化内存块,您会看到与静态 array<>
相同的行为. (如果 Linux 更愿意使用透明大页面进行动态分配而不是 BSS 映射,可能会稍微好一点。)
脚注 1 std::vector
在 libstdc++ 和 libc++ 中太愚蠢了,无法利用从 OS 获取 already-zeroed 页的优势,喜欢它可以,如果它使用 calloc
或等价物。如果库提供了一个 new
/delete
兼容的内存分配器,这将是可能的。
C++ new
/delete
相对于 malloc/free/calloc/realloc 是残废的。我不知道为什么 ISO C++ 遗漏了 calloc 和 realloc:两者对于大型分配都非常有用,尤其是 realloc 用于调整 std::vector of trivially-copyable 对象的大小,这些对象可能有空间在不复制的情况下增长其映射。但是由于 new
/delete
不能保证与 malloc
/free
兼容,并且 new
是可替换的,所以库不能很容易地使用 calloc
和 realloc
甚至在幕后。
另一个因素:read-only 将页 CoW 映射到同一物理零页
当读取(而不是写入)触发惰性分配时,它读取为零。 (BSS 页面读取为零,来自 mmap(MAP_ANONYMOUS)
的新页面读取为 all-zero。)
连接硬件页面的(软)页面错误处理程序 table 不需要实际分配物理页面又名 page-frame 来支持该虚拟页面。相反,Linux 将干净的(未写入的)匿名页面映射到单个物理归零页面。 (这适用于所有任务。)
如果我们对数组进行多次传递,这会导致奇怪的情况,我们可以得到 TLB 未命中但 L1d 或 L3 命中(取决于大页面与否),因为我们有多个虚拟页面指向相同的物理位置.
(一些 CPU,例如 AMD Ryzen,在 L1d 缓存中使用 micro-tagging 来保存,代价是缓存只能命中一个虚拟地址,即使相同的内存被映射到多个virtual addresses. Intel CPUs use true VIPT L1d caches and really get this effect),
我为 Linux 制作了一个测试程序,它将使用 madvise(MADV_HUGEPAGE)
(鼓励内核为大页面整理内存碎片)或 madvise(MADV_NOHUGEPAGE)
(即使对于 read-only案例).
出于某种原因 Linux BSS 页面在编写时不使用大页面。仅读取它们确实使用 2M 大页面(对于 L1d 或 L2 来说太大了,但确实适合 L3。但我们确实获得了所有 TLB 命中)。在 /proc/PID/smaps
中很难看到这一点,因为未写入的内存根本不会显示为“常驻”。 (请记住它由 system-wide 零共享区域物理支持)。
我对您的基准代码做了一些更改,以重新运行 求和循环多次 在 一个初始化传递 之后根据 command-line 参数读取或写入数组。 repeat-loop 使它 运行 更长,因此我们可以获得更精确的时间,并分摊 init 以便我们从 perf.
获得有用的结果
#include <vector>
#include <array>
#include <stdio.h>
#include <chrono>
#include <sys/mman.h>
using namespace std;
constexpr int n = 100'000'000;
//vector<int> v(n);
alignas(4096) array<int, n> v;
//template<class T>
__attribute__((noinline))
int toucharray(volatile int *vv, int write_init) {
int res=vv[0];
for(int i=32 ; i<n ; i+=128)
if(write_init)
vv[i] = 0;
else
res += vv[i];
// volatile int sum = res; // noinline is fine, we don't need to stop multiple calls from CSEing
return res;
}
template <class T>
__attribute__((noinline,noclone))
int sum_container(T &vv) {
unsigned int res=0;
for(int x : vv)
res += x;
__attribute__((used)) static volatile int sink;
sink = res; // a side-effect stops IPA from deciding that this is a pure function
return res;
}
int main(int argc, char**argv)
{
int write_init = 0;
int hugepage = 0;
if (argc>1) {
hugepage = argv[1][0] & 1;
write_init = argv[1][0] & 2;
}
int repcount = 1000;
if (argc>2)
repcount = atoi(argv[2]);
// TODO: option for no madvise.
madvise(v.data(), n*sizeof(v[0]), MADV_SEQUENTIAL);
madvise(v.data(), n*sizeof(v[0]), hugepage ? MADV_HUGEPAGE : MADV_NOHUGEPAGE);
madvise(v.data(), n*sizeof(v[0]), MADV_WILLNEED);
// SEQ and WILLNEED probably only matter for file-backed mappings to reduce hard page faults.
// Probably not encouraging faultahead / around for lazy-allocation soft page fault
toucharray(v.data(), write_init);
int res = 0;
auto start = chrono::steady_clock::now();
for(int i=0; i<repcount ; i++)
res = sum_container(v);
auto end = chrono::steady_clock::now();
double elapsed =
std::chrono::duration_cast<
std::chrono::duration<double, std::milli>
>(end - start).count();
printf("result: %d\ntime: %f\n", res, elapsed);
}
最好的情况:clang++ -O3 -march=native (skylake) 实际上是用多个累加器展开的,不像 gcc -funroll-loops 做的很傻。
在我的带有 DDR4-2666 DRAM 的 Skylake i7-6700k 上,配置为 4.2GHz 最大睿频和 governor=performance -
# using std::array<int,n>
# 0&1 = 0 -> MADV_NOHUGEPAGE. 0&2 = 0 -> read-only init
taskset -c 3 perf stat -etask-clock:u,context-switches,cpu-migrations,page-faults,cycles,instructions,mem_load_retired.l2_hit:u,mem_load_retired.l1_hit:u,mem_inst_retired.stlb_miss_loads:u ./touchpage-array-argc.clang 0 1000
result: 0
time: 1961.952394
Performance counter stats for './touchpage-array-madv-nohuge-argc.clang 0 1000':
2,017.34 msec task-clock:u # 1.000 CPUs utilized
50 context-switches # 0.025 K/sec
0 cpu-migrations # 0.000 K/sec
97,774 page-faults # 0.048 M/sec
8,287,680,837 cycles # 4.108 GHz
14,500,762,859 instructions # 1.75 insn per cycle
13,688 mem_load_retired.l2_hit:u # 0.007 M/sec
12,501,329,912 mem_load_retired.l1_hit:u # 6196.927 M/sec
144,559 mem_inst_retired.stlb_miss_loads:u # 0.072 M/sec
2.017765632 seconds time elapsed
1.979410000 seconds user
0.036659000 seconds sys
注意相当多的 TLB 未命中(mem_inst_retired.stlb_miss_loads:u
在 user-space 中计算二级 TLB 未命中)。和 97k 页面错误。这几乎与覆盖 100M * 4 = 400MB 数组所需的 4k 页一样多,因此我们每页有 1 个错误,没有 pre-fault / fault-around.
幸运的是,Skylake 有两个 page-walk 单元,因此它可以并行进行两个推测 page-walk。此外,所有数据访问都在 L1d 中进行,因此 page-tables 至少会保持热度L2,加快页面浏览速度。
# using array
# MADV_HUGEPAGE, read-only init
taskset -c 3 perf stat -etask-clock:u,context-switches,cpu-migrations,page-faults,cycles,instructions,mem_load_retired.l2_hit:u,mem_load_retired.l1_hit:u,mem_inst_retired.stlb_miss_loads:u ./touchpage-array-argc.clang 1 1000
result: 0
time: 5947.741408
Performance counter stats for './touchpage-array-argc.clang 1 1000':
5,951.40 msec task-clock:u # 1.000 CPUs utilized
9 context-switches # 0.002 K/sec
0 cpu-migrations # 0.000 K/sec
687 page-faults # 0.115 K/sec
24,377,094,416 cycles # 4.096 GHz
14,397,054,228 instructions # 0.59 insn per cycle
2,183,878,846 mem_load_retired.l2_hit:u # 366.952 M/sec
313,684,419 mem_load_retired.l1_hit:u # 52.708 M/sec
13,218 mem_inst_retired.stlb_miss_loads:u # 0.002 M/sec
5.951530513 seconds time elapsed
5.944087000 seconds user
0.003284000 seconds sys
注意 ~1/10 的 TLB 未命中,但是在相同的 ~12G 内存加载中,只有 2G 在 L2 中命中,这可能要归功于成功的 HW 预取。 (尽管其余部分确实在 L3 中命中。)而且我们只有 687 个页面错误; faultaround 和 hugepages 的组合使这更加有效。
请注意,由于 L3 带宽的瓶颈,所用时间增加了 3 倍。
Write-init 数组给了我们两个世界中最糟糕的:
# using array
# MADV_HUGEPAGE (no apparent effect on BSS) and write-init
taskset -c 3 perf stat -etask-clock:u,context-switches,cpu-migrations,page-faults,cycles,instructions,mem_load_retired.l2_hit:u,mem_load_retired.l1_hit:u,mem_inst_retired.stlb_miss_loads:u ./touchpage-array-argc.clang 3 1000
result: 0
time: 16510.222762
Performance counter stats for './touchpage-array-argc.clang 3 1000':
17,143.35 msec task-clock:u # 1.000 CPUs utilized
341 context-switches # 0.020 K/sec
0 cpu-migrations # 0.000 K/sec
95,218 page-faults # 0.006 M/sec
70,475,978,274 cycles # 4.111 GHz
17,989,948,598 instructions # 0.26 insn per cycle
634,015,284 mem_load_retired.l2_hit:u # 36.983 M/sec
107,041,744 mem_load_retired.l1_hit:u # 6.244 M/sec
37,715,860 mem_inst_retired.stlb_miss_loads:u # 2.200 M/sec
17.147615898 seconds time elapsed
16.494211000 seconds user
0.625193000 seconds sys
大量页面错误。还有更多的 TLB 未命中。
std::vector 版本与数组基本相同:
strace
说明 madvise 没有起作用,因为我没有对齐指针。 glibc / libstdc++ new
倾向于 return 一个 page-aligned + 16 的指针,分配器簿记在前 16 个字节中。对于数组,我使用 alignas(4096)
来确保我可以将它传递给 madvise。
madvise(0x7f760d133010, 400000000, MADV_HUGEPAGE) = -1 EINVAL (Invalid argument)
所以无论如何,使用我的内核调整设置,它只会尝试对 madvise 上的大页面进行内存碎片整理,而内存在 ATM 中是非常零散的。所以它最终没有使用任何大页面。
taskset -c 3 perf stat -etask-clock:u,context-switches,cpu-migrations,page-faults,cycles,instructions,mem_load_retired.l2_hit:u,mem_load_retired.l1_hit:u,mem_inst_retired.stlb_miss_loads:u ./touchpage-vector-argv.clang 3 1000
result: 0
time: 16020.821517
Performance counter stats for './touchpage-vector-argv.clang 3 1000':
16,159.19 msec task-clock:u # 1.000 CPUs utilized
17 context-switches # 0.001 K/sec
0 cpu-migrations # 0.000 K/sec
97,771 page-faults # 0.006 M/sec
66,146,780,261 cycles # 4.093 GHz
15,294,999,994 instructions # 0.23 insn per cycle
217,426,277 mem_load_retired.l2_hit:u # 13.455 M/sec
842,878,166 mem_load_retired.l1_hit:u # 52.161 M/sec
1,788,935 mem_inst_retired.stlb_miss_loads:u # 0.111 M/sec
16.160982779 seconds time elapsed
16.017206000 seconds user
0.119618000 seconds sys
我不确定为什么 TLB 未命中率比 THP read-only 测试高得多。也许内存访问争用 and/or 通过触摸更多内存逐出缓存页面 table 最终会减慢页面访问速度,因此 TLB-prefetch 跟不上。
在约 12G 的负载中,硬件预取能够使其中约 1G 命中 L1d 或 L2 缓存。
我最近问了这个问题: Why is iterating an std::array much faster than iterating an std::vector?
正如人们很快指出的那样,我的基准测试有很多缺陷。所以当我试图修复我的基准时,我注意到 std::vector
并不比 std::array
慢,事实上,它恰恰相反。
#include <vector>
#include <array>
#include <stdio.h>
#include <chrono>
using namespace std;
constexpr int n = 100'000'000;
vector<int> v(n);
//array<int, n> v;
int main()
{
int res = 0;
auto start = chrono::steady_clock::now();
for(int x : v)
res += x;
auto end = chrono::steady_clock::now();
auto diff = end - start;
double elapsed =
std::chrono::duration_cast<
std::chrono::duration<double, std::milli>
>(end - start).count();
printf("result: %d\ntime: %f\n", res, elapsed);
}
我在之前的基准测试中尝试改进的地方:
- 确保我使用的是结果,所以整个循环没有被优化掉
- 为速度使用
-O3
标志 - 使用
std::chrono
而不是time
命令。这样我们就可以隔离我们想要测量的部分(只是 for 循环)。变量的静态初始化之类的东西不会被测量。
实测次数:
数组:
$ g++ arrVsVec.cpp -O3
$ ./a.out
result: 0
time: 99.554109
矢量:
$ g++ arrVsVec.cpp -O3
$ ./a.out
result: 0
time: 30.734491
我只是想知道这次我做错了什么。
差异是由于 array
的内存页面不驻留在进程地址 space 中(全局范围数组存储在 .bss
section of the executable that hasn't been paged in, it is zero-initialized 中)。而 vector
刚刚被分配并且 zero-filled,所以它的内存页面已经存在。
如果你添加
std::fill_n(v.data(), n, 1); // included in <algorithm>
作为main
的第一行将页面引入(pre-fault),这使得array
时间与vector
.[=21的时间相同=]
在 Linux 上,您可以 mlock(v.data(), v.size() * sizeof(v[0]));
将页面放入地址 space 而不是那样。有关详细信息,请参阅 man mlock
。
内存mapping/allocating是惰性的:第一次访问页面会导致缺页异常(#PF
on x86)。这包括 BSS,以及 file-backed 映射,例如您的 executable 的文本段。这些页面错误是“有效的”,因此它们不会导致传递 SIGSEGV;相反,内核会在必要时分配一个物理页面并连接硬件页面 tables,以便加载或存储可以重新 运行 而不会在第二次出现故障。
这是昂贵的,特别是如果内核不“fault-around”并在一个页面错误期间准备多个页面。 (特别是启用 Spectre + Meltdown 缓解措施后,用户 <-> 内核往返在当前 x86-64 硬件上的成本更高。)
您让 std:vector
的构造函数在动态分配后将零写入数组 1。 std::vector
在您的定时循环之外执行所有 page-faulting。 这发生在 main 之前,而实现是静态对象的 运行ning 构造函数。
但是数组是 zero-initialized 所以它被放置在 BSS 中。触摸它的第一件事是你的循环。 您的 array<>
循环为计时区域内的所有页面错误支付费用。
如果您使用 new int[n]
动态分配但 未 初始化内存块,您会看到与静态 array<>
相同的行为. (如果 Linux 更愿意使用透明大页面进行动态分配而不是 BSS 映射,可能会稍微好一点。)
脚注 1 std::vector
在 libstdc++ 和 libc++ 中太愚蠢了,无法利用从 OS 获取 already-zeroed 页的优势,喜欢它可以,如果它使用 calloc
或等价物。如果库提供了一个 new
/delete
兼容的内存分配器,这将是可能的。
C++ new
/delete
相对于 malloc/free/calloc/realloc 是残废的。我不知道为什么 ISO C++ 遗漏了 calloc 和 realloc:两者对于大型分配都非常有用,尤其是 realloc 用于调整 std::vector of trivially-copyable 对象的大小,这些对象可能有空间在不复制的情况下增长其映射。但是由于 new
/delete
不能保证与 malloc
/free
兼容,并且 new
是可替换的,所以库不能很容易地使用 calloc
和 realloc
甚至在幕后。
另一个因素:read-only 将页 CoW 映射到同一物理零页
当读取(而不是写入)触发惰性分配时,它读取为零。 (BSS 页面读取为零,来自 mmap(MAP_ANONYMOUS)
的新页面读取为 all-zero。)
连接硬件页面的(软)页面错误处理程序 table 不需要实际分配物理页面又名 page-frame 来支持该虚拟页面。相反,Linux 将干净的(未写入的)匿名页面映射到单个物理归零页面。 (这适用于所有任务。)
如果我们对数组进行多次传递,这会导致奇怪的情况,我们可以得到 TLB 未命中但 L1d 或 L3 命中(取决于大页面与否),因为我们有多个虚拟页面指向相同的物理位置.
(一些 CPU,例如 AMD Ryzen,在 L1d 缓存中使用 micro-tagging 来保存,代价是缓存只能命中一个虚拟地址,即使相同的内存被映射到多个virtual addresses. Intel CPUs use true VIPT L1d caches and really get this effect),
我为 Linux 制作了一个测试程序,它将使用 madvise(MADV_HUGEPAGE)
(鼓励内核为大页面整理内存碎片)或 madvise(MADV_NOHUGEPAGE)
(即使对于 read-only案例).
出于某种原因 Linux BSS 页面在编写时不使用大页面。仅读取它们确实使用 2M 大页面(对于 L1d 或 L2 来说太大了,但确实适合 L3。但我们确实获得了所有 TLB 命中)。在 /proc/PID/smaps
中很难看到这一点,因为未写入的内存根本不会显示为“常驻”。 (请记住它由 system-wide 零共享区域物理支持)。
我对您的基准代码做了一些更改,以重新运行 求和循环多次 在 一个初始化传递 之后根据 command-line 参数读取或写入数组。 repeat-loop 使它 运行 更长,因此我们可以获得更精确的时间,并分摊 init 以便我们从 perf.
获得有用的结果#include <vector>
#include <array>
#include <stdio.h>
#include <chrono>
#include <sys/mman.h>
using namespace std;
constexpr int n = 100'000'000;
//vector<int> v(n);
alignas(4096) array<int, n> v;
//template<class T>
__attribute__((noinline))
int toucharray(volatile int *vv, int write_init) {
int res=vv[0];
for(int i=32 ; i<n ; i+=128)
if(write_init)
vv[i] = 0;
else
res += vv[i];
// volatile int sum = res; // noinline is fine, we don't need to stop multiple calls from CSEing
return res;
}
template <class T>
__attribute__((noinline,noclone))
int sum_container(T &vv) {
unsigned int res=0;
for(int x : vv)
res += x;
__attribute__((used)) static volatile int sink;
sink = res; // a side-effect stops IPA from deciding that this is a pure function
return res;
}
int main(int argc, char**argv)
{
int write_init = 0;
int hugepage = 0;
if (argc>1) {
hugepage = argv[1][0] & 1;
write_init = argv[1][0] & 2;
}
int repcount = 1000;
if (argc>2)
repcount = atoi(argv[2]);
// TODO: option for no madvise.
madvise(v.data(), n*sizeof(v[0]), MADV_SEQUENTIAL);
madvise(v.data(), n*sizeof(v[0]), hugepage ? MADV_HUGEPAGE : MADV_NOHUGEPAGE);
madvise(v.data(), n*sizeof(v[0]), MADV_WILLNEED);
// SEQ and WILLNEED probably only matter for file-backed mappings to reduce hard page faults.
// Probably not encouraging faultahead / around for lazy-allocation soft page fault
toucharray(v.data(), write_init);
int res = 0;
auto start = chrono::steady_clock::now();
for(int i=0; i<repcount ; i++)
res = sum_container(v);
auto end = chrono::steady_clock::now();
double elapsed =
std::chrono::duration_cast<
std::chrono::duration<double, std::milli>
>(end - start).count();
printf("result: %d\ntime: %f\n", res, elapsed);
}
最好的情况:clang++ -O3 -march=native (skylake) 实际上是用多个累加器展开的,不像 gcc -funroll-loops 做的很傻。
在我的带有 DDR4-2666 DRAM 的 Skylake i7-6700k 上,配置为 4.2GHz 最大睿频和 governor=performance -
# using std::array<int,n>
# 0&1 = 0 -> MADV_NOHUGEPAGE. 0&2 = 0 -> read-only init
taskset -c 3 perf stat -etask-clock:u,context-switches,cpu-migrations,page-faults,cycles,instructions,mem_load_retired.l2_hit:u,mem_load_retired.l1_hit:u,mem_inst_retired.stlb_miss_loads:u ./touchpage-array-argc.clang 0 1000
result: 0
time: 1961.952394
Performance counter stats for './touchpage-array-madv-nohuge-argc.clang 0 1000':
2,017.34 msec task-clock:u # 1.000 CPUs utilized
50 context-switches # 0.025 K/sec
0 cpu-migrations # 0.000 K/sec
97,774 page-faults # 0.048 M/sec
8,287,680,837 cycles # 4.108 GHz
14,500,762,859 instructions # 1.75 insn per cycle
13,688 mem_load_retired.l2_hit:u # 0.007 M/sec
12,501,329,912 mem_load_retired.l1_hit:u # 6196.927 M/sec
144,559 mem_inst_retired.stlb_miss_loads:u # 0.072 M/sec
2.017765632 seconds time elapsed
1.979410000 seconds user
0.036659000 seconds sys
注意相当多的 TLB 未命中(mem_inst_retired.stlb_miss_loads:u
在 user-space 中计算二级 TLB 未命中)。和 97k 页面错误。这几乎与覆盖 100M * 4 = 400MB 数组所需的 4k 页一样多,因此我们每页有 1 个错误,没有 pre-fault / fault-around.
幸运的是,Skylake 有两个 page-walk 单元,因此它可以并行进行两个推测 page-walk。此外,所有数据访问都在 L1d 中进行,因此 page-tables 至少会保持热度L2,加快页面浏览速度。
# using array
# MADV_HUGEPAGE, read-only init
taskset -c 3 perf stat -etask-clock:u,context-switches,cpu-migrations,page-faults,cycles,instructions,mem_load_retired.l2_hit:u,mem_load_retired.l1_hit:u,mem_inst_retired.stlb_miss_loads:u ./touchpage-array-argc.clang 1 1000
result: 0
time: 5947.741408
Performance counter stats for './touchpage-array-argc.clang 1 1000':
5,951.40 msec task-clock:u # 1.000 CPUs utilized
9 context-switches # 0.002 K/sec
0 cpu-migrations # 0.000 K/sec
687 page-faults # 0.115 K/sec
24,377,094,416 cycles # 4.096 GHz
14,397,054,228 instructions # 0.59 insn per cycle
2,183,878,846 mem_load_retired.l2_hit:u # 366.952 M/sec
313,684,419 mem_load_retired.l1_hit:u # 52.708 M/sec
13,218 mem_inst_retired.stlb_miss_loads:u # 0.002 M/sec
5.951530513 seconds time elapsed
5.944087000 seconds user
0.003284000 seconds sys
注意 ~1/10 的 TLB 未命中,但是在相同的 ~12G 内存加载中,只有 2G 在 L2 中命中,这可能要归功于成功的 HW 预取。 (尽管其余部分确实在 L3 中命中。)而且我们只有 687 个页面错误; faultaround 和 hugepages 的组合使这更加有效。
请注意,由于 L3 带宽的瓶颈,所用时间增加了 3 倍。
Write-init 数组给了我们两个世界中最糟糕的:
# using array
# MADV_HUGEPAGE (no apparent effect on BSS) and write-init
taskset -c 3 perf stat -etask-clock:u,context-switches,cpu-migrations,page-faults,cycles,instructions,mem_load_retired.l2_hit:u,mem_load_retired.l1_hit:u,mem_inst_retired.stlb_miss_loads:u ./touchpage-array-argc.clang 3 1000
result: 0
time: 16510.222762
Performance counter stats for './touchpage-array-argc.clang 3 1000':
17,143.35 msec task-clock:u # 1.000 CPUs utilized
341 context-switches # 0.020 K/sec
0 cpu-migrations # 0.000 K/sec
95,218 page-faults # 0.006 M/sec
70,475,978,274 cycles # 4.111 GHz
17,989,948,598 instructions # 0.26 insn per cycle
634,015,284 mem_load_retired.l2_hit:u # 36.983 M/sec
107,041,744 mem_load_retired.l1_hit:u # 6.244 M/sec
37,715,860 mem_inst_retired.stlb_miss_loads:u # 2.200 M/sec
17.147615898 seconds time elapsed
16.494211000 seconds user
0.625193000 seconds sys
大量页面错误。还有更多的 TLB 未命中。
std::vector 版本与数组基本相同:
strace
说明 madvise 没有起作用,因为我没有对齐指针。 glibc / libstdc++ new
倾向于 return 一个 page-aligned + 16 的指针,分配器簿记在前 16 个字节中。对于数组,我使用 alignas(4096)
来确保我可以将它传递给 madvise。
madvise(0x7f760d133010, 400000000, MADV_HUGEPAGE) = -1 EINVAL (Invalid argument)
所以无论如何,使用我的内核调整设置,它只会尝试对 madvise 上的大页面进行内存碎片整理,而内存在 ATM 中是非常零散的。所以它最终没有使用任何大页面。
taskset -c 3 perf stat -etask-clock:u,context-switches,cpu-migrations,page-faults,cycles,instructions,mem_load_retired.l2_hit:u,mem_load_retired.l1_hit:u,mem_inst_retired.stlb_miss_loads:u ./touchpage-vector-argv.clang 3 1000
result: 0
time: 16020.821517
Performance counter stats for './touchpage-vector-argv.clang 3 1000':
16,159.19 msec task-clock:u # 1.000 CPUs utilized
17 context-switches # 0.001 K/sec
0 cpu-migrations # 0.000 K/sec
97,771 page-faults # 0.006 M/sec
66,146,780,261 cycles # 4.093 GHz
15,294,999,994 instructions # 0.23 insn per cycle
217,426,277 mem_load_retired.l2_hit:u # 13.455 M/sec
842,878,166 mem_load_retired.l1_hit:u # 52.161 M/sec
1,788,935 mem_inst_retired.stlb_miss_loads:u # 0.111 M/sec
16.160982779 seconds time elapsed
16.017206000 seconds user
0.119618000 seconds sys
我不确定为什么 TLB 未命中率比 THP read-only 测试高得多。也许内存访问争用 and/or 通过触摸更多内存逐出缓存页面 table 最终会减慢页面访问速度,因此 TLB-prefetch 跟不上。
在约 12G 的负载中,硬件预取能够使其中约 1G 命中 L1d 或 L2 缓存。