我们使PHP 7的速度是PHP 5的两倍的方式。第1部分:优化数据结构

2015年12月 ,PHP 7.0发行了。 切换到“七个”的公司指出,生产力提高了,服务器的负载减少了。 首先搬到这七个国家的是Vebia和Etsy,我们有Badoo,Avito和OLX。 对于Badoo,切换到这七个服务器可节省100万美元的服务器成本。 借助OLX中的PHP 7,平均服务器负载减少了3倍,从而提高了效率并节省了资源。

Zend Technologies的Dmitry StogovHighLoad ++上发表讲话 ,这提高了生产率。 在解码中:关于PHP的内部结构,关于7.0版核心的思想,关于确定成功的基本数据结构和算法的变化。

免责声明:截至2019年3月 ,尽管自2019年1月1日 起不支持此版本 ,但 80%的站点 都在PHP上运行,其中 70 %的站点 PHP 5 上运行 德米特里(Dmitry)在2016年的报告中指出,PHP 5和7之间的生产率翻倍的原则也与2019年3月有关。当然,对于一半的站点而言。

关于发言人:德米特里·斯托戈夫(Dmitry Stogov)早在80年代开始编写程序:“ Electronics B3-34”,基本的汇编程序。 2002年,Dmitry熟悉了PHP,并很快开始进行改进:他开发了针对PHP的Turck MMCache,领导了PHPNG项目,并在开发PHP的JIT中发挥了重要作用。 Zend Technologies的首席工程师的最后14年。

Zend Technologies正在开发PHP和基于它的商业解决方案。 它是由以色列程序员Andy Gutmans和Zeev Suraski于1999年创立的,两年前他们创建了PHP3。这些人处在PHP开发的最前沿,在很大程度上决定了该语言的当前外观以及该技术的成功。

Zend Technologies为它开发了PHP核心和应用程序,在工作中,我不得不编写扩展,进入所有子系统,甚至从事商业项目,有时根本不与PHP连接。 但是对我来说最有趣的话题一直是表演

在加入Zend之前,我就开始寻找提高PHP速度的方法,从事与公司竞争的我自己的项目。 在该项目的工作过程中,我彻底理解了该语言,并意识到不与主流项目一起工作,您只能影响脚本执行的某些方面,而所有最有趣和有效的操作都只能在内核中创建。 这种理解和巧合使我走向了Zend。

略论PHP历史


PHP不仅是而且不仅仅是一种编程语言 。 PHP代表个人主页-用于创建个人网页和动态网站的工具。 语言只是其主要部分之一。 PHP是一个功能强大的库,许多扩展用于与其他第三方库一起使用,例如,用于访问数据库或XML解析器,以及用于与各种Web服务器通信的一组模块。

丹麦程序员Rasmus Lerdorf 于1995年6月推出了PHP。 那时,它只是用Perl编写的CGI脚本集合 。 96年4月,拉斯穆斯(Rasmus)推出了PHP / FI,并于6月发布了PHP / FI 2.0。 随后,Andy Gutmans和Zeev Surasky对这个版本进行了实质性的重新设计,并在第98发行了PHP 3.0。 到2000年,无论是在语言还是内部架构方面,这种语言已经成为我们如今所见的那种语言-基于Zend Engine的PHP 4。

从版本4开始,PHP不断发展。 转折点是2004年PHP 5的发布,当时对象模型已完全更新 。 是她开启了PHP框架的时代,并将性能问题提升到了一个新的高度。 预见到这一点,在5.0版本发布后,我们Zend立刻考虑了加速PHP的工作,并开始致力于提高生产力。

7.1版于2016年11月在综合测试中发布, 比2002版快25倍 。 根据不同部门的性能变化图,主要突破在5.1和7.0中可见。



在5.1版中,我们才刚刚开始进行性能方面的工作,我们获得了一切,但是在5.3版之后,我们碰壁了,所有改进解释器的尝试都落空了。

不过,我们找到了挖掘的地方,并获得了超出预期的结果-与先前的5.6版测试相比,加速度提高了2.5倍。 但是最有趣的是,在未更改的实际应用程序中,我们获得了相同的2.5倍加速。 这是一种现象,因为我们在10年中的5个生命周期中都建立了先前的因子2。



