数据结构拼接(Data Structure Splicing, DSS)
DSS 是指通过合并或拆分数据结构、重排序字段、内联指针等操作来重新组织数据结构的技术。DSS 可以提高空间局部性(spatial locality),也就是说,当一起访问的数据字段在内存地址空间上紧密排列时,硬件缓存的利用率会提高,从而减少缓存未命中(cache miss)。
研究背景和现有问题
之前的许多 DSS 方法中,每一种都只针对了一两个具体的拼接优化(例如,只对数据结构进行拆分或字段重排序),且这些方法使用的底层抽象无法扩展以涵盖其他优化。
数据结构访问图(Data Structure Access Graph, D-SAG)
为了统一所有 DSS 优化,这项工作提出了一种新的抽象,叫做数据结构访问图(Data Structure Access Graph, D-SAG)。这个抽象有以下两个主要优势:
- 统一工具集:D-SAG 抽象能够覆盖所有之前提出的 DSS 优化,并且还能解锁新的优化机会。这意味着开发者可以使用单一的工具来进行所有 DSS 优化,而不需要依赖多个工具。
- 避免冲突:有时不同工具可能会提出互相冲突的建议,例如一个工具建议拆分数据结构,而另一个工具建议重排序字段。D-SAG 统一了所有优化建议,从而避免了这种冲突。
工具链和性能提升
基于 D-SAG 抽象,研究人员构建了一个工具链,利用静态分析和动态分析来向开发者推荐 DSS 优化。使用这个工具,他们在SPEC CPU2017 和 PARSEC 基准测试中找到了 10 个适合 DSS 的基准,还包括一个在 RocksDB 上运行的负载,该负载主要针对 RocksDB 的内存表。
通过遵循工具的建议对数据结构进行重构,程序的性能平均提高了 11%(几何平均值),同时缓存未命中率降低了 28%,这些结果来自七个不同的负载。
核心贡献
- 提出了一个统一的抽象模型 D-SAG,覆盖了所有的 DSS 优化,并提供了一种集成的方法来避免工具冲突。
- 基于 D-SAG 的工具链可以通过静态和动态分析,推荐数据结构拼接的优化方法,有效提升了程序的性能并降低了缓存未命中。
专业术语解释
- 数据结构拼接(Data Structure Splicing, DSS):通过合并或拆分数据结构、重排序字段、内联指针等操作来优化数据结构的技术。
- 空间局部性(spatial locality):指在程序运行过程中,如果某个内存地址被访问,那么与其相邻的地址也很可能在不久的将来被访问。良好的空间局部性意味着程序访问的内存位置在物理上是接近的,能更好地利用缓存。
- 缓存未命中(cache miss):当程序访问的数据不在缓存中时,需要从更慢的主存中读取数据,这种情况叫做缓存未命中,会降低性能。
- 数据结构访问图(Data Structure Access Graph, D-SAG):一种新的抽象模型,用于统一管理和应用各种数据结构拼接优化。
- 静态分析:在不执行程序的情况下,对代码进行分析以找到优化机会。
- 动态分析:在程序运行时,通过监控程序的行为来找到优化机会。
1. 硬件缓存与局部性
硬件缓存是计算机系统中用于加速内存访问的特殊存储层。它基于两种局部性的直觉来工作:
-
时间局部性(temporal locality):
- 指的是最近被访问的数据在短期内很可能会再次被访问。
- 例如,一个循环内的变量通常会被反复访问,这就是时间局部性的体现。
-
空间局部性(spatial locality):
- 指的是存储在相邻内存地址的数据很可能会在同一时间被访问。
- 比如,当程序访问一个数组元素时,通常会很快访问相邻的其他元素,因此把这些数据放在相邻的内存地址中会提高访问效率。
2. 空间局部性和缓存的工作方式
为了利用空间局部性,硬件缓存从内存中以**批量的方式(cache lines)来获取数据,而不是只获取需要的单个字节。一个缓存行(cache line)**通常包含 64–128 字节的数据。
- 如果程序的空间局部性很强,意味着在一次缓存行被加载进缓存后,该缓存行中的大部分数据都会被后续的访问使用,这使得缓存工作更加高效。
- 高效的缓存可以降低内存访问的延迟(即减少等待时间)并减少内存带宽的需求,从而提高程序的整体性能。
3. 现代应用中的空间局部性问题
然而,现代应用程序中的空间局部性往往较低。这意味着,当程序从内存中加载一个缓存行(64 字节)到缓存中时,实际使用到的数据可能只有其中的一部分。
- 引文提到,图1展示了不同基准测试的缓存行利用率,平均来看,缓存行中 64 字节的数据只有 40% 被实际访问。这意味着,剩余的 60% 的数据被浪费了,这降低了缓存的效率,增加了内存访问的延迟,影响了程序的性能。
专业术语解释
- 硬件缓存(hardware cache):一种快速的存储层,位于 CPU 和主存之间,用于加速数据的读取和写入。
- 时间局部性(temporal locality):程序中已经访问过的数据,在短时间内很可能会再次被访问。
- 空间局部性(spatial locality):程序在访问某个内存地址时,很可能会在不久的将来访问与之相邻的地址。
- 缓存行(cache line):缓存中以固定大小存储的数据块,通常为 64–128 字节,用于一次性加载相邻的内存数据。
- 内存访问延迟(memory access latency):程序从内存中读取数据所需要的等待时间。较高的缓存命中率可以降低这种延迟。
1. 改善空间局部性的方式
空间局部性可以通过以下两种方式来改善:
- 硬件改造:例如自适应缓存行粒度(adaptive cache line granularity)或子缓存行过滤(sub-cache-line filtering),这些方法都是通过改进缓存硬件来提高效率。然而,这些硬件解决方案目前并未被主要的商用处理器厂商广泛采用。
- 软件数据布局重组:由于硬件方案的限制,这项工作聚焦于通过重组应用程序的数据布局来改善空间局部性。
2. 数据结构的空间局部性挑战
编写具有良好空间局部性的程序是非常困难的,因为数据结构通常是根据语义来组织的,开发者很难直观地理解哪些字段会在相同的时间被访问。例如,一个结构体可能包含很多字段,但其中一些字段在使用时很少会一起被访问。
为了帮助开发者解决这一问题,之前的一些研究提出了各种工具来推荐(或自动执行)数据结构的变换,这些工具包括:
- 类拆分(Class Splitting):将“热字段”(即频繁访问的字段)与“冷字段”(即不常访问的字段)分开存储。
- 字段重排序(Field Reordering):将经常一起访问的字段在结构体中相邻存储,以提升缓存的利用效率。
- 指针内联(Pointer Inlining):将结构体中的指针替换为它所指向的数据,以避免因间接引用而破坏局部性。
这些技术统称为数据结构拼接(DSS),它们的核心思想都是通过重新组织类或数据结构的定义来提高空间局部性。
3. 统一抽象:数据结构访问图(D-SAG)
作者注意到,以前的 DSS 技术缺乏一个统一的抽象,这带来了几个问题:
- 不同工具分散:开发者需要使用不同的工具来进行不同的优化,例如一个工具用于类拆分,另一个工具用于字段重排序,还有一个工具用于指针内联。
- 优化建议冲突:不同工具可能提出互相冲突的建议,例如一个工具建议拆分数据结构,而另一个工具建议重排序字段,这些建议可能相互矛盾。
- 类似优化被忽略:一些类似的优化(例如类合并和字段迁移)在之前的研究中被忽略了。
为了统一这些优化,作者提出了一个数据结构访问图(Data Structure Access Graph, D-SAG)。D-SAG 是一个图模型,其中每个字段表示为一个节点,同时经常一起被访问的字段通过边连接。边的权重表示字段同时被访问的频率,权重越大,表示同时访问的频率越高。通过 D-SAG,开发者可以直观地看到哪些字段需要放在一起,从而优化数据布局。
4. D-SAG 工具链和性能提升
基于 D-SAG 抽象,作者开发了一套工具链,使用静态分析和动态分析来分析程序并推荐 DSS 优化。这些算法通过**图聚类(graph clustering)**技术对 D-SAG 进行分析,并向开发者推荐数据结构变更。
- 图 2 展示了一个来自 RocksDB 数据结构的 D-SAG 片段,其中较粗的边表示字段之间更频繁的同时访问。
- 图 3 显示了工具链为 RocksDB 数据结构推荐的新定义,例如将经常一起访问的字段放在同一个类中。应用这些优化后,RocksDB 的内存表压力测试中的运行时间减少了 20%。
为了验证概念,作者开发了一套工具链,基于 DINAMITE LLVM pass,可以自动生成 D-SAG 并推荐数据结构或类的修改。这个工具链对可以用 LLVM 3.5 编译的 C 和 C++ 程序有效,尤其适用于那些使用复杂类或数据结构的内存绑定(memory-bound)程序。
通过分析来自 SPEC CPU2017、PARSEC 和 RocksDB db_bench 的十个内存绑定基准,作者发现优化后的程序性能平均提高了 11%,缓存未命中率平均减少了 28%。
专业术语解释
- 空间局部性(spatial locality):程序在访问某个内存位置时,临近位置的数据也很可能被访问。良好的空间局部性可以提升缓存利用率。
- 数据结构拼接(Data Structure Splicing, DSS):通过合并、拆分、重排序等方式优化数据结构的存储,以提高性能。
- 类拆分(Class Splitting):将频繁访问和不常访问的字段分开,优化内存访问。
- 指针内联(Pointer Inlining):用直接的数据替换指针,以减少间接访问带来的性能损耗。
- 数据结构访问图(Data Structure Access Graph, D-SAG):一种图模型,用于表示数据结构中的字段访问关系,通过分析这些关系来指导数据布局优化。
- 图聚类(graph clustering):通过分析图中节点的连接关系,将频繁访问的字段分到一组,以优化存储布局。
- 内存绑定程序(memory-bound program):程序性能主要受内存访问速度限制,CPU 的计算能力往往不是瓶颈。
1. 共同抽象的需求(Requirements for a Common Abstraction)
为了理解 DSS 优化的有效抽象的需求,作者提供了两个示例:RocksDB 代码片段和canneal 代码片段。
-
RocksDB 示例(如图 2 所示):
- RocksDB 中的数据结构占据了内存表负载大约 85% 的缓存未命中(参见第 5 节)。
- 访问图显示字段
next_hash
和hash
经常被同时访问,但在当前的内存布局中,这两个字段被 六个其他字段分隔开,而这些字段并不是同时被访问的。 - 这些字段可能会跨多个缓存行,例如,即使
LRUHandle
被分配在一个 64 字节的缓存行边界上,next_hash
可能在第一个缓存行中,而hash
可能横跨第一个和第二个缓存行。 - 类似地,字段
next
、key_length
和deleter
也是经常一起访问的,但它们被其他字段隔开,虽然它们只占用 48 字节,但实际分配时它们可能横跨两个缓存行。因此,理想情况下,我们希望将next_hash
和hash
,以及next
、key_length
和deleter
放在内存中彼此更接近的位置。
要实现这样的优化,我们需要知道:
- 哪些字段被频繁访问。
- 哪些字段经常被同时访问。
-
canneal 示例(如图 4 所示,来自 PARSEC 基准测试):
- 原始代码中,类
netlist_elem
中的字段fanin
、fanout
和present_loc
经常同时被访问,而字段item_name
则很少被访问。 - 此外,
present_loc
被解引用后会访问其元素x
和y
。 - 为了改进缓存行利用率,我们需要将
item_name
从fanin
、fanout
中分离,并将x
和y
进行内联(即直接放在结构体中),并将所有四个字段在地址空间上放在一起。
- 原始代码中,类
由此可见,统一的抽象需要能够:
- 识别频繁和不常访问的字段。
- 捕捉同时访问,不仅是在同一数据结构内,也包括跨数据结构边界。
2. 访问亲和性(Access Affinity)
为了捕捉哪些字段是同时被访问的,以及它们之间的访问间隔有多近,作者引入了**Mattson 堆栈距离(stack distance)**的概念。
- **堆栈距离(stack distance)**是指在内存访问轨迹中,两次访问字段
u
和v
之间访问了多少个独特的数据元素。例如,在访问轨迹uababv
中,u
和v
之间的堆栈距离为 2。堆栈距离越短,说明两个字段在时间上被访问得越接近。 - 在优化空间局部性时,只有短堆栈距离是有意义的。如果两个字段之间的堆栈距离很长,即使它们在内存中相邻存储,缓存命中率也不会显著提高,因为缓存行可能在两次访问之间已经被逐出(evicted)。
- 因此,作者定义了一个亲和事件(affinity event),即如果两个字段
u
和v
之间的堆栈距离小于某个阈值t
,则认为发生了一个亲和事件。随后,两个字段之间的**访问亲和性(access affinity)**定义为内存轨迹中发生的亲和事件的次数。
通过定义访问亲和性,作者可以分析任意两个字段的关系,并判断它们是否应该被放在一起。
3. 数据结构访问图(D-SAG)
为了分析不同数据结构中字段之间的关系,作者构建了数据结构访问图(D-SAG)。
- D-SAG 是一个无向图,每个节点代表一个数据结构或类中的字段。每个节点包括一个计数器,表示该字段被访问的次数。
- 边的权重等于相关字段对之间的访问亲和性。例如,权重为 20 的边
u - v
表示字段u
和v
在低于阈值的堆栈距离内被访问了 20 次。 - 因为访问次数通常依赖于输入数据,因此 D-SAG 既与应用程序的具体数据结构定义相关,也与内存访问轨迹相关。
图 7 展示了从示例代码的内存访问轨迹中构建的 D-SAG,其中较粗的边表示更强的访问亲和性。
4. 算法和优化建议
基于 D-SAG,作者开发了一组算法,分析图中的节点和边,进而向程序员推荐数据结构的变更以提升空间局部性。算法的基本思路是利用**图聚类(graph clustering)**技术,将具有高访问亲和性的字段聚集在一起,从而优化它们在内存中的布局。
例如,在 RocksDB 示例中,图 3 显示了工具链为 RocksDB 数据结构推荐的变更,即将经常一起访问的特定字段放在同一个类中。应用这些优化后,RocksDB 的内存表压力测试中的运行时间减少了 20%。
总结
本部分描述了 D-SAG 抽象的要求和构建过程,以及它如何用于统一管理和优化数据结构拼接(DSS)中的各种优化方法。关键点包括:
- 访问亲和性(access affinity):通过分析内存访问轨迹,确定哪些字段经常一起被访问。
- D-SAG 抽象:通过构建数据结构访问图,将具有高访问亲和性的字段放在一起,以优化空间局部性。
- 算法和工具链:利用 D-SAG 进行分析,并推荐优化措施,例如类拆分、字段重排序和指针内联,以提升内存绑定程序的性能。
通俗说明:
研究背景和现有问题
之前的 DSS 方法通常是只对一个具体问题进行优化,比如只拆分数据结构或者只重排字段,而且这些方法都各自为政,没有一个通用的框架能把所有优化整合到一起。这就像你家里的每个房间有不同的整理方式,导致房间之间物品的摆放风格不一致,有时候甚至会互相冲突。
数据结构访问图(Data Structure Access Graph, D-SAG)
为了统一所有这些优化,研究人员提出了一个叫做**数据结构访问图(D-SAG)**的东西。它就像一个家居布置指南,可以指导你如何安排家里的物品,让它们放置得更加合理。
- 统一工具集:D-SAG 就像一个整理工具箱,里面有各种工具来帮助你优化摆放家里的东西,比如分类、重排序、移除不必要的东西等。通过这个工具箱,你可以用一个工具完成所有的优化,而不是为不同任务用不同的工具。
- 避免冲突:如果你用不同的工具,有的工具可能会让你把物品拆开,而另一个可能会建议你把它们合并,这样会产生冲突。D-SAG 就像一个全能的家庭整理专家,能同时考虑所有因素,避免这些冲突。
工具链和性能提升
基于 D-SAG,研究人员开发了一个工具,它可以分析代码并推荐 DSS 优化。就像一个家庭设计顾问,可以帮你重新安排家居布置,使得房间更加有序、更方便使用。
使用这个工具对程序进行优化后,程序的性能平均提升了 11%,而且缓存未命中率降低了 28%。这就像通过重新整理房间,让你在使用常用物品时,减少了时间浪费,增加了效率。
专业术语的通俗解释
- 数据结构拼接(DSS):重新整理数据,让常用的部分集中放在一起,方便高效访问。
- 空间局部性:类似于把常用物品放在一起,这样一次性可以取到需要的所有东西,而不需要来回走很多趟。
- 缓存未命中:相当于你需要某个物品,但找了半天在常用的抽屉里找不到,还得去另一个房间拿。这就叫未命中,浪费了时间。
- 数据结构访问图(D-SAG):一个用来指导如何更好摆放数据的模型,就像家庭布置指南,帮助你高效整理物品。
硬件缓存与局部性
硬件缓存就像是你放在手边的小篮子,用来装那些你最常用的东西。缓存有两个重要的原则:
- 时间局部性:如果某样东西最近被用过,那么很有可能还会被再次用到。比如你正在写字,那支笔很可能会被重复使用,所以放在手边。
- 空间局部性:如果你拿了一个物品,比如拿出一本笔记本,那么你很可能需要拿这本笔记本旁边的铅笔和橡皮。因此把这些相关的东西放在一起,能让你更容易拿到。
空间局部性和缓存的工作方式
为了提高空间局部性,缓存通常会一次性从内存中取一大块数据,而不是只取一个小片段。这就像你每次从书架上拿书时,顺便拿出旁边的几本相关的书,因为你很可能也需要它们。这样做能减少你来回书架取书的次数,提高效率。
改善空间局部性的方式
空间局部性可以通过以下两种方式来改善:
- 硬件改造:比如你可以换一个更大的篮子来装更多的物品,但这种改造成本高,并且不容易被广泛采用。
- 软件重组数据布局:通过更好地摆放你的物品来提高效率,比如把常用的东西放在更容易拿到的地方。这种方式就像重组数据结构,以提高它们在内存中的布局效率。
访问亲和性(Access Affinity)
访问亲和性是指哪些数据经常一起被访问,就像你发现每次煮饭时,盐和酱油几乎总是一起用到,所以你可以把它们放在厨房的同一个地方。为了判断哪些数据应该放在一起,研究人员引入了**堆栈距离(stack distance)**的概念:
- 堆栈距离可以理解为两样物品被用到之间,使用了多少其他的东西。比如你拿盐和酱油之间用了铲子和锅,那么堆栈距离就是 2。如果堆栈距离很短,说明这两样东西是紧密相关的,应该放在一起。
通过分析内存访问轨迹,作者定义了访问亲和性,用于确定哪些字段应该被放在一起,以提高数据访问的效率。
数据结构访问图(D-SAG)
**数据结构访问图(D-SAG)**类似于一个图表,用于展示哪些数据是经常一起被访问的。每个字段相当于图中的一个节点,而这些字段之间的关系就是图中的边。如果两个字段经常一起被访问,边的权重就会越大。
- **图聚类(Graph Clustering)**可以理解为把关系紧密的物品分组,就像你根据使用习惯把厨房用品放在一起,书房用品放在一起,以便更方便地使用它们。
数据结构访问图(D-SAG)如何用于优化数据结构,以提高空间局部性(spatial locality),并将优化过程分为三阶段流水线:类拆分与合并、字段内联、字段重排序。以下是对每个阶段的通俗解释。
3.4.1 阶段 1:类拆分与合并(Class Splitting and Merging)
在这个阶段,我们将数据结构中的类进行拆分和合并:
- 合并字段:将经常一起被访问的字段合并到同一个类中,这样当访问某个字段时,也可以把经常一起访问的字段一起加载到缓存中,减少缓存未命中。
- 拆分字段:将不经常一起被访问的字段拆分到不同的类中,这样访问一个字段时,不会把不必要的字段一起加载到缓存中,避免浪费缓存空间。
这种操作类似于整理物品的分类:
- 经常一起使用的物品(比如盐和酱油)放在同一个抽屉里,这样你在做饭时可以一次性拿到所有需要的调料。
- 而不常用的物品则被分开放在其他地方,以免占用主要抽屉的空间。
在实现这种优化时,作者使用了一种图聚类算法,它的目标是将图中互相联系紧密的节点分到同一个簇中,就像把经常一起用到的东西放在同一个抽屉里。图 8 中展示了从原始图 G0 到 G1 的转变,类 Large
被拆分成 Large.1
和 Large.2
,因为 large_b
和 large_d
不常用,而 Foo
和 Bar
类被合并,因为它们之间有很强的联系。
3.4.2 阶段 2:字段内联(Field Inlining)
字段内联是指将通过指针引用的子结构的字段合并到包含它们的结构中,如果这些字段和包含它们的其他字段经常被一起访问的话。内联的好处在于:
- 提高缓存命中率:将字段直接放入包含它们的结构中,意味着这些字段更有可能位于同一个缓存行中,而不是分散在不同的位置。
- 提高指令级并行性:因为内联后的字段不再依赖于需要先读取指针引用的数据,访问这些字段时可以更快地获取数据,即使父结构的访问是缓存未命中。
简单来说,字段内联就像是把一个需要钥匙才能打开的小盒子里的物品直接放到外面的抽屉里,这样每次你需要这些物品时,就不需要先找到盒子和钥匙了。
图 9 中展示了从 G1 到 G2 的转变,foo_bar_p
指针被移除,因为它指向的 Bar
类已经在第 1 阶段完全合并到 Foo
类中。
3.4.3 阶段 3:字段重排序(Field Reordering)
尽管第 1 阶段已经将高亲和性字段聚集在同一个数据结构中,但这并不一定意味着它们在内存中就紧邻在一起。特别是当内存分配不考虑缓存行边界时,字段之间可能仍然跨越多个缓存行,这会影响空间局部性。因此在这个阶段,我们需要对字段进行重新排序,使得具有最高访问亲和性的字段在内存中紧邻在一起。
这种操作类似于把抽屉里的物品按使用频率重新排列:
- 将最常一起使用的物品放在最容易拿到的地方,即使抽屉有可能被部分打开,常用的东西仍然可以很快地被找到。
为了实现字段重排序,作者开发了一种层次聚类算法的变体,按照边的权重(字段之间的访问亲和性)从大到小依次将节点配对并合并,同时保留它们在原始数据结构中的相对顺序。如果两个字段原本来自不同的数据结构,那么访问频率更高的字段会排在前面。
图 10 展示了从 G2 到 G3 的转变,在 Foo+Bar
簇中,foo_head
和 foo_tail
因为连接有最重的边而首先被合并,并保留了原始顺序,接下来是 bar_a
,因为 foo_tail
和 bar_a
之间的连接权重次重,依此类推。
总结
D-SAG 分析用于重组数据结构以提高空间局部性,优化过程分为三个阶段:
- 类拆分与合并:将高频访问的字段合并在一起,而将低频访问的字段分开,减少缓存浪费。
- 字段内联:将通过指针引用的子结构直接内联到父结构中,以减少间接访问,提升缓存命中率和指令并行性。
- 字段重排序:将高亲和性的字段在内存中排列在一起,以确保即使缓存行被随机分割,这些字段也能位于同一缓存行中,减少缓存未命中。
工具链概览
工具链接收一个原始程序作为输入,并给出**数据结构拼接(DSS)**的优化建议,就像在图 3 和图 5 中展示的那样。整个工作流程如图 12 所示,主要包含以下四个阶段:
- 阶段 1:确定程序是否为内存绑定程序,并识别导致大量缓存未命中的函数。
- 阶段 2:收集内存访问轨迹并进行静态分析。
- 阶段 3:构建数据结构访问图(D-SAG)并进行分析,提出数据结构拼接建议。
- 阶段 4:模拟优化建议的效果,以评估其对缓存利用的影响。
4.1 阶段 1:识别内存绑定的程序和函数
首先,工具链使用 Linux perf 工具对程序进行性能分析(profiling),判断程序是否是**内存绑定(memory-bound)**的,并找出那些导致大量缓存未命中的函数。内存绑定的程序意味着它的性能主要受到内存访问速度的限制,而不是 CPU 计算能力。
- 使用 **L1、L2 和 LLC(最后一级缓存)**未命中率作为判断标准:
- 如果程序的 L1 缓存未命中率低于 3%,且 LLC 缓存未命中率低于 1%,则认为该程序不需要优化。
- 如果某个函数的 L1 缓存未命中率高于 0.5% 或 LLC 缓存未命中率高于 0.2%,则选择该函数进行插装(instrumentation)。
通过识别内存绑定的函数,工具链可以只对这些函数进行插装,从而缩短内存访问轨迹,提升分析速度。
4.2 阶段 2:内存轨迹收集和静态分析
在这个阶段,工具链使用 DWARF 格式从编译后的二进制文件中提取数据结构定义,这需要在编译时加入 -g
选项,以便使这些调试信息可用。
- 使用 LLVM Clang 编译程序,并运行 DINAMITE 插装 pass,对在第 1 阶段选中的函数以及内存分配函数进行插装。
- 插装的目的是收集内存访问的轨迹,识别对象的分配情况、包含的字段,以及字段的访问情况。
通过这些信息,工具链可以知道内存中的哪些对象被分配了,包含哪些字段,以及这些字段何时被访问到。
4.3 阶段 3:D-SAG 构建和分析
在这个阶段,工具链通过解析内存访问轨迹来构建数据结构访问图(D-SAG)。
- 阴影堆(Shadow Heap):工具链设置一个阴影堆来检测新分配的对象,识别它们的字段访问情况,并检测亲和事件(affinity events)。
- 当遇到内存分配条目时,工具链会在阴影堆中为相应的对象分配内存并记录其类型。
- 当遇到对动态分配对象的内存访问时,工具链会在阴影堆中找到相应的对象,并通过字段偏移找到被访问的字段。
- 如果该字段是第一次被访问,则在 D-SAG 中创建一个新节点。然后检查最近访问的内存地址来检测亲和事件(即字段之间的访问在堆栈距离阈值内)。
- 亲和事件:如果两个字段在阈值内被访问,工具链会在相应的节点之间创建一条权重为 1 的边,或者如果边已经存在,则增加权重。作者尝试了不同的堆栈距离阈值,发现值为 10 时效果最好。
对于原始类型的分配(例如 int
或 double
),工具链将其视为只有一个字段的类,并为每个分配创建单独的“类”。如果这些单独的类之间具有高亲和性,D-SAG 会建议合并这些“类”,实质上是将两个数组合并,使它们的元素交替存储。
构建完成后的 D-SAG 会通过三阶段优化流水线进行分析,并生成优化建议(如图 3 和图 5 所示的文本文件)。
4.4 阶段 4:模拟建议的效果
为了评估 DSS 优化建议的潜在影响,工具链会重新安排原始内存访问轨迹,使其反映出优化建议下字段的新位置。然后使用修改后的 DineroIV 缓存模拟器对新轨迹进行模拟,评估这些建议是否有助于降低缓存未命中率和提高缓存行利用率。
通过这种方式,作者验证了模拟器产生的未命中率与应用优化后在实际硬件上的测量结果是相匹配的。因此,开发人员可以根据模拟器的输出决定是否值得投入时间和精力来修改实际代码。
总结
D-SAG 工具链是一个用于分析和优化程序中数据结构存储布局的工具,分为以下四个阶段:
- 识别内存绑定程序和函数:找出那些因内存访问导致性能瓶颈的程序和函数。
- 收集内存访问轨迹并进行静态分析:通过插装收集内存访问轨迹,并提取数据结构定义。
- 构建 D-SAG 并分析:利用内存访问轨迹构建 D-SAG,并通过三阶段优化流水线生成 DSS 优化建议。
- 模拟优化建议的效果:通过模拟优化后的内存访问轨迹,评估 DSS 优化对缓存利用的影响。
工具链概述
这个工具链的工作方式就像一个家居设计顾问,它会检查你家的物品摆放情况,找出不合理的地方,然后给出建议,帮助你重新安排家居布局,让你家里的东西更好地摆放,使用起来更加高效。
整个流程分为四个阶段,每个阶段都有特定的任务:
1. 阶段 1:找出问题区(内存绑定程序和函数)
- 工具链会先检查程序,看它是否存在“内存瓶颈”问题。可以理解为,顾问先来到你家,看看哪些房间或物品的摆放导致使用不便,找出那些最需要重新整理的地方。
- 如果发现某些地方(程序中的函数)总是找不到需要的东西(即缓存未命中率高),工具链就会集中精力对这些地方进行分析和优化,而不是整个房间都重新布置。
2. 阶段 2:收集使用习惯(内存轨迹收集和静态分析)
- 接下来,工具链会收集你是如何使用家里物品的数据(程序中的内存访问轨迹)。
- 这就像顾问观察你平常是如何取用和摆放家里的物品,记录下每样物品的位置,以及每次你去拿物品的过程。这些信息会用来了解物品的使用习惯,从而找到可以优化的地方。
3. 阶段 3:创建家居布局图(D-SAG)并进行分析
- 工具链通过收集的数据,构建出一个家居布局图(数据结构访问图,D-SAG),每样物品就是图中的一个点,物品之间的连接代表它们的“使用关系”。
- 这个图可以帮我们找到哪些物品是经常一起使用的,就像找出做饭时哪些调料经常搭配使用,哪些工具总是一起被取用。
- 然后通过分析这个布局图,工具链会给出一些建议,例如:
- 把经常一起用的物品放在同一个地方(类合并),这样你一次性可以取到所有需要的东西。
- 把不常用的东西分开存放(类拆分),减少拿取常用物品时的干扰。
- 把盒子里的小工具直接拿出来放到外面(字段内联),减少取用时需要开盒子的麻烦。
4. 阶段 4:模拟调整效果
- 最后,工具链会模拟一下重新布置后的效果,就像顾问用虚拟家居软件帮你看看重新整理后的房间会是什么样子,看看是不是使用起来更顺手。
- 如果发现重新布置后的房间更加高效,使用起来也更方便,那么这个工具链就会建议你去实际实施这些调整,改动程序中的代码来实现这些优化。
5.1 基准测试(Benchmarks)
基准测试是用于评估工具链性能的标准程序集,来自 SPEC CPU 2017 和 PARSEC,以及 RocksDB 的修改版本。
- 一些程序被排除在测试之外,例如:不兼容 LLVM-clang 3.5 的程序、过度使用模板类型导致静态分析无法进行的程序,以及使用自定义内存分配器的程序。这些限制是工具链实现上的问题,而不是方法本身的问题,也就是说,未来的改进版本可以解决这些挑战。
- 经过筛选,最终剩下 21 个应用程序,其中 11 个没有发现优化机会,因为它们已经有接近完美的缓存行利用率,或者它们的缓存未命中率很低,或者它们只使用了简单的类或数组(而不是复杂的数据结构)。
- 优化适用的程序列在第 1 组中,它们具有复杂的数据结构和内存瓶颈问题,工具链对这些程序进行了完整的分析。
通俗比喻:可以想象你有一个家庭整理顾问,他去观察你家的各个房间,筛选出那些需要改进的地方。如果某些地方已经整理得很整洁(例如东西都在合适的位置,使用起来很方便),顾问就不会再给这些房间提出建议。
5.2 方法论(Methodology)
实验是在一台配备 Intel Core i5-7600K 四核 CPU 的机器上进行的,每个核有 32KB L1 指令和数据缓存,256KB L2 缓存,6MB 共享 LLC。
工具链对第 1 组基准测试的程序进行了优化建议,并手动将这些建议应用到代码中。例如:
- 合并类:如果工具建议合并多个数组对应的类,代码中会将这些数组替换为包含合并后的类对象的一个数组。
- 拆分类:如果工具建议拆分类,则将指向该类的指针也相应拆分。
对于建议做大量改动的情况,我们只选择那些影响“热”数据结构的部分进行修改,即那些导致 至少 2% LLC 未命中的数据结构。
通俗比喻:这就像整理顾问给你家里出了一堆建议,但有的建议涉及大规模重新装修,于是你决定先挑出对日常生活影响最大的几项,逐步去实现。
5.3 性能(Performance)
工具链的性能评估包括优化前后程序的运行时间和缓存未命中率,同时使用缓存模拟器 DineroIV 评估缓存行利用率。
- 图 13 和图 14 展示了优化后的运行时间和缓存未命中率。对于 10 个基准测试中的 7 个,运行时间减少了最多 30%,平均减少 11%。未能改进的 3 个基准测试是因为它们的数据结构已经优化良好。
- 运行时间的改进主要来自于缓存未命中率的减少,例如对于 fluidanimate,虽然 L1 缓存未命中率略有增加,但 L2 和 LLC 未命中率显著减少,这对整体性能有正面影响。
通俗比喻:可以把运行时间的改进想象成你重新整理了厨房,让做饭的步骤变得更高效,减少了反复拿取工具的时间。虽然可能某些情况下还要多一步操作,但整体上更快了,因为很多原本浪费在寻找和拿取的时间被节省了。
5.4 案例研究 - canneal
canneal 是一个基准测试程序,其原始数据访问模式是:
- 随机选择一个大数组中的元素,这个元素属于
A
类。 - 读取该类对象的索引和指针字段,以访问属于
B
类和两个C
类的对象。
- 每次迭代需要随机访问 四个内存位置:一个
A
类对象,一个B
类对象,以及两个C
类对象。这导致大多数情况下会有 四次缓存未命中,因为这些对象在缓存中的空间不够容纳它们的全部内容。 - 图 18 展示了优化前的访问模式,
A
、B
和C
类对象的缓存行利用率分别为 56%、12.5% 和 25%,平均缓存行利用率只有 30%。
优化建议是将 B
类的字段移动到 A
类中,因为 B
类对象总是通过 A
类对象的索引字段访问。这样,优化后的访问模式减少了内存访问次数,从 四次减少到三次,理论上可以减少 25% 的缓存未命中,实际的改进也非常接近这个数值。
通俗比喻:原始模式就像你在厨房做饭时,每次要用一种酱料,都要跑到另一个房间去找,这样就非常低效。而优化后的模式则是把常用的调料都放在厨房里,不仅减少了来回跑的次数,而且让做饭更加顺手,提高了整体效率。