说到性能优化,更多的同学可能会想到系统层面的性能优化。比如在web服务程序中,使用Redis或者其他缓存来提高网站访问速度。程序代码本身的优化比较少。编译器一方面为我们做了很多优化工作,另一方面感觉系统层面的优化效果更明显更高。其实除了系统层面的性能优化,程序代码层面的性能优化效果也非常好。废话不多说,还是用事实说话吧。让我们来看看下面两个程序。两个程序的功能完全一样,就是对二维数组中的每个元素加1。让我们来看看,您认为这两个程序之间会有性能差异吗?实际测试结果是,两者性能相差近4倍。性能差异原因分析我们想一想,为什么会有这么大的性能差异?结合代码可以看出,两段代码的区别在于访问数组元素的顺序,前者是逐列访问,后者是逐行访问。结合图1,可以理解的更清楚。那么,我们结合C语言中二维数据在内存中的排列规则(可以通过上面代码中打印地址来验证),可以知道前者访问的是连续地址空间,后者访问的是跳跃地址空间.图1两种访问形式以整型数组为例,即前者访问的地址为X、X+4、X+8等。后者访问的地址依次为X、X+4096、X+8192。后者每次跳转4KB的地址空间。了解了以上差异后,你有没有想过造成性能差异的原因?我们知道,CPU为了提高访问内存的性能,会在它和内存之间加入缓存。现代CPU缓存通常是3级缓存,分别是L1、L2和L3,其中L1和L2是CPU核心独有的,L3是同一个CPU的多个核心共享的。其基本架构如图2所示。图2CPU缓存架构由于分布式缓存的特点,需要保证其在多个CPU之间的一致性。远,总之就是需要将缓存切割成比较小的粒度进行管理。这个小粒度的管理单元称为缓存行(可以类比为页缓存中的缓存页)。由于缓存的容量远小于内存,缓存无法加载内存中的所有内容。缓存之所以能够起作用,主要是因为常规的业务访问数据有两个特点,即空间局部性和时间局部性。空间局部性:对于刚被访问过的数据,其相邻的数据在未来被访问的概率很高。时间局部性:对于刚被访问过的数据,未来被访问的概率很高。了解了上面的原理后,我们知道,对于上面的程序代码,由于第二个程序依次跳转太远,即不满足空间局部性,导致缓存命中失败。也就是说,第二个程序实际上并不能访问缓存中的数据,而是直接访问内存。内存访问性能远低于缓存访问性能,因此与文章开头有近4倍的性能差异。关于程序性能的其他注意事项对我们的程序进行非常小的更改会对性能产生非常大的影响。因此,在我们日常的开发中,要注意代码中是否存在不合适的代码导致性能问题。下面我们列出一个与性能相关的程序示例,供大家在以后的开发中参考。1、程序结构不合理程序结构对性能的影响有时是灾难性的。在字符串很长的情况下,下面两个函数的性能差异会很大。函数lower1在每次循环中计算字符串的长度,这个计算不是必须的。函数lower2在循环开始前计算字符串的长度,然后用一个常量变量进行条件判断。问题的根源在于strlen函数。该函数通过循环计算字符串的长度。如果字符串比较长,这个函数会比较耗时。2、过程(函数)调用我们知道,在调用过程时,会有入栈、出栈等操作。这些操作通常都是对内存的操作,过程比较复杂。也就是说,调用函数的过程是一个比较耗时的操作,应该尽量减少函数调用。好消息是现代编译器可以对函数调用做很多优化工作,简单的函数调用通常可以被编译器优化。所谓优化调优只是在机器语言(汇编语言)层面,高级语言中没有函数调用。下面看一个具体的例子,通过C语言实现一个简单的函数调用,其中函数fun_1调用函数fun_2,函数fun_2调用printf。这里fun_2并没有做太多的工作,它只是将两个参数相加并传递给printf。图3函数调用优化如图所示,在gcc不做任何优化的情况下,从反汇编代码(图3左下角)可以看出,整个逻辑非常清晰,通过stepby调用函数步。但是经过-O2优化后,汇编代码变得非常简洁(图3右下角)。从fun_1的汇编代码可以看出,它根本没有调用fun_2,而是直接调用了printf函数。因此,编译器可以在不影响其功能的情况下优化函数调用。但这也不是绝对的,稍微复杂一点的函数调用编译器可能就无能为力了,这个时候可能会导致性能损失。3、操作者的差异不同操作的耗时差异也是非常巨大的。比如乘法的耗时是加法的两三倍,除法的耗时是加法的十几倍。因此,在访问频率比较高的逻辑中减少除法的使用会有明显改善。在Java的HashMap实现中,hash的key是按位运算计算的,不是按模运算计算的。因为模运算本身就是除法运算,性能比位运算差十几倍。staticfinalinthash(Objectkey){inth;return(key==null)?0:(h=key.hashCode())^(h>>>16);}更详细的处理逻辑可以参考JDK源码.本文只是一块砖。4.引用和复制支持类的高级语言在传递对象参数的时候涉及到复制的过程,而对象的复制也是一个比较耗性能的操作。当然,高级语言通过一种叫做引用的机制来实现对象地址的传递,从而避免了复制的过程(这就是按值传递和按地址传递的区别)。在程序开发过程中,关于性能的问题还很多,本文无法一一列举。然而,关键问题是掌握该技术的底层实现原理。俗话说,任何其他高层次的内容都可以通过底层原理来解释。