合成测试中5.1的巨大飞跃在实际应用中并不明显。 原因是在不同的用途下,PHP性能取决于与不同子系统相关的制动器。

PHP 7的历史始于三年停滞 ,始于2012年,直到2015年结束第七个版本的发布。 然后我们意识到,仅通过对口译员进行少量改进,就无法再提高生产率,因此转向了JIT。

在JIT周围徘徊


我们花了将近两年的时间在PHP-5.5的JIT原型上。 首先,我们生成了一个非常简单的代码-一系列对标准处理程序的调用,类似于缝合的Fort代码。 然后,他们编写了自己的Runtime Assembler ,这是用于解决方法的内联单独代码,但意识到这种低级优化甚至对测试也没有实际效果。

然后,我们考虑使用静态分析方法导出变量类型。 意识到结论后,我们立即在测试中获得了2倍的加速度。 受到鼓舞,他们尝试编写全局寄存器分配器,但失败了。 我们使用了相当高级的表示形式,几乎不可能将其用于寄存器分配。

为了避免出现低级问题,我们决定尝试LLVM,一年后,Bench.php的加速度提高10倍 ,但在实际应用中却没有。 此外,现在编译真实的应用程序要花几分钟的时间,例如, 对Wordpress的第一次请求花了2分钟,并且没有加速。 当然,这完全不适合实际实践。

正确的类型预测可以产生良好的代码,而这种类型的预测在实际的应用程序中效果不佳,而使用PHP数据结构会使生成的代码效率低下。

什么变慢了?


我们重新考虑了失败的原因,然后再次决定看看PHP为何运行缓慢。 该图显示了将多个请求分析到Wordpress主页的结果。



不到30%的时间用于解释字节码,20%的内存管理器开销,13%的哈希表处理,5%的正则表达式处理。

在JIT工作时,我们仅摆脱了前30%的工作,其他所有工作都无法实现。 几乎在所有地方,我们都被迫使用标准的PHP数据结构,这会产生开销:内存分配,引用计数等。 这种理解得出的结论是,有必要替换PHP中的关键数据结构。 以此为基础PHPNG项目开始了。

png 新世代


该项目是在尝试为PHP创建JIT失败之后开发的。 主要目标是达到更高的生产率水平并为将来的改进奠定基础

我们承诺一段时间后,不再使用综合测试来衡量性能-这些通常是小型计算机程序,它们使用的数据量有限,完全适合处理器的缓存。 相比之下,实际的应用程序受制于与子系统内存相关的制动,并且从内存中进行一次读取可能会花费100条计算指令。 PHPNG项目是对关键PHP数据结构的重构,以优化内存访问 。 没有创新,与PHP 5兼容100%。

如何改变这些结构是显而易见的。 但是相关更改的数量巨大,因为PHP本身的核心是150,000行 ,几乎三分之一的内容都需要更改。 添加基本​​分发中包含的一百多个扩展,针对不同Web服务器的十二个模块,您将实现该项目的宏伟壮丽。

我们甚至不确定我们是否会完成该项目。 因此,他们秘密启动了该项目,并仅在第一个乐观结果出现时才打开该项目。 简单地编译内核花了两个星期。 两个星期后,benched.php获得了。 我们花了一个半月的时间来确保Wordpress的工作。 一个月后,我们打开了该项目-2014年5月。 那时,我们在Wordpress上获得了30%加速 。 这似乎已经是一个盛大的活动。

PHPNG立即引起了人们的关注,并于2014年8月将其用作PHP 7未来的基础 。 这已经是另一个具有不同目标集的项目,其中生产力只是其中一个。

PHP 7.0


版本号7本身令人怀疑。 前一个版本是第五个版本。 第六个是几年前开发的,完全致力于本机Unicode支持,但是在开发的早期阶段做出的不成功决定导致内核代码和每个扩展的过度复杂。 最后,决定冻结该项目。

