Як віртуальна машина Hip hop (HHVM) теоретично покращує продуктивність виконання PHP?


9

З високого рівня, як це робить Facebook та ін. al використовувати для підвищення продуктивності PHP за допомогою віртуальної машини Hip Hop?

Чим він відрізняється від виконання коду за допомогою традиційного двигуна zend? Це тому, що типи необов’язково визначаються за допомогою хак, що дозволяє використовувати методи попередньої оптимізації?

Моя цікавість виникла після прочитання цієї статті, прийняття HHVM .

Відповіді:


7

Вони замінили траслети TranslatorX64 на нове проміжне представництво HipHop (hhir) та новий шар непрямості, в якому розміщена логіка для генерування hhir, що насправді називається однойменною, hhir.

З високого рівня, він використовує 6 інструкцій, щоб виконати те, що вимагало 9 інструкцій раніше, як зазначено тут: "Починається з тих же типів перевірок, але в тілі перекладу є 6 інструкцій, що значно краще, ніж 9 з TranslatorX64"

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 та умовно стрибають кудись на основі результату. Якщо вас цікавить детальніше про те, що відбувається в байт-коді, ви можете проглядати байт-код. JIT, як зараз, так і назад у TranslatorX64 днів, розбиває цей код на два сліди: один із лише CGetM, а потім інший з рештою інструкцій (повне пояснення, чому це відбувається тут, не має значення, але це здебільшого тому, що ми не знаємо під час компіляції, яким буде тип елемента масиву). Переклад CGetM зводиться до виклику допоміжної функції C ++ і не дуже цікавий, тому ми подивимось на другий траслет. Ця переконання була офіційною відставкою 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 та значення у верхній частині стека - це типи, які ми очікуємо. Якщо будь-який з них не вдається, ми перейдемо до коду, який запускає повторну трансляцію, використовуючи нові типи для створення іншої спеціалізованої частини машинного коду. М'ясо перекладу випливає, і код має багато можливостей для вдосконалення. На лінії 8 є мертве навантаження, реєстр, який легко ухиляється для реєстрації переміщення по лінії 12, і можливість постійного поширення між рядками 10 і 16. Це все наслідки підходу байт-коду за часом, який використовує TranslatorX64. Жоден респектабельний компілятор ніколи не випускає такий код, але прості оптимізації, необхідні для його уникнення, просто не вписуються в модель TranslatorX64.

Тепер давайте подивимось той самий траслет, перекладений за допомогою hhir, при тій же редакції 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

Він починається з тих же типів перевірок, але в основі перекладу є 6 інструкцій, що значно краще, ніж 9 з TranslatorX64. Зауважте, що немає мертвих вантажів або зареєструватись для реєстрації переміщень, і негайний 0 від байтового коду Int 0 поширився вниз до cmp у рядку 12. Ось 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, наприклад LdStack у рядку 6, який є частиною SetL. Використовуючи безіменні часові (t1, t2 тощо) замість фізичних регістрів для представлення потоку значень, ми можемо легко відстежувати визначення та використання (-ів) кожного значення. Це робить тривіальним питання про те, чи фактично використовується призначення навантаження, або чи один із входів до інструкції дійсно є постійним значенням від 3-х байт-кодів тому. Щоб отримати більш ретельне пояснення, що таке hhir і як він працює, подивіться на ir.specification.

Цей приклад показав лише декілька поліпшень, зроблених за TranslatorX64. Перехід на виробництво та звільнення TranslatorX64 у травні 2013 року було чудовою віхою, але це було лише початком. З того часу ми впровадили ще багато оптимізацій, які були б майже неможливими в TranslatorX64, що робить hhvm майже вдвічі ефективнішим у цьому процесі. Це також має вирішальне значення в наших зусиллях, щоб зробити hhvm запущеним на процесорах ARM, виділивши та зменшивши кількість коду, що стосується архітектури, який нам потрібно повторно реалізувати. Слідкуйте за наступною публікацією, присвяченою нашому порту ARM, щоб отримати детальну інформацію! "


1

Коротше кажучи: вони намагаються мінімізувати випадковий доступ до пам'яті і перескакують між фрагментами коду в пам'яті, щоб гарно грати з кешем процесора.

Відповідно до HHVM Performance Status, вони оптимізували найбільш часто використовувані типи даних, це рядки та масиви, щоб мінімізувати доступ до випадкової пам'яті. Ідея полягає в тому, щоб фрагменти даних, які використовуються разом (як елементи в масиві), максимально наближали один до одного в пам'яті, в ідеалі лінійно. Таким чином, якщо дані вписуються в кеш-пам'ять процесора L2 / L3, вони можуть оброблятися на порядки швидше, ніж якби вони були в оперативній пам'яті.

Ще одна згадана техніка - це компілювання найбільш часто використовуваних контурів у коді таким чином, що компільована версія є максимально лінійною (ei має найменший обсяг "стрибків") і завантажує дані в / з пам'яті якомога рідше.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.