Вони замінили траслети 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 та умовно стрибають кудись на основі результату. Якщо вас цікавить детальніше про те, що відбувається в байт-коді, ви можете проглядати байт-код. 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, щоб отримати детальну інформацію! "