CPU的性能,是这样被编译器压榨的!

Posted 程序芯世界

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了CPU的性能,是这样被编译器压榨的!相关的知识,希望对你有一定的参考价值。

1.一个例子

先来看一段非常简单的伪代码:

   a = 0;
L1: b = a + 1;
c = c + b;
a = b * 2;
if a < 9 goto L1;
return c;
CPU的性能,是这样被编译器压榨的!
CPU的性能,是这样被编译器压榨的!
CPU的性能,是这样被编译器压榨的!
CPU的性能,是这样被编译器压榨的!
CPU的性能,是这样被编译器压榨的!
CPU的性能,是这样被编译器压榨的!

2. 活跃变量分析

2.1 活跃变量的定义

如果程序中的变量在某个点之后会被使用,则该变量在该点是活跃的。

1   a = 0;
2 L1: b = a + 1;
3 c = c + b;
4 a = b * 2;
5 if a < 9 goto L1;
6 return c;

定义比较抽象,看完下面的例子就明白定义的具体含义了。

  • 活跃变量定义举例

在上图的代码中,变量c在第4行是活跃的,因为变量c在第4行之后还会继续使用(第6行会使用变量c),这就是我们所说的活跃变量。

2.2 活跃变量分析的用途

只要两个变量不会同时活跃,那么这两个变量就可以共用同一个寄存器,不用担心寄存器的内容被覆盖的问题。

也就是说当一个寄存器中的变量在后续不再活跃,我们可以重新分配该寄存器给其他变量。

知道了变量的活跃信息之后,就可以重复利用CPU中的寄存器啦。

看完这篇文章你就会知道最后的谜底,知道这段小程序最少需要几个寄存器。

2.3 符号及说明

  • in[b]表示在基本块b入口处活跃的变量集合。
  • out[b]表示在基本块b出口处活跃的变量集合。
  • use[b]表示在基本块b使用,但是使用前在基本块b中没有定义的变量集合。
  • def[b]表示在基本块中被定义的变量集合。
  • succ[b]表示基本块b的后继基本块。

2.4 计算方程与伪代码

  • 如果一个变量在基本块b中使用,则该变量在基本块b的入口处是活跃的。

  • 如果一个变量在基本块b的入口处是活跃的,在该变量在基本块b的所有前驱节点的出口是活跃的。

  • 如果一个变量在基本块b的出口处是活跃的,且该变量在基本块b中没有定义,则该变量在基本块b的入口处是活跃的。

用公式表示就是:

(公式1)

 (公式2)

伪代码如下:CPU的性能,是这样被编译器压榨的!

伪代码用一句话概括就是:从后向前计算每个节点的in和out值,然后不断的循环,直到当前循环和上一次循环的所有节点in和out值一致时结束循环。

2.5 实例

公式和代码可能比较抽象,下面来看一个具体实例。

控制流图,是用在编译器中的一个抽象数据结构,它用图的形式表示一个过程内所有基本块执行的可能流向, 也能反映一个程序的实时执行过程。为简单起见,这里将每一条语句作为一个节点。

还是文章最初的那段代码,放在下面方便回看。

1   a = 0;
2 L1: b = a + 1;
3 c = c + b;
4 a = b * 2;
5 if a < 9 goto L1;
6 return c;

上面代码对应的控制流图如下:CPU的性能,是这样被编译器压榨的!

下表是根据伪代码和公式1、公式2计算的控制流图中每个节点的in和out值。

CPU的性能,是这样被编译器压榨的!
每一遍迭代后的结果

以节点4的第一次迭代为例,表格中其它节点的计算原理与节点4一致。文中所提到的公式1与公式2,是2.4节中已经标明的计算in与out的两个公式。

  1. use[4]和def[4]的计算:
  • use[4] = {b}

因为第四行的等式右边使用了b。

  • def[4] = {a}

因为第四行对变量a进行了赋值,也就是对变量a进行了定义。

需要注意的是活跃变量分析要获得将来的信息,因此是从后向前的一个过程,也就从节点6向节点1的一个过程。

  1. in[4]与out[4]的计算:

从控制流图中可知,4的后继节点为5,所以有:

  • succ[4]={5}

代入到out的计算公式(公式2)里面:

  • out[4] =  in[s]

前面已经计算出succ[4] = {5},代入到上式可得:

  • out[4] = in[5] = {ac}

对于in[4]的计算,直接带入到in的计算方程(公式1)中可得:

  • in[4] = use[4] (out[4] - def[4])

其中use[4],out[4]和def[4]已知,因此就可以求出in[4]了。

当然这只是第一遍迭代的结果,还需要继续迭代直到前一次计算的in与out与本次in与out的结果一致,此时算法收敛,迭代结束。

可以看到表格中第三遍迭代的结果与第二遍迭代的结果一致,因此程序迭代到第三次后结束。

3. 最终答案

再来回顾一下这段代码

1   a = 0;
2 L1: b = a + 1;
3 c = c + b;
4 a = b * 2;
5 if a < 9 goto L1;
6 return c;
  • 变量a的活跃范围是:

1->2 和 4->5->2

  • 变量b的活跃范围是:

2->3->4

  • 变量c的活跃范围是:

1->2->3->4->5->2和5->6

因此最初问题的答案如下:

只需要2个寄存器就可以了,因为变量a与变量b的活跃范围不会冲突,可以分配一个寄存器,变量c分配一个寄存器,整段程序一共只需要两个寄存器。


所以现在我们就知道编译器是如何压榨CPU的啦,当然编译器还有很多算法去高效的利用CPU的硬件资

本文仅阐述了数据流分析中的活跃变量分析,这是寄存器分配的基础。后续还会写一篇关于图着色算法进行寄存器分配的文章,结合两篇文章就可以了解我们的编译器是如何分配CPU中的寄存器了。

4.总结

文章介绍了数据流分析中活跃变量计算的迭代算法。活跃变量计算是寄存器分配的基础,后续还会写一篇图着色算法在分配寄存器时的应用,两个算法结合在一起才构成了完整的寄存器分配。

寄存器的高效利用只是编译器后端优化中的一种,减少程序中的冗余计算也是编译器要做的事情。

实际上不管什么语言,程序中总会存在一些程序员容易忽略的冗余计算。编译器后端在做优化时会优化掉这些冗余计算。比如公共子表达式删除、复写传播、死代码删除、循环不变量外提等。

5.往期文章推荐

该文章涵盖了三个方向:计算机图形学,人工智能、GPU体系结构。这三个方向也是连续三届图灵奖授予的领域。

CPU的性能,是这样被编译器压榨的!

关于作者

为了保证文章的质量,更新的频率会低一些,但会坚持下去的,敬请期待。


以上是关于CPU的性能,是这样被编译器压榨的!的主要内容,如果未能解决你的问题,请参考以下文章

简单!代码原来是这样被CPU跑起来的

简单!代码原来是这样被CPU跑起来的

一道面试题目

CLR加载程序集代码时,JIT编译器对性能的产生的影响

Windows 程序启动性能优化(先载入EXE,后载入DLL,只取有限的代码载入内存,将CPU的IP指向程序的入口点)

多线程与并发编程