设为首页收藏本站

LUPA开源社区

 找回密码
 注册
文章 帖子 博客
LUPA开源社区 首页 业界资讯 技术文摘 查看内容

每个程序员都应当知道的编译器优化知识

2015-3-16 22:41| 发布者: joejoe0332| 查看: 3446| 评论: 0|原作者: alexxxx|来自: 伯乐在线

摘要: 高级编程语言提供的函数、条件语句和循环这样的抽象编程构造极大地提高了编程效率。然而,这也潜在地使性能显著下降成为了用高级编程语言写程序的一大劣势。在理想条件下,在不以性能为妥协的情况下,你应该写出易读 ...

  高级编程语言提供的函数、条件语句和循环这样的抽象编程构造极大地提高了编程效率。然而,这也潜在地使性能显著下降成为了用高级编程语言写程序的一大劣势。在理想条件下,在不以性能为妥协的情况下,你应该写出易读并且易维护的代码。因此,编译器尝试自动优化代码以提高其性能,当今的编译器都深谙其道。编译器可以转化循环、条件语句和递归函数、消除整块代码和利用目标指令集的优势让代码变得高效而简洁。所以对程序员来说,写出可读性高的代码要比因为手工优化而使代码变得神秘且难以维护更加可贵。事实上,手工优化的代码反而可能会让编译器难以进行额外和更加有效的优化。


  比起手工优化代码,你更应该考虑关于设计的各个方面,比如使用更快的算法,引入线程级并行机制和利用框架特性(比如move构造函数)。


  这篇文章是关于Visual C++ 编译器优化的。为了便于应用,我将会讨论编译器采取的最重要的优化技巧和决策。我的目的不是告诉你如何手工优化代码,而是向你展示为什么你可以信赖编译器来优化你写出的代码。这篇文件绝不是对Visual C++ 编译器优化工作的全面考察。但是将会给你展示那些你真正想要了解的优化工作和怎样与你的编译器沟通来应用它们。


  有一些重要的优化是超出所有现有编译器能力的——比如,用高效的算法代替低效的,或者改变数据结构的排列以优化其在内存中的布局。但是这些优化话题超出了本文的范围。


定义编译器优化

  优化工作涉及到的一个方面,是把一行代码转化成同等效果的另一行代码,在这个过程中提升它的一项或多项性能。最重要的两项性能(指标)是代码的执行速度和长度。其他一些特性包括代码执行开销,代码编译所需时间,如果代码需要通过即时编译机制(Just-in-Time (JIT))进行编译,那么JIT所需的编译时间也是指标之一。


  编译器经常会依据它们所使用的技术优化代码。虽然并不完美,但是比起花时间手工苦苦推敲一个程序,利用编译器提供的特有功能和让编译器来优化代码要高效得多。


  这里有4种方法让你的编译器更加高效地优化代码:

  1. 书写可读、高效的代码。不要把Visual C++ 面向对象的特性当作性能的敌人。最新版本的C++可以让这些开销保持到最低甚至消除这些开销。

2.使用编译器声明。例如让编译器使用比默认情况更快的函数调用约定。

3.使用编译器内置函数(compiler-intrinsic functions)。内在函数是其实现由编译器自动提供的特殊函数。编译器对其很熟悉并且会用极其高效的指令序列来代替函数调用,以充分利用目标指令集的优势。当前Microsoft .NET Framework不支持编译器内置函数,因此其下的语言都不支持。但是Visual C++ 对这一特性有外在支持。注意,虽然使用内置函数能够提升代码性能,但是会降低可读性和可移植性。

4. 使用性能分析引导优化(profile-guided optimization)。使用这一技术,可以让编译器搜集更多关于代码的运行时行为,并且以此来作为优化依据。

  本文的目的是通过证明编译器可以在低效但是可读性强的代码上应用优化(应用第一条方法),从而向你展示为什么你可以信任编译器。当然我也会提供一些对性能分析引导优化(profile-guided optimization)的简短说明,和提到一些可以微调代码的编译器声明。

  编译器有许多优化技巧,从像常量折叠这样简单的变换,直到像指令重排(instruction scheduling)这样极其复杂的变换。然而在这篇文章中我只有限地讨论了一些最重要的优化——那些可以显著地提升性能(两位数的百分数来衡量)和减少代码长度的优化:内联函数(function inlining)、COMDAT优化(COMDAT optimizations)和循环优化。我将会在下一部分讨论前两个话题,然后展示你如何控制Visual C++实现优化。最后会有.NET Framework优化的简略说明。通篇我都将会采用Visual Studio 2013来构建代码。


