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 最终浪费时间等待数据,无论是来自缓存还是主内存。

鉴于此,我想知道的是:

你的前提是错误的。页面错误从来都不是流水线/超便宜的。崩溃(和 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 以前的 freed 分配)在 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