Meltdown 缓解措施与 calloc() 的 CoW "lazy allocation" 相结合是否意味着 calloc() 分配内存的性能下降?
Does the Meltdown mitigation, in combination with `calloc()`s CoW "lazy allocation", imply a performance hit for calloc()-allocated memory?
所以 calloc()
通过向 OS 请求一些虚拟内存来工作。 OS 与 MMU 协同工作,并巧妙地响应一个实际映射到 copy-on-write, read-only page full of zeroes 的虚拟内存地址。当程序试图写入该页面中的任何位置时,会发生页面错误(因为您不能写入只读页面),创建该页面的副本,并且您的程序的虚拟内存将映射到这些页面的这个全新副本零。
现在 Meltdown 已经成为现实,OSes 已被修补,因此不再可能跨内核用户边界推测性执行。这意味着无论何时用户代码调用内核代码,都会有效地导致流水线停顿。通常,当管道在循环中停止时,它对性能是毁灭性的,因为 CPU 最终浪费时间等待数据,无论是来自缓存还是主内存。
鉴于此,我想知道的是:
- 当程序写入分配给
calloc()
的以前从未访问过的页面,并且发生到新 CoW 页面的重新映射时,这是在执行内核代码吗?
- 页错误写时复制功能是在 OS 级别还是 MMU 级别实现的?
- 如果我调用
calloc()
分配 4GiB 内存,然后在一个紧密循环中用某个任意值(比如 0xFF
而不是 0x00
)初始化它,我的 ( Intel) CPU 每次写入新页面时都会达到推测边界?
- 最后,如果它是真实的,是否存在这种影响对实际性能有显着影响的情况?
你的前提是错误的。页面错误从来都不是流水线/超便宜的。崩溃(和 Spectre)缓解措施确实使它们变得更加昂贵,但是,以及系统调用和所有其他用户-> 内核转换。
跨越 kernel/user 边界的推测执行是不可能的;英特尔 CPUs 不重命名特权级别,即 kernel/user 转换总是需要完整的管道刷新。我认为您误解了 Meltdown:这纯粹是由于 user-space 和 delayed handling of the privilege checks on TLB hits.
中的推测执行造成的
这在 CPU 设计中是通用的,AFAIK。我不知道有任何重命名特权级别或以其他方式推测内核代码、x86 或其他方式的微体系结构。
Meltdown 缓解措施增加的成本是进入内核会刷新 TLB。 (或者在支持 TLB 进程上下文 ID 的CPUs 上,内核可以使用 PCID 来使内核使用单独的页面-tables 与用户-space 便宜得多)。 =28=]
内核入口点(在 Linux 上)变成了交换页面 tables 并跳转到 real 内核入口点的蹦床,以避免将内核 ASLR 偏移量暴露给 user-space。但除此之外,还有一个额外的 mov cr3, reg
进入和退出内核(设置一个新页面 table),没有其他改变。
(Spectre 缓解也很棘手,需要更多更改,如 retpolines...并且还可能显着增加用户-> 内核-> 用户的成本。关于页面错误成本的 IDK。)
@BeeOnRope 报告(查看评论和他的回答以获得完整的详细信息)没有 Spectre 补丁,只是应用了 Meltdown 补丁,但是 nopti
"disable" 它的启动选项增加了往返成本到 Skylake CPU 上的内核(syscall
和伪造的 RAX,立即返回 -ENOSYS
)从 ~100 增加到 ~300 周期。所以这可能就是蹦床的成本? 并且在启用实际页面-table 隔离的情况下,它上升到约 700 个周期。那是 根本没有 幽灵缓解补丁。 (此外,这是 x86-64 syscall
入口点,而不是页面错误。不过它们可能相似。)
页面错误异常:
CPUs 不预测页面错误,所以他们无论如何都不能推测性地执行处理程序。页面错误入口点的预取或解码可能会在流水线刷新时发生,但是直到页面错误指令试图退出时该过程才会开始。错误 load/store 被标记为在退休时生效,并且不会重新引导前端; Meltdown 的全部关键是在故障负载报废之前不采取任何措施。
相关:When an interrupt occurs, what happens to instructions in the pipeline?
另外: 有一些关于什么样的推测真正导致崩溃的细节,以及 CPU 如何处理故障。
When a program writes to a never-before-accessed page which was allocated with calloc()
, and the remapping to the new CoW page occurs, is this executing kernel code?
是的,页面错误由内核的页面错误处理程序处理。写时复制没有纯硬件处理。
If I call calloc() to allocate 4GiB of memory, then initialize it with some arbitrary value (say, 0xFF instead of 0x00) in a tight loop, is my (Intel) CPU going to be hitting a speculation boundary every time it writes to a new page?
是的。内核不会对归零页面进行故障处理(与页面缓存中数据热时的文件支持映射不同)。所以每一个新的页面被触及都会导致一个页面错误,即使对于小的 4k 普通页面也是如此。 (感谢@BeeOnRope 提供有关这方面的准确信息。)使用匿名大页面,每 2MiB (x86-64) 只会出现一次页面错误,这要好得多。
如果您想避免每页成本,请在 Linux 系统上分配 mmap(MAP_POPULATE)
以将所有页面预置到硬件页面 table 中。我不确定 madvise
是否可以为您预先设置页面,例如madvise(MADV_WILLNEED)
在已映射的区域上。但是 madvise(MADV_HUGEPAGE)
会鼓励内核使用匿名大页(并且可能会整理物理内存碎片以释放连续的 2M 块以启用它,如果你没有将它配置为在没有 madvise
的情况下这样做)。
相关: 在带有 KPTI 补丁的 Linux 内核上有一些 perf
结果。
是 使用 calloc()
分配的内存会因 Meltdown 和 Spectre 补丁而导致性能下降。
事实上,calloc()
在这里并不特殊:malloc()
、new
以及更普遍的所有分配的内存可能会受到大致相同的性能影响。 calloc()
和 malloc()
最终都由 OS 返回的页面支持(尽管分配器将在它们被释放后 re-use 它们)。唯一真正的区别是智能分配器,当它沿着使用来自 OS 的新页面的路径(而不是 re-using 以前的 free
d 分配)在 calloc
它可以省略归零,因为 OS-provided 页保证为零。除此之外,分配器行为在很大程度上是相同的,OS-level 归零行为也是相同的(通常没有选项要求 OS 获取 non-zero 页)。
所以性能影响比您想象的更广泛,但性能影响可能比您建议的要小,因为页面错误已经做了很多工作,所以您说的不是一个数量级退化或任何东西。有关性能影响可能有限的原因,请参阅 Peter's answer。我写这个答案主要是因为你的标题问题的答案仍然是 yes 因为有 some 影响。
为了估计对 malloc
繁重工作流程的影响,我尝试了 运行 一些分配和 page-fault 使用 Spectre 在当前内核 (4.13.0-39-generic
) 上进行繁重测试和崩溃缓解措施,以及在这些缓解措施之前的旧内核上。
测试代码很简单:
#include <stdlib.h>
#include <stdio.h>
#define SIZE (40 * 1024 * 1024)
#define PG_SIZE 4096
int main() {
char *mem = malloc(SIZE);
for (volatile char *p = mem; p < mem + SIZE; p += PG_SIZE) {
*p = 'z';
}
printf("pages touched: %d\npoitner value : %p\n", SIZE / PG_SIZE, mem);
}
较新内核的结果是每个页面错误大约 3700 个周期,而在没有缓解措施的旧内核上大约 3300 个周期。由于缓解措施导致的总体回归(大概)约为 14%。请注意,这是在 Skylake 硬件 (i7-6700HQ) 上,其中一些 Spectre 缓解措施的成本更低,并且内核支持 PCID,这使得 KPTI Meltdown 缓解措施更便宜。在不同的硬件上结果可能更差。
奇怪的是,在启动时禁用了 Spectre 和 Meltdown 缓解措施(使用 spectre_v2=off nopti
)的新内核的结果 比新内核默认值或旧内核,每个页面错误大约有 5050 个周期,与具有缓解措施 启用 的同一内核相比大约有 35% 的回归。所以当缓解措施被禁用时,确实出现了问题,performance-wise。
完整结果
这是两次运行的完整 perf stat
输出。
旧内核 (4.10.0-42)
pages touched: 10240
poitner value : 0x7f7d2561e010
Performance counter stats for './pagefaults':
12.980048 task-clock (msec) # 0.976 CPUs utilized
0 context-switches # 0.000 K/sec
0 cpu-migrations # 0.000 K/sec
10,286 page-faults # 0.792 M/sec
33,662,397 cycles # 2.593 GHz
27,230,864 instructions # 0.81 insn per cycle
4,535,443 branches # 349.417 M/sec
11,760 branch-misses # 0.26% of all branches
0.013293417 seconds time elapsed
新内核 (4.13.0-39)
pages touched: 10240
poitner value : 0x7f306ad69010
Performance counter stats for './pagefaults':
14.789615 task-clock (msec) # 0.966 CPUs utilized
8 context-switches # 0.541 K/sec
0 cpu-migrations # 0.000 K/sec
10,288 page-faults # 0.696 M/sec
38,318,595 cycles # 2.591 GHz
28,796,523 instructions # 0.75 insn per cycle
4,693,944 branches # 317.381 M/sec
26,853 branch-misses # 0.57% of all branches
0.015312764 seconds time elapsed
新内核 (4.13.0.-39) spectre_v2=关闭 nopti
pages touched: 10240
poitner value : 0x7ff079ede010
Performance counter stats for './pagefaults':
16.690621 task-clock (msec) # 0.982 CPUs utilized
0 context-switches # 0.000 K/sec
0 cpu-migrations # 0.000 K/sec
10,286 page-faults # 0.616 M/sec
51,964,080 cycles # 3.113 GHz
28,602,441 instructions # 0.55 insn per cycle
4,699,608 branches # 281.572 M/sec
25,064 branch-misses # 0.53% of all branches
0.017001581 seconds time elapsed
所以 calloc()
通过向 OS 请求一些虚拟内存来工作。 OS 与 MMU 协同工作,并巧妙地响应一个实际映射到 copy-on-write, read-only page full of zeroes 的虚拟内存地址。当程序试图写入该页面中的任何位置时,会发生页面错误(因为您不能写入只读页面),创建该页面的副本,并且您的程序的虚拟内存将映射到这些页面的这个全新副本零。
现在 Meltdown 已经成为现实,OSes 已被修补,因此不再可能跨内核用户边界推测性执行。这意味着无论何时用户代码调用内核代码,都会有效地导致流水线停顿。通常,当管道在循环中停止时,它对性能是毁灭性的,因为 CPU 最终浪费时间等待数据,无论是来自缓存还是主内存。
鉴于此,我想知道的是:
- 当程序写入分配给
calloc()
的以前从未访问过的页面,并且发生到新 CoW 页面的重新映射时,这是在执行内核代码吗? - 页错误写时复制功能是在 OS 级别还是 MMU 级别实现的?
- 如果我调用
calloc()
分配 4GiB 内存,然后在一个紧密循环中用某个任意值(比如0xFF
而不是0x00
)初始化它,我的 ( Intel) CPU 每次写入新页面时都会达到推测边界? - 最后,如果它是真实的,是否存在这种影响对实际性能有显着影响的情况?
你的前提是错误的。页面错误从来都不是流水线/超便宜的。崩溃(和 Spectre)缓解措施确实使它们变得更加昂贵,但是,以及系统调用和所有其他用户-> 内核转换。
跨越 kernel/user 边界的推测执行是不可能的;英特尔 CPUs 不重命名特权级别,即 kernel/user 转换总是需要完整的管道刷新。我认为您误解了 Meltdown:这纯粹是由于 user-space 和 delayed handling of the privilege checks on TLB hits.
中的推测执行造成的这在 CPU 设计中是通用的,AFAIK。我不知道有任何重命名特权级别或以其他方式推测内核代码、x86 或其他方式的微体系结构。
Meltdown 缓解措施增加的成本是进入内核会刷新 TLB。 (或者在支持 TLB 进程上下文 ID 的CPUs 上,内核可以使用 PCID 来使内核使用单独的页面-tables 与用户-space 便宜得多)。 =28=]
内核入口点(在 Linux 上)变成了交换页面 tables 并跳转到 real 内核入口点的蹦床,以避免将内核 ASLR 偏移量暴露给 user-space。但除此之外,还有一个额外的 mov cr3, reg
进入和退出内核(设置一个新页面 table),没有其他改变。
(Spectre 缓解也很棘手,需要更多更改,如 retpolines...并且还可能显着增加用户-> 内核-> 用户的成本。关于页面错误成本的 IDK。)
@BeeOnRope 报告(查看评论和他的回答以获得完整的详细信息)没有 Spectre 补丁,只是应用了 Meltdown 补丁,但是 nopti
"disable" 它的启动选项增加了往返成本到 Skylake CPU 上的内核(syscall
和伪造的 RAX,立即返回 -ENOSYS
)从 ~100 增加到 ~300 周期。所以这可能就是蹦床的成本? 并且在启用实际页面-table 隔离的情况下,它上升到约 700 个周期。那是 根本没有 幽灵缓解补丁。 (此外,这是 x86-64 syscall
入口点,而不是页面错误。不过它们可能相似。)
页面错误异常:
CPUs 不预测页面错误,所以他们无论如何都不能推测性地执行处理程序。页面错误入口点的预取或解码可能会在流水线刷新时发生,但是直到页面错误指令试图退出时该过程才会开始。错误 load/store 被标记为在退休时生效,并且不会重新引导前端; Meltdown 的全部关键是在故障负载报废之前不采取任何措施。
相关:When an interrupt occurs, what happens to instructions in the pipeline?
另外:
When a program writes to a never-before-accessed page which was allocated with
calloc()
, and the remapping to the new CoW page occurs, is this executing kernel code?
是的,页面错误由内核的页面错误处理程序处理。写时复制没有纯硬件处理。
If I call calloc() to allocate 4GiB of memory, then initialize it with some arbitrary value (say, 0xFF instead of 0x00) in a tight loop, is my (Intel) CPU going to be hitting a speculation boundary every time it writes to a new page?
是的。内核不会对归零页面进行故障处理(与页面缓存中数据热时的文件支持映射不同)。所以每一个新的页面被触及都会导致一个页面错误,即使对于小的 4k 普通页面也是如此。 (感谢@BeeOnRope 提供有关这方面的准确信息。)使用匿名大页面,每 2MiB (x86-64) 只会出现一次页面错误,这要好得多。
如果您想避免每页成本,请在 Linux 系统上分配 mmap(MAP_POPULATE)
以将所有页面预置到硬件页面 table 中。我不确定 madvise
是否可以为您预先设置页面,例如madvise(MADV_WILLNEED)
在已映射的区域上。但是 madvise(MADV_HUGEPAGE)
会鼓励内核使用匿名大页(并且可能会整理物理内存碎片以释放连续的 2M 块以启用它,如果你没有将它配置为在没有 madvise
的情况下这样做)。
相关:perf
结果。
是 使用 calloc()
分配的内存会因 Meltdown 和 Spectre 补丁而导致性能下降。
事实上,calloc()
在这里并不特殊:malloc()
、new
以及更普遍的所有分配的内存可能会受到大致相同的性能影响。 calloc()
和 malloc()
最终都由 OS 返回的页面支持(尽管分配器将在它们被释放后 re-use 它们)。唯一真正的区别是智能分配器,当它沿着使用来自 OS 的新页面的路径(而不是 re-using 以前的 free
d 分配)在 calloc
它可以省略归零,因为 OS-provided 页保证为零。除此之外,分配器行为在很大程度上是相同的,OS-level 归零行为也是相同的(通常没有选项要求 OS 获取 non-zero 页)。
所以性能影响比您想象的更广泛,但性能影响可能比您建议的要小,因为页面错误已经做了很多工作,所以您说的不是一个数量级退化或任何东西。有关性能影响可能有限的原因,请参阅 Peter's answer。我写这个答案主要是因为你的标题问题的答案仍然是 yes 因为有 some 影响。
为了估计对 malloc
繁重工作流程的影响,我尝试了 运行 一些分配和 page-fault 使用 Spectre 在当前内核 (4.13.0-39-generic
) 上进行繁重测试和崩溃缓解措施,以及在这些缓解措施之前的旧内核上。
测试代码很简单:
#include <stdlib.h>
#include <stdio.h>
#define SIZE (40 * 1024 * 1024)
#define PG_SIZE 4096
int main() {
char *mem = malloc(SIZE);
for (volatile char *p = mem; p < mem + SIZE; p += PG_SIZE) {
*p = 'z';
}
printf("pages touched: %d\npoitner value : %p\n", SIZE / PG_SIZE, mem);
}
较新内核的结果是每个页面错误大约 3700 个周期,而在没有缓解措施的旧内核上大约 3300 个周期。由于缓解措施导致的总体回归(大概)约为 14%。请注意,这是在 Skylake 硬件 (i7-6700HQ) 上,其中一些 Spectre 缓解措施的成本更低,并且内核支持 PCID,这使得 KPTI Meltdown 缓解措施更便宜。在不同的硬件上结果可能更差。
奇怪的是,在启动时禁用了 Spectre 和 Meltdown 缓解措施(使用 spectre_v2=off nopti
)的新内核的结果 比新内核默认值或旧内核,每个页面错误大约有 5050 个周期,与具有缓解措施 启用 的同一内核相比大约有 35% 的回归。所以当缓解措施被禁用时,确实出现了问题,performance-wise。
完整结果
这是两次运行的完整 perf stat
输出。
旧内核 (4.10.0-42)
pages touched: 10240
poitner value : 0x7f7d2561e010
Performance counter stats for './pagefaults':
12.980048 task-clock (msec) # 0.976 CPUs utilized
0 context-switches # 0.000 K/sec
0 cpu-migrations # 0.000 K/sec
10,286 page-faults # 0.792 M/sec
33,662,397 cycles # 2.593 GHz
27,230,864 instructions # 0.81 insn per cycle
4,535,443 branches # 349.417 M/sec
11,760 branch-misses # 0.26% of all branches
0.013293417 seconds time elapsed
新内核 (4.13.0-39)
pages touched: 10240
poitner value : 0x7f306ad69010
Performance counter stats for './pagefaults':
14.789615 task-clock (msec) # 0.966 CPUs utilized
8 context-switches # 0.541 K/sec
0 cpu-migrations # 0.000 K/sec
10,288 page-faults # 0.696 M/sec
38,318,595 cycles # 2.591 GHz
28,796,523 instructions # 0.75 insn per cycle
4,693,944 branches # 317.381 M/sec
26,853 branch-misses # 0.57% of all branches
0.015312764 seconds time elapsed
新内核 (4.13.0.-39) spectre_v2=关闭 nopti
pages touched: 10240
poitner value : 0x7ff079ede010
Performance counter stats for './pagefaults':
16.690621 task-clock (msec) # 0.982 CPUs utilized
0 context-switches # 0.000 K/sec
0 cpu-migrations # 0.000 K/sec
10,286 page-faults # 0.616 M/sec
51,964,080 cycles # 3.113 GHz
28,602,441 instructions # 0.55 insn per cycle
4,699,608 branches # 281.572 M/sec
25,064 branch-misses # 0.53% of all branches
0.017001581 seconds time elapsed