全球数据局部性-原因和方式

2020-06-08 03:09:58

#DEFINE FULY_SETINGING_DEFINE(_PROJECT,_NAME,_TYPE,_DEF,_DESC)\/*FastPath优化,参见Folly_Settings_Define_LOCAL_FUNC__中的说明。\将所有这些都聚合到单个部分中,以获得更好的TLB\和缓存局部性。*/\__attribute__((__section__(";.folly.settings.cache";)\std::atomic<;folly::settings::detail::SettingCore<;_Type>;*>;\FLOLY_SETTINGS_CACHE__##_PROJECT##_##_NAME;

将所有这些内容聚合到单个部分中,以实现更好的TLB和缓存局部性。

TLB代表转换后备缓冲器。当CPU需要加载存储在RAM中的某个值时,它只有虚拟内存的地址,需要将其转换为物理地址。只有到那时,计算机才知道物理48条地址线的哪个子集(假设x86-64架构)需要设置为高电压,其余的设置为低电压。

虚拟内存分为多个页面。虚拟地址中的高位表示页面ID。低位表示页面内特定字节的偏移量。TLB内置在存储空间有限的硬件中。典型的TLB具有4Ki个条目。TLB中的每个条目是从物理存储器的页ID到帧ID的映射。对于给定的虚拟地址,如果存在TLB条目,它将找到帧ID并获得物理地址。所有这些步骤都在硬件中完成。TLB只是存储在RAM中的页表的高速缓存,而不是像TLB那样的专用硬件存储。在TLB未命中时,CPU将遍历页表以填充TLB并重新启动进程。页表是进程特定的,每个进程在RAM中都有自己的页表。因此,有一个特殊的寄存器CR3,它存储负责CPU上当前运行进程的页表地址。CPU可以在没有操作系统参与的情况下进行页面遍历,这就是页表结构是特定于体系结构的原因。只有当出现页面错误时,操作系统才会介入,并且可以将数据交换到RAM或暂停程序。请注意,TLB不必是特定于进程的。通常在上下文切换时,TLB会被刷新,因此它只有当前运行进程的查找数据。但在较新的架构中也有类似PCID的解决方案。

x86具有两级页表结构。虚拟地址的第一部分标识第一级块(通用页面目录)。虚拟地址的第二部分标识目录内的内部页表条目。因为页表驻留在RAM中,所以页表查找具有访问RAM的所有开销。它需要在x86上进行两次RAM访问,与TLB命中相比,这需要多一个数量级的CPU周期。

当CPU从RAM加载例如uint64_t时,它从不只读取8个字节。最小的块,即公共CPU读取,将是64字节,即一条CPU高速缓存线。因此,CPU将从64的倍数地址开始读取64字节,该地址包含您感兴趣的数据。这意味着1)您希望将频繁读取的数据放在一起,物理上也要靠近虚拟地址空间中的数据;2)如果数据可以放入一条高速缓存线中,则希望避免数据跨越两条高速缓存线。这就是您看到例如__ATTRIBUTE__((aligned(16)用于结构的原因。

现在我们知道什么是TLB了。以及我们为什么要减少TLB未命中(因为页表查找速度慢了10倍)并提高高速缓存局部性。但是,我们如何才能编写具有较少TLB未命中的程序呢?对于堆和堆栈中的数据,通常的良好实践适用,例如避免追逐指针(也称为不使用链表,对齐结构以避免高速缓存线未命中)。但是,通常位于.text和.bss部分中的全局变量又如何呢?

这是变量的GCC属性。因此,所有声明了该属性的变量都将在最终可执行文件的同一节";.folly.settings.cache";中创建。因此,一旦加载到RAM中,它们在虚拟地址空间中的物理位置会更近。

objdump-D.Section--demangle--Section=.test.mySection-hSections:idx名称大小vma LMA文件关闭ALGN 21.test.mySection 00000008 00000000002b22e8 00000000002b22e8 000b22e8**3目录,ALLOC,LOAD,DATA.test.mySection:00000000002b22e8<;Section_data>;:.。

因此,链接器创建了一个名为";.test.mySection";的新节,它的大小正好是8个字节,用来保存我在程序中定义的uint64_t。当程序启动时,该部分被复制到RAM。