有幸参加了2015年PHP技术峰会(PHPCON),听鸟哥(惠新辰)分享PHP7的新特性和性能优化,一切都激动人心。鸟哥是国内最权威的PHP专家。他的分享有很多非常有价值的东西。本人将分享的PPT整理整理,收集相关资料,整理成这篇解读性质的技术文章。希望可以送给正在做PHP开发的同学。一些帮助。PHP已经走过了20年的历史。直到今天,PHP7才发布了RC版。据说PHP7正式版应该会在2015年11月左右发布。与之前的PHP5.*系列相比,PHP7可以说是一次大规模的革新,尤其是在性能方面。PHP是一种在全球范围内被广泛使用的Web开发语言,PHP7的革新当然会给这些Web服务带来更深刻的变化。下面是鸟哥PPT中的一张图表(82%的网站使用PHP作为开发语言):(注:一个网站可以使用多种语言作为开发语言)(注:本文包含大量来自鸟哥的师兄PPT中截图,图片版权归鸟师兄所有)先来看看两个激动人心的性能测试结果:Benchmark对比(图片来自PPT):PHP7性能测试结果,性能压力测试结果,耗时自2.991跌至1.186,跌幅达60%。WordPressQPS压力测试(图片来自PPT):在WordPress项目中,与PHP5.6相比,PHP7的QPS提升了2.77倍。看完激动人心的性能测试结果对比,我们进入正题。PHP7中有许多新特性,但是,我们将更多地关注那些主要变化。一、新特性和变化1、标量类型和返回类型声明(ScalarTypeDeclarations&ScalarTypeDeclarations)PHP语言的一个很重要的特性就是“弱类型”,它使得PHP程序非常容易编写。新手可以快速上手PHP,但是,也伴随着一些争议。支持变量类型的定义可以说是一个创新的变化,PHP开始以可选的方式支持类型定义。另外,一个switch指令declare(strict_type=1);介绍。一旦开启该指令,将强制当前文件下的程序遵循严格的函数参数类型和返回类型。比如一个add函数加上一个类型定义可以这样写:如果配合强制类型切换命令,可以变成这样:如果不启用strict_type,PHP会尝试将其转换为需要的类型对你来说,启用它之后,它会改变PHP不再做类型转换,如果类型不匹配就会抛出错误。这对于喜欢“强类型”语言的同学来说是一大福音。更详细的介绍:PHP7scalartypedeclarationRFC[译]2.更多的Errors成为可捕捉的ExceptionPHP7实现了一个全局的throwable接口,原来的Exception和一些Errors都实现了这个接口(interface),以接口的方式定义了异常的继承结构。因此,PHP7中更多的Errors变成了Exceptions,可以被捕获并返回给开发者。如果它们没有被捕获,它们将是错误的。如果它们被捕获,它们将成为一个可以在程序内处理的异常。这些能够被捕获的错误通常是不会对程序造成致命伤害的错误,比如函数不存在。PHP7进一步方便了开发者的操作,让开发者对程序有更大的控制权。因为在默认情况下,Error会直接导致程序中断,而PHP7提供了捕获和处理的能力,让程序继续执行,为程序员提供了更灵活的选择。比如执行一个我们不确定是否存在的函数,PHP5兼容的方法是在函数调用前加上判断function_exist,而PHP7支持catchException的处理方法。下图示例(截图来自PPT):3.AST(AbstractSyntaxTree,抽象语法树)AST在PHP编译过程中起到中间件的作用,取代了原来直接从解释器吐出opcode的方式,让解释器(parser)和编译器(compliler)解耦,可以减少一些Hack代码,同时,让实现更容易理解和维护。PHP5:PHP7:更多AST资料:https://wiki.php.net/rfc/abstract_syntax_tree4.NativeTLS(NativeThreadlocalstorage,nativethreadlocalstorage)多线程模式下的PHP(例如Web服务器Apache的woker和event模式,是多线程),需要解决“线程安全”(TS,ThreadSafe)的问题,因为线程共享进程的内存空间,所以每个线程本身都需要建立一个私有空间来保存自己的私有数据以某种方式,避免与其他线程相互污染。PHP5采用的方法是维护一个大的全局数组,为每个线程分配一个独立的存储空间,线程通过各自的键值访问这个全局数据组。而这个唯一键值在PHP5中需要传递给每一个需要使用全局变量的函数,PHP7认为这种传递方式不友好,存在一些问题。因此,尽量使用一个全局的线程专用变量来保存这个键值。相关NativeTLS问题:https://wiki.php.net/rfc/native-tls5。其他新特性PHP7的新特性和变化还有很多,这里就不展开详细说明了。Int64支持,不同平台下统一整数长度,字符串和文件上传都支持大于2GB。统一变量语法(Uniformvariablesyntax)。foreach行为一致(Consistentlyforeachbehaviors)Newoperator<=>,??Unicode字符格式支持(\u{xxxxx})匿名类支持(AnonymousClass)......#p#2.Leap-forwardperformancebreakthrough:fullspeedforward1.JIT和性能JustInTime(即时编译)是一种在运行时将字节码编译成机器码的软件优化技术。从直觉出发,我们很容易认为机器码可以被计算机直接识别并执行,这比Zend读取操作码一条一条执行效率更高。其中,HHVM(HipHopVirtualMachine,HHVM是Facebook开源的PHP虚拟机)使用了JIT,将他们的PHP性能测试提升了一个数量级,并发布了令人震惊的测试结果,这也让我们直观地认为JIT是一个强大的点石成金技术。其实在2013年,鸟哥和Dmitry(PHP语言内核开发者之一)就对PHP5.5版本进行了JIT的尝试(并没有发布)。PHP5.5原来的执行过程是将PHP代码通过词法和语法分析,编译成opcode字节码(格式有点类似汇编),然后Zend引擎读取这些opcode指令,逐条解析执行.而他们在opcode链接之后引入了类型推断(TypeInf),然后通过JIT生成ByteCodes,然后执行。因此在benchmark(测试程序)中得到了令人振奋的结果,实现JIT后的性能比PHP5.5提高了8倍。然而,当他们把这个优化放到实际项目WordPress(一个开源博客项目)中时,性能几乎没有任何提升,他们得到了一个令人费解的测试结果。因此,他们使用Linux下的profile类工具来分析程序执行的CPU耗时。WordPress执行100次CPU消耗分布(PPT截图):注:21%的CPU时间花在了内存管理上。12%的CPU时间花在哈希表操作上,主要是PHP数组的增删改查。30%的CPU时间花费在strlen等内置函数上。25%的CPU时间花费在VM(Zend引擎)上。经过分析,得到两个结论:(1)如果JIT生成的ByteCodes过大,会导致CPU缓存失败率(CPUCacheMiss)下降。在PHP5.5的代码中,由于没有明显的类型定义,只能通过类型推断。尽可能定义可以推断的变量类型,然后结合类型推断,去除不属于该类型的分支代码,生成直接可执行的机器码。但是,类型推断不能推断出所有类型。在WordPress中,只能推断出不到30%的类型信息,能够减少的分支代码也很有限。结果JIT后直接生成机器码,生成的ByteCodes过大,最终导致CPU缓存cache(CPUCacheMiss)明显减少。CPU缓存***是指在读取和执行指令的过程中,如果在CPU的一级缓存(L1)中无法读取到需要的数据,就不得不继续查找,直到到达二级缓存。(L2)和L3缓存(L3),最终会尝试在内存区中寻找需要的指令数据,内存和CPU缓存的读取时间差距可达100倍。因此,如果ByteCodes太大,执行的指令数太多,导致多级缓存无法容纳这么多数据,一些指令就不得不存放在内存区。CPU各级缓存的大小也是有限制的。下图是Inteli7920的配置信息:因此CPU缓存效率的下降会导致时间消耗的严重增加。另一方面,JIT带来的性能提升也被认为是抵消了。通过JIT,可以减少VM的开销。同时,通过指令优化,可以间接减少内存管理的开发,因为可以减少内存分配的次数。然而,对于一个真正的WordPress项目来说,只有25%的CPU时间花在了VM上,主要问题和瓶颈其实并不在VM上。因此,JIT优化计划绝对不包含在该版本的PHP7特性中。不过很有可能会在之后的版本中实现,同样值得期待。(2)JIT性能的提升效果取决于项目的实际瓶颈。JIT在benchmark中有了很大的提升,因为代码量比较少,生成的ByteCodes也比较少。同时,主要开销在VM中。但是在实际的WordPress项目中并没有明显的性能提升。原因是WordPress的代码量比benchmark大很多。虽然JIT减少了VM的开销,但是由于ByteCodes太大,会导致CPU缓存减少***和额外的内存开销,最终证明没有任何提升。不同类型的项目会有不同的CPU开销率,会得到不同的结果。脱离实际项目的性能测试,代表性不强。2、zval改变了PHP中各种类型的变量。事实上,真正的存储载体是Zval,它的特点是包容和包容。本质上,它是一种用C语言实现的结构体(struct)。对于写PHP的同学来说,可以大致理解成数组数组之类的东西。PHP5的zval,内存占用24字节(PPT截图):PHP7的Zval,内存占用16字节(PPT截图):Zval从24字节降到16字节,为什么降了?这里需要补充一点C语言基础,辅助不熟悉C的同学理解。Struct与联合(union)有点不同。Struct的每个成员变量占用一个独立的内存空间,而union中的成员变量共享一个内存空间(也就是说,如果其中一个成员变量被修改,公共空间将被删除。修改后,其他成员变量的记录会消失)。因此,虽然成员变量看起来多了很多,但实际占用的内存空间却减少了。此外,还有显着变化的特性,一些简单类型不再使用引用。Zval结构图(来自PPT):图中的Zval由两个64bits组成(1byte=8bit,bit为“bit”)。如果变量类型为long、bealoon等,长度不超过64bit,则直接存入值中,无后续引用。当变量类型为array、objec、string等超过64bit时,存储的值是一个指针,指向真正的存储结构地址。对于简单的变量类型,Zval存储变得非常简单和高效。不需要引用的类型:NULL、Boolean、Long、Double需要引用的类型:String、Array、Object、Resource、Reference3。内部类型zend_stringZend_string是一个实际存储字符串的结构,实际内容会存储在val(char,字符类型)中,val是一个char数组,长度为1(方便成员变量占空间)。该结构的第一个成员变量使用char数组而不是char*。这里有一个小的优化技巧,可以减少CPU的cachemiss。如果使用char数组,malloc申请上述结构体的内存时,是在同一个区域申请的,通常长度为sizeof(_zend_string)+实际的char存储空间。但是如果使用char*的话,这个位置只是存放了一个指针,真正的存放在另外一个独立的内存区。使用char[1]和char*分配内存的对比:从逻辑实现上看,两者其实没有太大区别,效果也很相似。事实上,当这些内存块被加载到CPU中时,看起来就很不一样了。因为前者是连续分配在一起的同一块内存,通常CPU读取的时候可以一起获取到(因为会在同级缓存)。对于后者,因为是两块内存的数据,当CPU读取第一块内存时,很可能第二块内存的数据不在同级缓存中,这样CPU还得到L2(二级缓存)下面找,甚至到内存区去寻找想要的第二块内存数据。这样就会造成CPUCacheMiss,两者耗时可以相差100倍。另外,在复制字符串的时候,使用引用赋值,zend_string可以避免内存复制。6.PHP数组(HashTable和ZendArray)的变化在编写PHP程序的过程中,使用频率最高的类型就是数组。PHP5数组是使用HashTable实现的。粗略地概括一下,它可以看作是一个支持双向链表的HashTable。它不仅支持通过数组键的hash映射访问元素,还支持通过foreach访问双向链表的方式遍历数组元素。PHP5的HashTable(PPT截图):这张图看起来很复杂,各种指针跳来跳去。当我们通过键值访问一个元素的内容时,有时需要3次指针跳转才能找到需要的内容。.最重要的一点是这些数组元素存放在不同的内存区域。同理,当CPU读取时,由于它们很可能不在同一级缓存中,CPU将不得不寻找更下级的缓存甚至内存区域,这意味着CPU缓存内存会减少,并且然后将添加更多。耗时。PHP7之ZendArray(截图自PPT):新版数组结构非常简洁醒目。最大的特点是整块数组元素和哈希映射表都连在一起,分配在同一块内存中。如果是遍历一个简单的整型数组,效率会很快,因为数组元素(Bucket)本身是在同一块内存中连续分配的,而数组元素的zval会把整型元素存放在里面,而不会有指针外部链接,所有数据都存放在当前内存区。当然最重要的是可以避免CPUCacheMiss(CPU缓存故障率降低)。ZendArray的变化:数组的值默认为zval。HashTable的大小从72字节减少到56字节,减少了22%。Buckets的大小从72字节减少到32字节,减少了50%。数组元素的Buckets的内存空间是一起分配的。数组元素的键(Bucket.key)指向zend_string。数组元素的值嵌入到Bucket中。降低CPUCacheMiss。7、函数调用约定(FunctionCallingConvention)PHP7改进了函数调用机制,通过优化参数传递的环节,减少了一些指令,提高了执行效率。PHP5的函数??调用机制(PPT截图):图中,vm栈中send_val和recv参数的指令是一样的。PHP7通过减少这两个重复实现了函数调用机制的底层优化。PHP7的函数调用机制(PPT截图):8.通过宏定义和内联函数(inline),让编译器提前完成部分工作。C语言的宏定义会在预处理阶段(编译阶段)执行,完成部分工作后,程序运行时不需要分配内存,可以实现类似函数的功能,但有没有函数调用栈的压栈和出栈开销,效率会更高。内联函数也是如此。在预处理阶段,程序中的函数被替换为函数体,真正运行的程序在这里执行,不会产生函数调用的开销。PHP7在这方面做了很多优化,把很多需要在运行阶段进行的工作放到了编译阶段。比如参数类型的判断(ParametersParsing),因为涉及到固定的字符常量,所以可以在编译阶段完成,从而提高后续的执行效率。比如下图中,处理传参类型的方法从左边的写法优化为右边宏的写法。3.总结鸟哥在PPT中放出了一组对比数据,即WordPress在PHP5.6中执行100次,会产生70亿次CPU指令执行,而在PHP7中只需要25亿次,减少了64.2%。这是一个令人震惊的统计数据。在整个鸟哥的分享中,对我来说最深刻的一个观点就是:关注细节,做很多小的优化,一点一点的积累,一点一点做多,最后汇聚成惊人的结果。我想,这大概也是难成大山非一日之功的道理吧。毫无疑问,PHP7在性能上实现了跨越式的提升。如果这些成果能够应用到PHPweb系统中,或许我们只需要更少的机器就可以支持更高请求量的服务。PHP7正式版的发布让人充满期待。Reference&参考资料:鸟哥(惠新臣)分享的PPT,http://www.laruence.com/PHP官方社区,http://php.net/致谢:感谢鸟哥(惠新臣)提供的Help和支持。来自:http://hansionxu.blog.163.com/