链接时代码生成

  链接时代码生成(LTCG)是一项应用在C/C++代码上的程序全局优化(WPO)技术。C/C++编译器独立地编译每个源文件然后产生出相应的目标文件。这意味着编译器只能在单个源文件上应用优化技术,而无法照顾到整个程序。但是,一些重要的优化却只能浏览全部程序后才能产生。所以你只能在链接时(link time)应用这些优化,而非编译时(compile time),因为链接器可以完整地看到程序。

  当LTGC被打开时(通过指定编译器开关/GL),编译器驱动程序(cl.exe)将只调用编译器前端(c1.dll or c1xx.dll),并把后端调用(c2.dll)推迟到链接时间。产出的目标文件包含通用中间语言(Common Intermediate Language——CIL)代码,而不是依赖机器的汇编代码。然后,当链接器(link.exe)被调用,它就能看到包含C中间语言的目标文件,并调用编译器后端,依次进行程序全局优化,生成二进制目标文件,再返回链接器把所有目标文件链接在一起,最后生成可执行文件。

  编译器前端实际上进行了一些优化,比如无论优化启用还是禁用,都会进行常量折叠。但是所有重要的优化工作都是在编译器后端进行的,并且可以使用编译器开关控制。

  链接时代码生成(LTCG)能让后端积极地执行许多优化(通过指定/GL与/O1或/O2,以及/Gw编译器开关,和/OPT:REF 与 /OPT:ICF链接器开关)。在本文中,讨论仅限于内联函数(function inlining)和COMDAT优化(COMDAT optimizations)。关于完整的链接时代码生成优化,请参考相关文档。注意链接器可以在本地目标文件,本地/托管混合目标文件,纯托管目标文件,安全托管目标文件和安全.net模块上执行链接时代码生成。

  我编写了一个包含两个源文件(source1.c 和 source2.c)和一个头文件(source2.h)的程序。source1.c 和 source2.c分别在Figure 1 and Figure 2中。由于头文件中非常简单地包含了source2.c中的函数原型, 所以并没有列出。


Figure 1 The source1.c File

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h> // scanf_s and printf.
#include "Source2.h"
int square(int x) { return x*x; }
main() {
  int n = 5, m;
  scanf_s("%d", &m);
  printf("The square of %d is %d.", n, square(n));
  printf("The square of %d is %d.", m, square(m));
  printf("The cube of %d is %d.", n, cube(n));
  printf("The sum of %d is %d.", n, sum(n));
  printf("The sum of cubes of %d is %d.", n, sumOfCubes(n));
  printf("The %dth prime number is %d.", n, getPrime(n));
}


Figure 2 The source2.c File

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#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函数。我将会利用这些函数证实被称作内联函数的优化,和一些其他的优化,其中内联函数这是编译器最重要的优化之一。

  我会在三种不同的配置下生成代码并且检验结果来验证代码是如何被编译器转化的。如果你也照做的话,你需要汇编生成文件(由编译器开关/FA[s]生成)来检验生成的汇编代码以及映像文件(由链接器开关/MAP生成)来检验初始化数据优化是否被执行(如果你指定了/verbose:icf 和 /verbose:ref开关,链接器也可以汇报这一项)。因此你需要确保在接下来的配置中指定了上述开关。我也会使用C编译器(/TC)以让生成的代码容易检验。但是这篇文章中所有我讨论的东西对于C++一样适用。



酷毙

雷人

鲜花

鸡蛋

漂亮
  • 快毕业了,没工作经验,
    找份工作好难啊?
    赶紧去人才芯片公司磨练吧!!

最新评论

关于LUPA|人才芯片工程|人才招聘|LUPA认证|LUPA教育|LUPA开源社区 ( 浙B2-20090187 浙公网安备 33010602006705号   

返回顶部