¿Cómo teóricamente la máquina virtual de hip hop (HHVM) mejora el rendimiento del tiempo de ejecución de PHP?

9

Desde un alto nivel, ¿cómo funciona Facebook, et. Al uso para mejorar el rendimiento de PHP con la máquina virtual Hip Hop?

¿En qué difiere de ejecutar código usando el motor zend tradicional? ¿Es porque los tipos se definen opcionalmente con hack que permiten técnicas de optimización previa?

Mi curiosidad surgió después de leer este artículo, la adopción de HHVM .

chrisjlee
fuente

Respuestas:

7

Reemplazaron los tracelets de TranslatorX64 con la nueva representación intermedia de HipHop (hhir), y una nueva capa de indirección en la que reside la lógica para generar hhir, que en realidad se conoce con el mismo nombre, hhir.

Desde un alto nivel, está utilizando 6 instrucciones para hacer lo que antes requería 9 instrucciones, como se señala aquí: "Comienza con los mismos controles de tipo pero el cuerpo de la traducción es 6 instrucciones, significativamente mejor que el 9 de TranslatorX64"

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

Eso es principalmente un artefacto de cómo está diseñado el sistema y es algo que planeamos limpiar eventualmente. Todo el código que queda en TranslatorX64 es una maquinaria necesaria para emitir código y vincular las traducciones; el código que entendió cómo traducir códigos de bytes individuales se ha ido de TranslatorX64.

Cuando hhir reemplazó TranslatorX64, estaba generando código que era aproximadamente un 5% más rápido y se veía significativamente mejor después de la inspección manual. Seguimos su debut en la producción con otro mini bloqueo y obtuvimos un 10% adicional en ganancias de rendimiento además de eso. Para ver algunas de estas mejoras en acción, veamos una función addPositive y parte de su traducción.

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;
    }

Esta función se parece a un montón de código PHP: recorre una matriz y hace algo con cada elemento. Centrémonos en las líneas 5 y 6 por ahora, junto con su código de bytes:

    $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)

Estas dos líneas cargan un elemento de una matriz, lo almacenan en una variable local, luego comparan el valor de ese local con 0 y saltan condicionalmente en algún lugar según el resultado. Si está interesado en obtener más detalles sobre lo que está sucediendo en el bytecode, puede hojear bytecode.specification. El JIT, tanto ahora como en los días de TranslatorX64, divide este código en dos trazos: uno solo con el CGetM, luego otro con el resto de las instrucciones (una explicación completa de por qué sucede esto no es relevante aquí, pero es principalmente porque no sabemos en tiempo de compilación cuál será el tipo de elemento de matriz). La traducción del CGetM se reduce a una llamada a una función auxiliar de C ++ y no es muy interesante, por lo que veremos el segundo brazalete. Este compromiso fue el retiro oficial de 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

Las primeras cuatro líneas son verificaciones de tipo que verifican que el valor en $ elem y el valor en la parte superior de la pila son los tipos que esperamos. Si alguno de ellos falla, saltaremos al código que desencadena una retraducción de la pulsera, utilizando los nuevos tipos para generar un fragmento de código de máquina especializado de manera diferente. Sigue la esencia de la traducción, y el código tiene mucho margen de mejora. Hay una carga muerta en la línea 8, un registro fácilmente evitable para registrar el movimiento en la línea 12, y una oportunidad para la propagación constante entre las líneas 10 y 16. Estas son todas las consecuencias del enfoque bytecode-at-a-time utilizado por TranslatorX64. Ningún compilador respetable emitiría un código como este, pero las simples optimizaciones requeridas para evitarlo simplemente no encajan en el modelo TranslatorX64.

Ahora veamos la misma pulsera traducida usando hhir, en la misma revisión hhvm:

  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

Comienza con los mismos controles de tipo pero el cuerpo de la traducción tiene 6 instrucciones, significativamente mejor que el 9 de TranslatorX64. Observe que no hay cargas muertas o registro para registrar movimientos, y el 0 inmediato del código de bytes Int 0 se propagó hacia abajo al cmp en la línea 12. Aquí está el hhir que se generó entre el brazalete y esa traducción:

  (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

Las instrucciones del código de bytes se han desglosado en operaciones más pequeñas y simples. Muchas operaciones ocultas en el comportamiento de ciertos códigos de byte están explícitamente representadas en hhir, como el LdStack en la línea 6 que forma parte de SetL. Al usar temporarios sin nombre (t1, t2, etc.) en lugar de registros físicos para representar el flujo de valores, podemos rastrear fácilmente la definición y el uso (s) de cada valor. Esto hace que sea trivial ver si el destino de una carga se usa realmente, o si una de las entradas a una instrucción es realmente un valor constante de hace 3 bytecodes. Para una explicación mucho más exhaustiva de qué es hhir y cómo funciona, eche un vistazo a ir.specification.

Este ejemplo mostró solo algunas de las mejoras que hhir realizó sobre TranslatorX64. Implementarlo en la producción y retirar TranslatorX64 en mayo de 2013 fue un gran hito, pero fue solo el comienzo. Desde entonces, hemos implementado muchas más optimizaciones que serían casi imposibles en TranslatorX64, haciendo que hhvm sea casi el doble de eficiente en el proceso. También ha sido crucial en nuestros esfuerzos para que hhvm se ejecute en procesadores ARM aislando y reduciendo la cantidad de código específico de arquitectura que necesitamos reimplementar. ¡Esté atento a una próxima publicación dedicada a nuestro puerto ARM para obtener más detalles! "

Paul W
fuente
1

En resumen: intentan minimizar el acceso aleatorio a la memoria y saltan entre piezas de código en la memoria para jugar bien con el caché de la CPU.

Según el estado de rendimiento de HHVM , optimizaron los tipos de datos utilizados con mayor frecuencia, que son cadenas y matrices, para minimizar el acceso aleatorio a la memoria. La idea es mantener las piezas de datos utilizadas juntas (como elementos en una matriz) lo más cerca posible entre sí en la memoria, idealmente de forma lineal. De esa forma, si los datos se ajustan a la memoria caché L2 / L3 de la CPU, se pueden procesar órdenes de magnitud más rápido que si estuvieran en la RAM.

Otra técnica mencionada es compilar las rutas utilizadas con mayor frecuencia en un código de tal manera que la versión compilada sea tan lineal (ei tiene la menor cantidad de "saltos") como sea posible y carga los datos de entrada / salida de la memoria tan raramente como sea posible.

scriptin
fuente