到那时为止,已经积累了许多专门用于PHP 6的材料:在会议上的演讲,出版的书籍。 为了不让任何人感到困惑,我们将项目PHP 7跳过了PHP6。该版本要幸运得多-PHP 7几乎按计划于2015年12月发布。

除了性能之外,PHP 7中还出现了一些人们期待已久的创新:

  • 能够定义参数和返回值的标量类型。
  • 异常而不是错误-现在我们可以捕获并处理它们。
  • Zero-cost assert() ,匿名类,清理不一致,新的运算符和函数(<=>,??)。

创新是好的,但要回到内部变化。 让我们讨论一下PHP 7遵循的路径以及该路径可以指引我们的方向。

兹瓦尔


这是基本的PHP数据结构。 它用于表示PHP中的任何值 。 由于我们的语言是动态类型的,并且变量的类型可以在程序执行期间更改,因此我们需要存储一个类型字段(zend_uchar类型),该字段可以使用值IS_NULL,IS_BOOL,IS_LONG,IS_DOUBLE,IS_ARRAY,IS_OBJECT等,实际上由union(值)表示的值,其中可以存储整数,实数,字符串,数组或对象。

PHP 5中的zval


每个此类结构的内存都在堆中单独分配。 除了类型和值外,它还包含对该结构的引用计数器。 因此,该结构占用了24个字节,还没有计算内存管理器及其指向的开销。

右上方的图片显示了在PHP 5内存中为简单脚本创建的数据结构。



在堆栈上,为指针所代表的4个变量分配了内存。 值本身(zval)在堆上。 在我们的例子中,这只是两个zval,每个zval由两个变量引用,因此它们的引用计数器设置为2。

要访问类型或标量值,您至少需要读取两次:首先读取指针的值,然后读取结构的值。 如果您不需要读取标量值,而是例如读取字符串或数组的一部分,则至少需要再读取一次。

PHP 7中的zval


在以前使用指针的地方,在七个指针中我们开始嵌入zval。 我们已经不再使用标量类型的引用计数了。 字段的类型和值保持不变,但是没有添加其他标志和保留位置,稍后我将讨论。



左边是PHP 5,右边是PHP 7。



现在,zval本身就在堆栈中。 要读取类型和标量值,只需一条机器指令就足够了。 所有值都分组在一个内存区域中,这意味着使用局部变量时,由于处理器高速缓存未命中,我们几乎不会损失。 但是,当需要复制时,新性能的真正力量就包括在内。

复制记录


在脚本的第一行,添加了另一个任务。



在PHP5中,我们从堆中为新的zval分配了内存,初始化了它的int(2),更改了指向变量b的指针的值,并减少了b先前已引用的值的引用计数器。

在PHP 7中,我们只需要用几条指令就可以直接初始化变量b ,而在PHP 5中则需要数百条指令。 所以zval现在看起来在内存中。



这是两个64位字。 第一个单词的含义是:整数,实数或指针。 在第二个单词中,是类型 (表示如何解释含义),标志和保留位置,它们在对齐时仍会添加。 但是它并没有消失,但是被不同的子系统用来存储间接相关的值。

标志是一组位 ,其中每个位指示zval是否支持协议。 例如,如果它是IS_TYPE_REFCOUNTED ,则在使用此zval时,引擎应注意参考计数器的值。 分配时,增加;离开范围时,减少;如果参考计数器达到零,则销毁从属结构。

在这些类型中,与PHP 5相比,出现了几个新类型。

  • IS_UNDEF未初始化变量的标记。
  • 单个IS_BOOL单独的IS_FALSEIS_TRUE
  • 添加了链接的单独类型和更多魔术类型。

IS_UNDEFIS_DOUBLE类型是标量,不需要额外的内存。 要复制它们,只需复制第一个机器的64位字的值和第二个一半的类型和标志的值即可。

重新计价


与其他类型比较困难。 它们全部由一个从属结构表示,zval仅存储对该结构的引用。 对于每种类型,此结构都不同,但是就OOP而言,它们都有一个通用的抽象祖先或zend_refcounted结构。 它确定第一个64位字的格式,其中存储了垃圾回收的引用计数和其他信息。



