随着LLVM 12.0.0的即将发布,32位ARM目标已获得机器外侧型代码大小优化的全部支持,用于ARM和Thumb-2指令集。通过此优化提供的预期代码大小增益平均约为5%(您可以跳转到结果部分以获取更多详细信息)。默认情况下未打开(请参阅如何使用IT部分),但我们的目标是使其在LLVM 13.0.0内的所有ARM核心下启用它。
函数概述是一个编译过程,它包括用呼叫替换连续语句的块,调用包含这些语句的新函数。简而言之,它是众所周知的内联优化的反比力。它用于不同的编译领域,以实现源代码重构或内核提取的各种目标,源代码编译器,缩小了大功能以减少JIT编译器中的编译时间,或通过分离函数的热和冷区域和执行性能改进[1]中提出的部分内向。
如上所述,机器外侧目标是代码大小减少,接近链路时间的相同代码(ICF)正在进行[2]。它是在优化管道的最后一步,在代码发射之前的最后一步(代码选择,寄存器分配,指令调度等)在LLVM机器特定的中间表示(AKA MIR)上运行在LLVM机器特定的中间表示(AKA MIR)上运行的进一步优化(即未绑定到功能边界)。已经完成了)。
在为此C代码生成的ARM组件中,我们可以看到(在左侧),突出显示的线条< 3,4,5>,< 9,10,11>和< 15,16,17>完全相同,因此候选人概述。机器oudLiner将识别此冗余,将代码提取为新功能,然后通过对此功能的调用替换它,如下所示:
机器概述优化通行证最初由2016年从Apple的Jessica Paquette开发[3],并在LLVM开发人员会议上呈现[4]。它主要为AARCH64开发(对于X86_64的最小支持),首先在LLVM 5.0.0释放中提供。它后来延长了RISC-V目标,并在2019年的LLVM 10.0.0发布中延伸。对于32位ARM,我们在LLVM 11.0.0中提供了初始版本,我们继续改进它以便提供在LLVM 12.0.0中完全支持。
这是通过步行完成程序的所有基本块来完成的,以找到MIR指令的最长重复序列,这可以减少到最长的公共子字符串问题[5],其中基本块是字符串,以及指令字符。本类可以使用通用后缀树表示有效地解决问题[6]。
在下面的示例中,两个函数calc_1和calc_2可以分别由字符串ABABC和AABC表示。填充具有唯一终止符(#和$)后填充这些字符串之后的广义后缀树。该树的内部节点的深度表示候选的长度和从中到达的叶节点的数量,重复的次数。在我们的示例中寻找具有最小两个长度的重复的子字符串将为我们提供两次重复的BC,AB重复三次,ABC重复两次。
现在我们有一个候选人列表,我们必须小心概述这些代码不会破坏程序行为,实际将减少其大小。实际上并非所有指示都可以安全地提取。条件分支是无法安全概述的指令或序列的一部分,就像操作数是常量池的索引或例如常用池或常青或者序列包含用于计算偏移位置独立的代码的标签(PIC)模式等......因此,从列表中删除此类候选者。请参阅以下略微修改示例:
我们有两条候选人在线&lt 2,3,4>和< 10,11,12>和在线< 6,7>和< 14,15>一旦概述,这会给下面的代码会破坏。实际上,在线14上概述的返回指令是预测的,并且仅执行R0较低或等于R1,这意味着如果在第2行中调用outcleined_function_0时,程序不会重新开始执行减法在第3行,因为它应该这样做,但是在第17行上执行并执行乘法,这不是程序的正确行为。
让我们继续使用我们的例子,现在已经删除了不安全的候选者,我们只有两个指令从两个呼叫站点丢弃到一个函数中,我们的二进制文件的大小是28字节(12个指令4个字节:5在Calc_1,5中的12个指令Calc_2和2中的outcleind_function_1),它与未在不概述的文件中获得的文件相同,因此在这种情况下没有点钟。为了保证概述候选者时代码大小减少,我们需要检查此不平等是否为真,否则删除候选者:
其中:n是候选事项的数量cs是候选co的字节字节的大小是呼叫站点的字节的开销(添加的指令)是概述函数中的字节的开销(添加的指令)
一旦我们有一个安全的候选人列表,它仍然是通过创建新功能并通过调用替换每个候选人来实际转换代码。但是,鉴于撰写候选人的指示的性质,或他们上下文,它并不总是直接的,就像我们在前面的例子中看到的那样直截了当。
在CALC_1中,概述区域不是尾呼叫或返回指令此时间,因此需要插入一个(第14行),并且使用链路(BL)的分支来调用概述函数(这将节省返回地址进入链接寄存器LR)。它是CALC_2的相同的事情,但还需要在呼叫(第2行和4)周围保存和恢复LR以保留第6行中使用的返回地址,可以通过使用备用寄存器来完成(如R4在我们的情况下)或者如果没有可用的话,通过将其推在堆栈上。最后一个例例增加了另一个约束,因为CALC_3中概述的区域包含对另一个函数(第15行)的呼叫,需要保存并恢复(第9行和17行)以跳回正确的地址。当它在堆栈顶部完成时,必须相应地改变访问它的指令的偏移(第12行)。
默认情况下,机器outliner通过默认情况下启用了用于32位ARM的AARCH64和M-Profile Cores的攻击性代码大小优化级别-OZ,但也可以用-moutline / -mno-utline标志手动调用或禁用它。
通过使用标志-rpass = machine-utliner使用LLVM备注,还可以获得有关通过的转换所做的转换的信息,例如在我们的第一个示例中它将给出:
正如我们所看到的,机器概述始终是代码大小优化的双赢,在最坏的情况下,您的代码根本不会被触摸,但平均预期的代码大小减少了现有的攻击代码大小优化级别 - ARM模式为〜5%,拇指-2为〜4%。如果我们查看诸如SPEM CPU 2017等基准套件,我们会看到我们在大型基准上获得最佳结果(例如,帕拉斯特最多14%),这是预期的,因为有可能在a中找到重复的指令序列例如,大的代码基础比微小的调谐数学库。与链路时间优化(LTO)结合在整个程序上运行而不是每个文件,并且已经提供了一些非常好的结果时,它也是非常有益的。例如,当我们可以在Blender(-14%和Outliner中的-23%)或GCC(LTO和-18.7%的LTO和-18.7%,与outliner中的-23%和-18.7%)进行进一步。