性能优化特性之:LTO

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 本文介绍了倚天实例上的编译优化特性:LTO,并从优化原理、使用方法进行了详细阐述。

优化原理

LTO,顾名思义,就是在链接的时候,继续做一系列的优化。又由于编译器的优化器是以IR为输入,因此编译器在链接时优化的编译过程中将每个源文件生成的不再是真正的二进制文件,而是"LTO对象文件"。这些文件中存储的不是二进制代码,而是编译器生成的IR。 链接时,编译器会将所有"LTO对象文件"合并到一起,合并后生成的巨大IR文件包含了所有源代码信息,这样就能跨文件对所有源代码(合并后的IR文件)进行集中的分析和优化,并生成一个真正的二进制文件,链接并得到可执行文件。因为编译器能够一次性看到所有源代码的信息,所以它做的分析具有更高的确定性和准确性,能够实施的优化也要多得多。 但在使用LTO优化大型的应用程序的时候,“链接”阶段将所有"LTO对象文件"合并成一个IR文件并生成的单个二进制文件可能非常庞大,这给系统虚拟内存造成很大压力,导致频繁内存换页,最终影响编译、链接的效率,甚至出现out of memory错误。 实际上,GCC编译器在LTO阶段首先会将所有文件中的符号表(symbol table)信息和调用图(call graph)信息合并到一起,这样不仅能跨文件对所有的函数进行全局的分析,而且内存压力也较小。WPA(whole program analysis, 全局分析)阶段会确定具体的优化策略,并依据优化策略将所有的"LTO对象文件"合并成一或多个IR文件。 这些IR文件再次并发经过优化器优化后,生成真正的二进制对象文件并传递给真正的链接器进行链接,最后得到可执行文件。

使用方法

举例说明如何使用LTO来进行编译优化:

  • 编写了一个包含两个源文件(source1.c 和 source2.c)和一个头文件(source2.h)的程序。由于头文件中非常简单地包含了source2.c中的函数原型, 所以并没有列出。
/* source1.c */
#include <stdio.h> // scanf_s and printf.
#include "source2.h"
int square(int x) { return x*x; }
int main() {
  int n = 5, m;
  scanf("%d", &m);
  printf("The square of %d is %d.\n", n, square(n));
  printf("The square of %d is %d.\n", m, square(m));
  printf("The cube of %d is %d.\n", n, cube(n));
  printf("The sum of %d is %d.\n", n, sum(n));
  printf("The sum of cubes of %d is %d.\n", n, sumOfCubes(n));
  printf("The %dth prime number is %d.\n", n, getPrime(n));
}
/* source2.c */
#include <math.h> // sqrt.
#include <stdbool.h> // bool, true and false.
#include "source2.h"
int cube(int x) { return x*x*x; }
int sum(int x) {
  int result = 0;
  for (int i = 1; i <= x; ++i) result += i;
  return result;
}
int sumOfCubes(int x) {
  int result = 0;
  for (int i = 1; i <= x; ++i) result += cube(i);
  return result;
}
static
bool isPrime(int x) {
  for (int i = 2; i <= (int)sqrt(x); ++i) {
    if (x % i == 0) return false;
  }
  return true;
}
int getPrime(int x) {
  int count = 0;
  int candidate = 2;
  while (count != x) {
    if (isPrime(candidate))
      ++count;
  }
  return candidate;
}

source1.c文件包含两个函数,有一个参数并返回这个参数的平方的square函数,以及程序的main函数。main函数调用source2.c中除了isPrime之外的所有函数。source2.c有5个函数。cube返回一个数的三次方;sum函数返回从1到给定数的和;sumOfCubes返回1到给定数的三次方的和;isPrime用于判断一个数是否是质数;getPrime函数返回第x个质数。笔者省略掉了容错处理因为那并非本文的重点。 这些代码简单但是很有用。其中一些函数只进行简单的运算,一些需要简单的循环。getPrime是当中最复杂的函数,包含一个while循环且在循环内部调用了也包含一个循环的isPrime函数。通过这些函数,我们将在LTO下看到函数内联,常量折叠等编译器最重要,最常见的优化。

  • 编译命令(gcc版本是9.0.1)
# Non-LTO:
gcc source1.c source2.c -O2 -o source-nolto
# LTO:
gcc source1.c source2.c -flto -O2 -o source-lto
  • 检查source-nolto的汇编
1. 可以发现square被main函数inline了,对于第一次square(n)的调用,由于n在编译时刻可知,所以代码
   被常数25替代,对于square的第二次调用square(m),由于m的值在编译时是未知的,所以编译器不能对
   计算估值。
2. 可以发现isPrime被getPrime函数inline了,这是由于candidate有初始值,inline后可以做常量折叠;
   另外一方面,isPrime是static函数,且只有getPrime调用了它,inline之后,可以将这个函数当作dead
   code删除,减小代码体积。
  • 检查source-lto的汇编
1. 可以看到除了scanf之外的所有函数都被作为了内联函数,因为在GCC LTO的WPA阶段, 这些函数的定义都是
   可见的。并且,由n编译时刻已知,编译器在编译时刻就会完成函数square(n), cube(n),sum(n)和
   sumOfCubes(n)的计算。
2. LTO获得了30%以上的性能提升
  • 结论
1. 某些优化,当其作用于整个程序级别时,往往比其作用于局部时更加有效,函数内联就是这种类型的优化之一。
2. 事实上,大多数优化都在全局范围内都更加有效,这也就是我们编译大型应用程序时,推荐使用LTO的原因和理由。
3. FireFox、Chrome、 Mysql等大型应用使用LTO build都获得了5%-15%不等的性能提升




---------------------------------------------------------------------------------------

更多调优信息,请参考:

龙蜥社区:https://openanolis.cn/

KeenTune SIG:https://openanolis.cn/sig/KeenTune

阿里云龙蜥操作系统专区:https://developer.aliyun.com/group/aliyun_linux

相关文章
|
5月前
|
SQL 缓存 Java
系统性能优化总结
系统性能优化总结
83 10
|
编译器 C++ Anolis
性能优化特性之:PGO
本文介绍了倚天实例上的编译优化特性:PGO,并从优化原理、使用方法进行了详细阐述。
|
存储 缓存 NoSQL
性能优化方案及思考
周末闲暇在家,朋友让我帮忙优化一个接口,这个接口之前每次加载都需要40s左右,经过优化将性能提了10倍左右;又加了缓存直接接口响应目前为300ms左右,于是将自己的优化思路整理总结一下
|
Web App开发 SQL 缓存
性能优化
性能优化 前言 以前写过一篇性能优化的笔记前端性能优化小结,那时候算是列了一些优化的点,最近又读了几篇性能优化相关的文章,加上自己动手做了一些实践,相比之前有了更深一点的理解
|
SQL 缓存 NoSQL
服务性能优化总结
服务性能优化总结
136 0
|
Android开发 芯片 UED
初识性能优化
性能优化一词相信大家都经常听到,今天我们就简单的来认识以下性能优化,了解做性能优化的必要性以及优化的分类。
初识性能优化
|
并行计算 程序员 Linux
C++服务性能优化的道与术-道篇:阿姆达尔定律
在之前的文章 《2004:当CPU温和地走入那个良夜》 中我讲到了2000年后摩尔定律的终结,CPU时钟频率定格,多核成为CPU发展的新方向,并行计算成为趋势。
254 0
C++服务性能优化的道与术-道篇:阿姆达尔定律