这个词可以简单地视为垃圾收集器的信息,并且特定类型的结构在第一个词之后添加其字段。

线数


在字符串的七个中,我们存储哈希函数的计算值,其长度和字符本身。 这种结构的大小是可变的,并且取决于字符串的长度。 必要时,为字符串计算一次哈希函数。 在PHP 5中,它可以根据需要进行重新计算。



现在这些字符串是引用计数的,并且如果在PHP 5中我们复制了字符本身,现在足以增加此结构的引用计数。

与PHP 5一样,我们仍然具有不可变或内部字符串的概念。 它们通常存在于一个实例中,一直存在到查询结束,并且其行为类似于标量值。 我们不需要照顾对它们的引用的计数器,并且要进行复制就足以在四个机器指令的帮助下仅复制zval本身。

数组


数组由内置的哈希表表示,与PHP 5并没有太大区别。哈希表本身已更改,但在此方面有更多更改。



现在,数组是一种自适应结构 ,根据存储的数据会稍微改变其内部结构和行为。 如果我们仅存储具有紧密数字键的元素,那么我们就可以通过索引直接访问元素,访问速度可以与C中的数组速度相媲美。但是,如果将具有字符串键的元素添加到同一数组中,它将变成具有冲突解决方案的真实哈希。

这就是哈希表在PHP 5中的样子。



这是经典的哈希表实现,使用线性列表来解决冲突(如右上角所示)。 每个项目都由一个桶代表。 所有存储桶都通过双向链接列表链接来解决冲突,并通过另一个双向链接列表链接来依次进行迭代。 每个zval的值是单独分配的-在存储桶中,我们仅存储指向其的链接。 同样,可以分别分配字符串键。

因此,对于每个哈希表,您需要分配很多小的内存块,并且为了以后查找内容,您必须沿着指针运行。 每次这样的转换都会导致cahce丢失和大约10-100个处理器周期的延迟。

这就是PHP 7中发生的情况。



逻辑结构保持不变,只有物理结构改变。 现在,在哈希表下,通过一个操作分配内存。

在图片中,基本指针的底部是元素,顶部是由哈希函数寻址的哈希数组。 对于平面或压缩数组,当我们仅存储具有数字索引的元素时,上半部分根本没有分配,我们直接通过数字寻址存储桶。

为了绕过元素,我们从上到下或从下到上依次对其进行排序,现代处理器可以完美地做到这一点。 这些值内置在存储桶中,但其中的保留空间仅用于解决冲突。 它存储具有相同哈希函数值或列表标记结尾的另一个存储桶的索引。

键的字符串值的内存是单独分配的,但仍然是相同的zend_string。 当粘贴到数组中时,增加字符串的引用计数器就足够了,尽管在我们不得不直接复制字符之前,并且在搜索时,我们现在可以比较的不是字符串,而是指向字符串本身的指针。

不变数组


以前,我们有不可变的字符串,但现在也出现了不可变的数组。 像字符串一样,它们不使用引用计数,并且直到请求结束都不会销毁。 这是一个简单的脚本,它创建了一个包含一百万个元素的数组,每个元素都是带有单个“ hello”元素的同一数组。



在PHP 5中,每次循环迭代时,都会创建一个新的空数组,并向其中写入“ hello”,并将所有这些都添加到结果数组中。 在PHP 7中,在编译阶段,我们只创建一个行为类似于标量的不可变数组 ,并将其添加到结果数组中。 在给出的示例中,这使我们可以减少10倍以上的内存消耗和近10倍的加速。

当然,在实际应用中通常不会发现数百万个元素的恒定数组,但是较小的元素很常见。 在他们每个人上,您都会获得一个小小的胜利。

对象


链接到PHP 5中所有对象的链接位于单独的存储库中,在zval中只有句柄-唯一的对象ID。



为了达到目标,我们至少读取了3个读数。 此外,对象的每个属性的值的内存是分别分配的,我们至少需要再读取2次才能读取它。

在PHP 7中,我们能够转向直接寻址。



