Они заменили трейлеты TranslatorX64 новым промежуточным представлением HipHop (hhir) и новым косвенным слоем, в котором находится логика для генерации hhir, которая фактически называется тем же именем hhir.
С высокого уровня, он использует 6 инструкций для выполнения того, что требовалось 9 инструкций ранее, как отмечалось здесь: «Он начинается с тех же проверок типов, но текст перевода состоит из 6 инструкций, что значительно лучше, чем 9 из TranslatorX64»
Это в основном артефакт того, как спроектирована система, и это то, что мы планируем в конечном итоге очистить. Весь код, оставленный в 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. 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 и значение в верхней части стека соответствуют ожидаемым типам. Если какой-либо из них завершится неудачно, мы перейдем к коду, который запускает ретрансляцию tracelet, используя новые типы для генерации по-разному специализированного фрагмента машинного кода. Суть перевода следует, и в коде есть много возможностей для улучшения. В строке 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. Внедрение hhir в производство и выход на пенсию TranslatorX64 в мае 2013 года было большой вехой, но это было только начало. С тех пор мы внедрили намного больше оптимизаций, которые были бы почти невозможны в TranslatorX64, что сделало hhvm почти вдвое более эффективным в этом процессе. В наших усилиях также важно, чтобы hhvm работал на процессорах ARM, изолируя и уменьшая объем специфичного для архитектуры кода, который нам нужно переопределить. Следите за предстоящим постом, посвященным нашему порту ARM, чтобы узнать больше! "