上一讲,在讲 CPU 的性能时,我们提到了这样一个公式:
程序的 CPU 执行时间 = 指令数×CPI×Clock Cycle Time
这么来看,如果要提升计算机的性能,我们可以从指令数、CPI 以及 CPU 主频这三个地方入手。要搞定指令数或者 CPI,乍一看都不太容易。于是,研发 CPU 的硬件工程师们,从 80 年代开始,就挑上了 CPU 这个“软柿子”。在 CPU 上多放一点晶体管,不断提升 CPU 的时钟频率,这样就能让 CPU 变得更快,程序的执行时间就会缩短。
增加晶体管可以增加硬件能够支持的指令数量,增加数字通路的位数,以及利用好电路天然的并行性,从硬件层面更快地实现特定的指令,所以增加晶体管也是常见的提升cpu性能的一种手段。
然而,计算机科学界从来不相信“大力出奇迹”。奔腾 4 的 CPU 主频从来没有达到过 10GHz,最终它的主频上限定格在 3.8GHz。这还不是最糟的,更糟糕的事情是,大家发现,奔腾 4 的主频虽然高,但是它的实际性能却配不上同样的主频。想要用在笔记本上的奔腾 4 2.4GHz 处理器,其性能只和基于奔腾 3 架构的奔腾 M 1.6GHz 处理器差不多。
于是,这一次的“大力出悲剧”,不仅让 Intel 的对手 AMD 获得了喘息之机,更是代表着“主频时代”的终结。后面几代 Intel CPU 主频不但没有上升,反而下降了。到如今,2019 年的最高配置 Intel i9 CPU,主频也只不过是 5GHz 而已。相较于 1978 年到 2000 年,这 20 年里 300 倍的主频提升,从 2000 年到现在的这 19 年,CPU 的主频大概提高了 3 倍。
我们的 CPU,一般都被叫作超大规模集成电路(Very-Large-Scale Integration,VLSI)。这些电路,实际上都是一个个晶体管组合而成的。CPU 在计算,其实就是让晶体管里面的“开关”不断地去“打开”和“关闭”,来组合完成各种运算和功能。
想要计算得快,一方面,我们要在 CPU 里,同样的面积里面,多放一些晶体管,也就是增加密度;另一方面,我们要让晶体管“打开”和“关闭”得更快一点,也就是提升主频。而这两者,都会增加功耗,带来耗电和散热的问题。
因此,在 CPU 里面,能够放下的晶体管数量和晶体管的“开关”频率也都是有限的。一个 CPU 的功率,可以用这样一个公式来表示:
功耗 ~= 1/2 ×负载电容×电压的平方×开关频率×晶体管数量
那么,为了要提升性能,我们需要不断地增加晶体管数量。同样的面积下,我们想要多放一点晶体管,就要把晶体管造得小一点。这个就是平时我们所说的提升“制程”。从 28nm 到 7nm,相当于晶体管本身变成了原来的 1/4 大小。
但是,功耗增加太多,就会导致 CPU 散热跟不上,这时,我们就需要降低电压。这里有一点非常关键,在整个功耗的公式里面,功耗和电压的平方是成正比的。这意味着电压下降到原来的 1/5,整个的功耗会变成原来的 1/25。
虽然制程的优化和电压的下降,在过去的 20 年里,让我们的 CPU 性能有所提升。但是从上世纪九十年代到本世纪初,软件工程师们所用的“面向摩尔定律编程”的套路越来越用不下去了。“写程序不考虑性能,等明年 CPU 性能提升一倍,到时候性能自然就不成问题了”,这种想法已经不可行了。
于是,从奔腾 4 开始,Intel 意识到通过提升主频比较“难”去实现性能提升,边开始推出 Core Duo 这样的多核 CPU,通过提升“吞吐率”而不是“响应时间”,来达到目的。
但是,并不是所有问题,都可以通过并行提高性能来解决。如果想要使用这种思想,需要满足这样几个条件。
第一,需要进行的计算,本身可以分解成几个可以并行的任务。
第二,需要能够分解好问题,并确保几个人的结果能够汇总到一起。
第三,在“汇总”这个阶段,是没有办法并行进行的,还是得顺序执行,一步一步来。
这就引出了我们在进行性能优化中,常常用到的一个经验定律,阿姆达尔定律(Amdahl’s Law)。这个定律说的就是,对于一个程序进行优化之后,处理器并行运算之后效率提升的情况。具体可以用这样一个公式来表示:
优化后的执行时间 = 受优化影响的执行时间 / 加速倍数 + 不受影响的执行时间
我们可以看到,无论是简单地通过提升主频,还是增加更多的 CPU 核心数量,通过并行来提升性能,都会遇到相应的瓶颈。仅仅简单地通过“堆硬件”的方式,在今天已经不能很好地满足我们对于程序性能的期望了。于是,工程师们需要从其他方面开始下功夫了。
在“摩尔定律”和“并行计算”之外,在整个计算机组成层面,还有这样几个原则性的性能提升方法。
1.加速大概率事件。JNI,java中是用二八定律,百分二十的代码执行时间为百分之八十,那就一次编译,后续重复运行,无需在编译,java是解释执行,如果解释一条执行一条,对于循环类的代码,太费时,属于大概率。
2.通过流水线提高性能。比如java中的new操作,一行new代码,是分成三行cpu指令执行,这三个指令还被重排序了,就是为了性能,排序后更加快,相当于流水作业,谁先谁后更快些。
3.通过预测提高性能。最典型的就是局部性原理,要说局部性原理,必然想到mysql,查询一行数据,会把这行数据所在的这一页数据都加到内存,访问这条数据,那么和它相邻的都有可能被访问,所以为了性能,都加进去,这里的一页数据,要从b+索引说起,这一页都是索引顺序排列的叶子结点。