zend_object可以通过一条机器指令访问zend_object地址。 而且Property是内置的,要阅读它们,您只需要再阅读一遍。 它们也被分组在一起,从而改善了数据的局部性,并帮助现代处理器避免绊倒。

除了预定义的属性,此对象的类的链接也存储在此处,一些处理程序-虚拟方法表的类似物,以及尚未定义的属性的哈希表。 在PHP中,您可以将属性添加到最初未定义的任何对象中,并且如果多条机器指令足以访问预定义的属性,则对于非预定义的属性,您将必须使用哈希表,这将需要数十条机器指令。 当然,这要贵得多。

参考资料


最后,我们必须引入一个单独的类型来表示PHP链接。



这是一个完全透明的类型。 PHP脚本不可见。 脚本会看到另一个内置在zend_reference结构中的zval。 可以理解,我们至少从两个位置引用了这种结构,并且该结构的参考计数器始终大于1。一旦计数器降为1,链接就会变成常规的标量值。 链接中嵌入的zval被复制到引用它的最后一个zval,并且结构本身被删除。

看来现在使用引用比使用其他类型复杂得多(这是事实),但是实际上在PHP 5中,访问任何值(甚至是素数整数)时,我们都必须进行相当复杂的工作。 现在,我们仅将一种更复杂的协议应用于一种类型,从而加快了与其他所有类型的协议的工作,尤其是标量值的工作。

IS_FALSE和IS_TRUE


我已经说过,单个类型IS_BOOL被分为单独的IS_FALSE和IS_TRUE。 这个想法在LuaJIT的实现中得到了体现,并被用来加速最常见的操作之一-条件转换。



如果在PHP 5中需要读取类型,检查布尔值,读取值,确定是true还是false并基于此进行转换,那么现在只需检查类型并将其与true进行比较就足够了:

  • 如果是真的,那么我们沿着一个分支前进;
  • 如果小于真,则转到另一个分支;否则,转到另一个分支。
  • 如果它大于true,请转到所谓的慢路径(slow path),在那里我们检查它来自什么类型以及如何处理:如果它是整数,那么我们必须将其值与0进行比较,如果是float-则再次与0(但真实)等

通话约定


调用约定或函数调用约定的更改是一项重要的优化,不仅影响数据结构,而且还影响基础算法。 左图中是一个小的脚本,由foo()函数及其调用组成。 以下是PHP 5将该脚本编译到的字节码。



首先,我将告诉您它在PHP 5中是如何工作的。

PHP 5中的调用约定


第一个SEND_VAL语句是将值“ 3”发送到foo函数。 为此,她被迫在堆上分配一个新的zval,在其中复制值(3)并将指向该结构的指针的值写入堆栈。



与第二条指令类似。 进一步的DO_FCALL初始化了CALL FRAME ,为局部和临时变量保留了一个位置,并将控制权转移给了被调用的函数。



第一个RECV检查第一个参数,并使用相应的局部变量($ a)初始化堆栈上的插槽。 在这里,我们没有进行复制,只是增加了相应参数(zval的值为3)的参考计数器。 同样,第二条RECV在变量$ b和参数5之间建立了连接。



进一步的身体功能。 发生了3 + 5加法-结果为8。这是一个临时变量,其值直接存储在堆栈中。



返回,我们从函数返回。



返回时,我们释放所有超出范围的变量和参数。 为此,我们要检查释放帧中插槽引用的所有zval,并为每一个减少引用计数。 如果达到0,则销毁相应的结构。

如您所见,即使是向函数发送常量这样的简单操作,也需要分配新的内存,复制和增加引用计数器,然后还要进行两次减少和删除。

PHP 7中的调用约定


在PHP 7中,这些问题已修复-现在在堆栈上,我们存储的不是zval指针,而是zval指针。



我们还引入了一条新指令INIT_FCALL ,该指令现在负责在CALL FRAME下初始化和分配内存,并为参数和临时变量保留空间。



SEND_VAL 3现在仅将参数复制到CALL FRAME之后的第一个插槽。 接下来的SEND_VAL 5到第二个广告位。



