type
status
date
slug
summary
tags
icon
password
编译优化
编译优化涵盖了编译过程的过程间优化、循环优化、自动向量化、数据预取优化、浮点优化、反馈优化和链接时优化等,在不同的阶段实施不同的优化策略能尽可能的提高生成代码的执行效率。
1、 过程间优化
内联优化(在clang12和函数里面内联优化成功,clang13 14失败)
讲内联优化之前先讲一个概念叫做:内联函数。内联函数是一种编程语言的结构。就好像是在主函数里面调用其他的函数一样。普通的函数调用过程为:保护现场、转到被调函数执行、执行完毕返回调用处、恢复现场四个步骤。在这个完整的调用过程中会产生较大的时空开销(因为在频繁的调用被调用函数做运算)。所以为了减少时空开销,采用了内联优化的方法。
这个优化方法的思路是:调用函数在被调用函数处复制函数代码副本,并且通过代码膨胀将被调用函数的副本直接复制在调用函数处,同时被调过程内的形参也会变成实参。
但是是如何优化的呢?在这里有一些解释:C语言程序内存分为常量区、代码区、静态全局区、栈区、堆区。当我们的程序在运行的时,我们编译以后的二进制程序就会被放入到内存里面。函数代码段会被放入到代码区,局部变量和函数参数会被放到栈区,函数的调用就发生在栈区里面。在优化前:每次调用的时候会把和当前函数相关内容压入到栈中。然后,调用地址指向我们要执行的函数位置,开始处理函数内部的指令进行计算,当函数执行结束后,要弹出相关数据,处理栈内数据以及寄存器数。在优化后:消除了函数调用过程中所需的各种指令:包括在堆栈或寄存器中放置参数,调用函数指令,返回函数过程,获取返回值,从堆栈中删除参数并恢复寄存器等。
例如在函数前面放入关键字inline。
优化前代码:
汇编文件里面出现的结果就是这样的:

可以看到main函数和Add函数都分别的被进行了汇编。
优化后的代码:

可以看到在加了inline关键字之后,这个程序就被内联优化了。将Add函数添加到了main函数里面,而不会再编译一遍Add函数。
这样的话就减少了函数调用的入栈出栈开销。
2、 循环优化
常见的循环优化有:循环交换、循环展开、循环剥离、循环对齐、循环正规化、循环分布、循环反转、循环合并、循环分段、循环倾斜、循环多版本等。
① 循环展开(loop unrolling)
循环展开旨在:增加每一步loop的步长(假设原来是1:1->2->3……现在loop rolling为3的话;1->4->7……)在增加步长之后,在循环体内补充上所需要的操作,使其在前后功能完善不变的情况下减少循环次数,进而减少循环开销。优点是使得编译的速度变快,缺点是会增加代码的体积,最终会导致内存里面存储不下,导致指令的访问变慢。
这里有一个非常好的例子:
在上面的代码里面定义了三个函数,分别是loop_unroll1(步长为1)、loop_unroll2(步长为2)、loop_unroll3(步长为4)。
首先来分析一下loop_unroll1函数里面的内容:
定义了一个loop_unroll1函数,数组从a[0]开始到a[1000000]数组里面的每个值都+3,并且步长为1。在第一次循环的时候只能将a[0]里面的值+3,也就是说一次只能变化一个值。
看一下中间代码文件:

再来看一下loop_unroll2函数的内容:

定义了一个loop_unroll2函数,数组从a[0]开始到a[1000000]数组里面的每个值都+3,并且步长为2。但是在第一次循环的时候,loop_unroll2可以将a[0]和a[1]同时+3,这样就比loop_unroll1的效率高了一大截。
再看一下中间代码文件
很明显代码多了很多,因为是用空间换时间
再来看一下loop_unroll3函数
定义了一个loop_unroll3函数,数组从a[0]开始到a[1000000]数组里面的每个值都+3,并且步长为4。但是在第一次循环的时候,loop_unroll3可以将a[0]、a[1]、a[2]和a[3]同时+3,这样应该就比loop_unroll2的效率高了一大截(但是事实并没有高一大截)。
因为有两个指令进行优化(-funroll-loops、-O1指令),所以打算用三个表格来展示结果。
结果1(未使用任何优化指令)
命令:clang loop_unrolling.c -o loop_unrolling_original
函数 | 结果 |
Loop_unroll1 | 12063 |
Loop_unroll2 | 1856 |
Loop_unroll3 | 1657 |

结果2(使用-funroll-loops指令)
命令:clang loop_unrolling.c -funroll-loops -o loop_unrolling
函数 | 结果 |
Loop_unroll1 | 9878 |
Loop_unroll2 | 1967 |
Loop_unroll3 | 2307 |

结果3(使用-O1指令)
命令:clang loop_unrolling.c -O1 -o loop_unrolling_O1
命令:clang loop_unrolling.c -O1 -o loop_unrolling_O1
函数 | 结果 |
Loop_unroll1 | 10020 |
Loop_unroll2 | 699 |
Loop_unroll3 | 563 |

结论:不论是什么优化指令产开次数为2相对于未展开时,性能有明显提升,但展开次数为4时,性能相对于展开次数为2并没有多少提升(查了一下,对于循环展开而言,如果展开过度,就会导致运算器/寄存器不够用,这些数据和指令就被迫掉入L1 cache,cache的大小不够用,就会继续掉入内存。对于存储层级而言,每次数据被迫掉入下一个存储阶层,带宽大概率要减半或者更多了)
② 循环分布(loop distribute)
循环分布就是将循环内的一条或者多条不存在依赖关系的语句移到一个单独循环中。像这样:
为什么循环分布(loop distribute)可以对程序的性能进行优化呢?
答:循环分布将一个串行循环转变成多个并行循环。每个循环都有与原循环相同的迭代空间。空间变多了,循环变并行了优化的性能自然也就上升了。
这里有一个非常好的例子:
因为有在编译器中有选项-mllvm -enable-loop-distribute可以进行优化,所以在这里还是选择有编译优化选项和无编译优化选项进行编译。
结果1 (未使用任何优化指令)
命令:clang loop_distribute.c -o loop_distribute

优化前的loop_distribute1函数的中间文件为:

结果2 (使用-O1 -mllvm -enable-loop-distribute优化指令)
命令:clang loop_distribute.c -O1 -mllvm -enable-loop-distribute -o loop_distribute_optimization

优化后的loop_distribute2函数中间代码为:

③ 循环剥离
循环剥离本身不会起到什么特别厉害的作用,但是这个操作会对向量化等其他优化操作产生影响。
循环剥离常用于数据首地址不对齐的引用,以及循环末尾不够装载到一个向量寄存器的数据剥离出来,使剩余数据满足向量化对齐性需求。
- 作者:JucanaYu
- 链接:https://jucanayu.top/article/0fe3bd0a-eb5a-4a08-8c6e-ea2d7e069278
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。