理论上,嘻哈虚拟机(HHVM)如何提高PHP运行时性能?


9

从较高的角度来看,Facebook的表现如何。可以用于通过Hip Hop虚拟机提高PHP性能?

它与使用传统的zend引擎执行代码有何不同?是因为可以使用hack定义类型,这些类型允许进行预优化技术?

阅读本文HHVM的采用后,我产生了好奇。

Answers:


7

他们用新的HipHop中间表示(hhir)和新的间接层(其中包含生成hhir的逻辑)替换了TranslatorX64的tracelet,该逻辑实际上是由同名hhir引用的。

从高层次来看,它使用6条指令来执行之前需要的9条指令,如此处所述:“它以相同的类型检查开始,但翻译的主体为6条指令,明显优于TranslatorX64的9条指令”

http://hhvm.com/blog/2027/faster-and-cheaper-the-evolution-of-the-hhvm-jit

这主要是系统设计的人工产物,我们计划最终对其进行清理。TranslatorX64上剩下的所有代码都是发出代码并将翻译链接在一起的机器。TranslatorX64不再提供理解如何翻译单个字节码的代码。

当hhir替换TranslatorX64时,它生成的代码快大约5%,并且在手动检查时看起来更好。我们在产品首次亮相后又进行了一次迷你锁定,并在此基础上又获得了10%的性能提升。要查看其中的一些改进,让我们看一下addPositive函数及其转换的一部分。

function addPositive($arr) {
      $n = count($arr);
      $sum = 0;
      for ($i = 0; $i < $n; $i++) {
        $elem = $arr[$i];
        if ($elem > 0) {
          $sum = $sum + $elem;
        }
      }
      return $sum;
    }

该函数看起来像很多PHP代码:它遍历数组,并对每个元素执行某些操作。现在让我们关注第5和第6行以及它们的字节码:

    $elem = $arr[$i];
    if ($elem > 0) {
  // line 5
   85: CGetM <L:0 EL:3>
   98: SetL 4
  100: PopC
  // line 6
  101: Int 0
  110: CGetL2 4
  112: Gt
  113: JmpZ 13 (126)

这两行从数组中加载一个元素,将其存储在局部变量中,然后将该局部变量的值与0比较,并根据结果有条件地跳转到某个地方。如果您对字节码中发生的事情有更多的细节感兴趣,可以浏览bytecode.specification。现在和在TranslatorX64天内的JIT都将这段代码分为两个tracelet:一个仅包含CGetM,另一个则包含其余的指令(此处为何发生这种情况的完整说明与之无关,但是主要是因为我们在编译时不知道数组元素的类型是什么。CGetM的翻译可以归结为对C ++辅助函数的调用,并不是很有趣,因此我们将研究第二个tracelet。这是TranslatorX64的正式退休,

  cmpl  $0xa, 0xc(%rbx)
  jnz 0x276004b2
  cmpl  $0xc, -0x44(%rbp)
  jnle 0x276004b2
101: SetL 4
103: PopC
  movq  (%rbx), %rax
  movq  -0x50(%rbp), %r13
104: Int 0
  xor %ecx, %ecx
113: CGetL2 4
  mov %rax, %rdx
  movl  $0xa, -0x44(%rbp)
  movq  %rax, -0x50(%rbp)
  add $0x10, %rbx    
  cmp %rcx, %rdx    
115: Gt
116: JmpZ 13 (129)
  jle 0x7608200

前四行是类型检查,用来验证$ elem中的值和堆栈顶部的值是否是我们期望的类型。如果它们中的任何一个失败,我们将跳转到触发重新生成Tracelet的代码,并使用新类型生成不同专业的机器代码。翻译的内容紧随其后,并且代码有很大的改进空间。第8行有一个空载,第12行的寄存器移动很容易避免,并且第10行和第16行之间有不断传播的机会。这都是TranslatorX64一次使用字节码方法的结果。没有任何受人尊敬的编译器会发出这样的代码,但是为避免这种情况而进行的简单优化根本不适合TranslatorX64模型。

现在,让我们看看在相同的hhvm修订版中,使用hhir翻译的同一个tracelet:

  cmpl  $0xa, 0xc(%rbx)
  jnz 0x276004bf
  cmpl  $0xc, -0x44(%rbp)
  jnle 0x276004bf
101: SetL 4
  movq  (%rbx), %rcx
  movl  $0xa, -0x44(%rbp)
  movq  %rcx, -0x50(%rbp)
115: Gt    
116: JmpZ 13 (129)
  add $0x10, %rbx
  cmp $0x0, %rcx    
  jle 0x76081c0

它以相同的类型检查开头,但翻译的主体为6条指令,明显优于TranslatorX64的9条指令。请注意,没有静载或寄存器来记录移动,并且从Int 0字节码开始的立即数0向下传播到第12行的cmp。这是在tracelet和该转换之间生成的hhir:

  (00) DefLabel    
  (02) t1:FramePtr = DefFP
  (03) t2:StkPtr = DefSP<6> t1:FramePtr
  (05) t3:StkPtr = GuardStk<Int,0> t2:StkPtr
  (06) GuardLoc<Uncounted,4> t1:FramePtr
  (11) t4:Int = LdStack<Int,0> t3:StkPtr
  (13) StLoc<4> t1:FramePtr, t4:Int
  (27) t10:StkPtr = SpillStack t3:StkPtr, 1
  (35) SyncABIRegs t1:FramePtr, t10:StkPtr
  (36) ReqBindJmpLte<129,121> t4:Int, 0

字节码指令已分解为更小,更简单的操作。隐藏在某些字节码行为中的许多操作都在hhir中明确表示,例如SetL的第6行上的LdStack。通过使用未命名的临时变量(t1,t2等)代替物理寄存器来表示值的流,我们可以轻松地跟踪每个值的定义和使用。这使得琐碎的事情变得很简单:看看是否实际使用了加载的目的地,或者指令的输入之一实际上是3个字节码之前的常量值。有关hhir是什么以及它如何工作的更详尽的解释,请查看ir.specification。

此示例仅显示了与TranslatorX64相比所做的一些改进。在2013年5月将其部署到生产环境中并淘汰TranslatorX64是一个重要的里程碑,但这仅仅是个开始。从那时起,我们实现了更多的优化,这在TranslatorX64中几乎是不可能的,从而使hhvm的效率几乎提高了一倍。通过隔离和减少我们需要重新实现的特定于体系结构的代码量,使hhvm在ARM处理器上运行对于我们的工作也至关重要。请关注即将发布的专门讨论我们的ARM端口的文章,以了解更多详细信息!”


1

简而言之:他们试图最大程度地减少对随机内存的访问,并在内存中的代码段之间跳转以与CPU高速缓存很好地配合使用。

根据HHVM性能状态,他们优化了最常用的数据类型(字符串和数组),以最大程度地减少对随机存储器的访问。想法是使一起使用的数据块(如数组中的项)在内存中保持尽可能彼此接近,最好是线性方式。这样,如果数据适合CPU L2 / L3缓存,则处理速度要比RAM中的数据快几个数量级。

提到的另一种技术是以这种方式编译代码中最常用的路径,即编译版本尽可能地线性(ei具有最少的“跳转”数量),并尽可能少地将数据加载到内存中/从内存中加载出去。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.