然后最有趣。 似乎DO_FCALL应该将控制权传递给被调用函数的第一条指令。 但是参数已经触及了为变量参数$ a和$ b保留的位置,而RECV指令什么也没做。 因此,您可以直接跳过它们。 我们发送了两个参数,因此我们跳过了两个指令。 如果他们发送了三个,他们将错过三个。



因此,我们直接转到函数的主体,进行加法并返回。



返回时,我们清除所有局部变量,但是现在仅清除两个插槽,并且由于那里有标量,因此我们无需执行任何操作。



我的故事有些简化,它没有考虑变量数量可变的函数以及类型检查和其他一些要点。

新的《呼叫约定》已经破坏了兼容性 。 PHP具有func_get_argfunc_get_args类的功能。 如果以前他们返回了已发送参数的原始值,则现在他们返回了相应局部变量的当前值,因为我们只是不存储原始值。 就像C.调试器一样



此外,该函数不能再包含多个具有相同名称的参数。 之前没有任何意义,但是我遇到了这样的PHP代码foo($_, $_) 。 看起来像什么? (我认可Prolog)

新的内存管理器


完成数据结构和基本算法的优化后,我们再次提请注意所有制动子系统。 PHP 5中的内存管理器在Wordpress上占用了近20%的处理器时间

在我们摆脱了很多分配之后,他的管理费用变得更少了,但是仍然很可观-并不是因为他正在做任何重要的工作,而是因为他偶然发现了缓存。 发生这种情况的原因是,我们使用了经典的道格·里阿(Doug Lea)的malloc算法,该算法涉及通过链接和树来查找合适的空闲内存位置,所有这些行程不可避免地导致高速缓存未命中。

如今,有新的内存管理算法考虑了现代处理器的功能。 例如: Google的 jemallocptmalloc 。 最初,我们尝试不加改变地使用它们,但没有成功,因为缺少特定的PHP功能使在请求结束时完全释放内存的成本更高。 结果,我们放弃了dlmalloc并编写了自己的东西,结合了旧内存管理器和jemalloc的想法。

我们将内存管理器的开销减少到5% ,减少了服务信息的内存开销,并改善了CPU缓存的使用。 现在可以通过位图搜索合适的内存块,从各个页面分配小块的内存,并在释放时进行缓存,并添加针对常用块大小的专用功能。

许多小的改进


我只谈到了最重要的改进,但是还有很多次要的改进。 我可以提到其中一些。

  • 用于解析内部函数参数的快速API,以及用于在HashTable上进行迭代的新API。
  • 新的VM指令:字符串连接,特殊化,超级指令。
  • 一些内部功能已变成VM指令:strlen,is_int。
  • 将CPU寄存器用作VM寄存器:IP和FP。
  • 优化复制和删除数组。
  • 使用链接计数而不是尽可能复制。
  • PCRE JIT。
  • 优化内部功能并序列化()。
  • 减少代码大小并处理数据。

有些非常简单,例如,只需要三行代码就可以在常规Perl表达式中启用JIT,这立即为几乎所有应用程序带来了可见的(2-3%)加速。 其他优化涉及某些PHP函数的某些狭窄方面,尽管这些小改进的总贡献是非常重要的,但并不是特别有趣。

发生了什么


这是WordPress / PHP 7.0上各个子系统的贡献。



虚拟机的贡献增加到50%。 Memory Manager 5% — Memory Manager, . 130 . , 10 . , Memory Manager , .


:

  • 2 .
  • MM 17 .
  • - 4 .
  • WordPress 3,5 .

2,5- , . ? , , CPU time, — , . PHP , .

PHP 7


WordPress 3.6 — . - , PHP 7 mysql, , .



, PHPNG. 2/3 . , .

, WordPress, , — 1,5 2- .

PHP 7 HHVM


HHVM.



— . . Facebook . HHVM . , , , , .



PHP 7 — . Vebia, Etsy Badoo. Highload- , .

PHP 7.0 Etsy Badoo -. Badoo .



, 2 , — 7 .

PHP 7.0. , PHP 7.1, .

PHP Russia PHP 8 . PHP, , , — 1 . , , — , , , .

Source: https://habr.com/ru/post/zh-CN444508/


All Articles