Bagaimana mesin virtual Hip hop (HHVM) secara teoritis meningkatkan kinerja runtime PHP?

9

Dari level tinggi, bagaimana Facebook, et. al digunakan untuk meningkatkan kinerja PHP dengan Hip Hop Virtual Machine?

Apa bedanya dengan mengeksekusi kode menggunakan mesin zend tradisional? Apakah ini karena jenis ditentukan dengan hack yang memungkinkan untuk teknik pra-optimasi?

Keingintahuan saya muncul setelah membaca artikel ini, adopsi HHVM .

chrisjlee
sumber

Jawaban:

7

Mereka mengganti tracelet TranslatorX64 dengan Representasi Menengah HipHop yang baru (hhir), dan lapisan tipuan yang baru di mana terdapat logika untuk menghasilkan hhir, yang sebenarnya disebut dengan nama yang sama, hhir.

Dari level tinggi, ia menggunakan 6 instruksi untuk melakukan apa yang diperlukan 9 instruksi sebelumnya, seperti disebutkan di sini: "Ini dimulai dengan typechecks yang sama tetapi isi terjemahannya adalah 6 instruksi, jauh lebih baik daripada 9 dari TranslatorX64"

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

Itu sebagian besar artefak tentang bagaimana sistem dirancang dan merupakan sesuatu yang kami rencanakan untuk akhirnya dibersihkan. Semua kode yang tersisa di TranslatorX64 adalah mesin yang diperlukan untuk memancarkan kode dan menghubungkan terjemahan secara bersamaan; kode yang mengerti cara menerjemahkan bytecodes hilang dari TranslatorX64.

Ketika hhir menggantikan TranslatorX64, itu menghasilkan kode yang kira-kira 5% lebih cepat dan tampak secara signifikan lebih baik pada inspeksi manual. Kami menindaklanjuti debut produksinya dengan mini-lockdown lainnya dan mendapat tambahan keuntungan kinerja 10% di atas itu. Untuk melihat beberapa peningkatan ini dalam tindakan, mari kita lihat fungsi addPositive dan bagian dari terjemahannya.

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

Fungsi ini terlihat seperti banyak kode PHP: ia berputar di atas array dan melakukan sesuatu dengan setiap elemen. Mari kita fokus pada baris 5 dan 6 untuk saat ini, bersama dengan bytecode mereka:

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

Dua baris ini memuat elemen dari array, menyimpannya dalam variabel lokal, lalu membandingkan nilai lokal itu dengan 0 dan melompat secara kondisional di suatu tempat berdasarkan hasilnya. Jika Anda tertarik lebih detail tentang apa yang terjadi dalam bytecode, Anda dapat membaca bytecode.specification. JIT, baik sekarang dan kembali di hari-hari TranslatorX64, memecah kode ini menjadi dua jejak: satu dengan hanya CGetM, kemudian yang lain dengan sisa instruksi (penjelasan lengkap mengapa ini terjadi tidak relevan di sini, tapi itu sebagian besar karena kita tidak tahu pada waktu kompilasi apa jenis elemen array akan). Terjemahan CGetM bermuara pada panggilan ke fungsi pembantu C ++ dan tidak terlalu menarik, jadi kita akan melihat tracelet kedua. Komitmen ini adalah pensiun resmi 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

Empat baris pertama adalah typechecks yang memverifikasi bahwa nilai dalam $ elem dan nilai di atas tumpukan adalah jenis yang kita harapkan. Jika salah satu dari mereka gagal, kami akan melompat ke kode yang memicu retranslation tracelet, menggunakan tipe baru untuk menghasilkan potongan kode mesin yang berbeda. Daging terjemahan mengikuti, dan kode memiliki banyak ruang untuk perbaikan. Ada dead load pada baris 8, register yang mudah dihindari untuk register move pada baris 12, dan kesempatan untuk propagasi konstan antara baris 10 dan 16. Ini semua adalah konsekuensi dari pendekatan bytecode-at-a-time yang digunakan oleh TranslatorX64. Tidak ada kompiler terhormat yang akan memancarkan kode seperti ini, tetapi optimasi sederhana yang diperlukan untuk menghindarinya tidak cocok dengan model TranslatorX64.

Sekarang mari kita lihat tracelet yang sama yang diterjemahkan menggunakan hhir, pada revisi hhvm yang sama:

  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

Ini dimulai dengan typechecks yang sama tetapi isi terjemahannya adalah 6 instruksi, jauh lebih baik daripada 9 dari TranslatorX64. Perhatikan bahwa tidak ada beban mati atau register untuk mendaftar gerakan, dan kode 0 langsung dari kode byt Int 0 disebarkan ke cmp pada baris 12. Inilah hhir yang dihasilkan antara tracelet dan terjemahan itu:

  (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

Instruksi bytecode telah dipecah menjadi operasi yang lebih kecil, lebih sederhana. Banyak operasi yang tersembunyi dalam perilaku bytecode tertentu secara eksplisit diwakili dalam hhir, seperti LdStack on line 6 yang merupakan bagian dari SetL. Dengan menggunakan temporari tanpa nama (t1, t2, dll ...) alih-alih register fisik untuk mewakili aliran nilai, kita dapat dengan mudah melacak definisi dan penggunaan dari setiap nilai. Ini membuatnya sepele untuk melihat apakah tujuan suatu beban benar-benar digunakan, atau jika salah satu input ke instruksi benar-benar merupakan nilai konstan dari 3 bytecode yang lalu. Untuk penjelasan yang jauh lebih menyeluruh tentang apa dan bagaimana cara kerjanya, lihat ir.spesifikasi.

Contoh ini menunjukkan hanya beberapa peningkatan yang dilakukan oleh TranslatorX64. Memperoleh hhir yang digunakan untuk produksi dan pensiun TranslatorX64 pada Mei 2013 adalah tonggak pencapaian yang bagus, tetapi itu baru permulaan. Sejak itu, kami telah menerapkan lebih banyak optimasi yang hampir tidak mungkin dilakukan di TranslatorX64, menjadikan hhvm hampir dua kali lebih efisien dalam prosesnya. Ini juga sangat penting dalam upaya kami untuk menjalankan hhvm pada prosesor ARM dengan mengisolasi dan mengurangi jumlah kode khusus arsitektur yang perlu kami implementasikan kembali. Tonton pos mendatang yang ditujukan untuk port ARM kami untuk detail lebih lanjut! "

Paul W.
sumber
1

Singkatnya: mereka mencoba meminimalkan akses memori acak dan melompat di antara potongan-potongan kode dalam memori untuk bermain dengan baik dengan cache CPU.

Menurut Status Kinerja HHVM mereka mengoptimalkan tipe data yang paling sering digunakan, yaitu string dan array, untuk meminimalkan akses memori acak. Idenya adalah untuk menjaga potongan data yang digunakan bersama (seperti item dalam array) sedekat mungkin satu sama lain dalam memori, idealnya secara linier. Dengan begitu, jika data cocok dengan cache CPU L2 / L3, maka dapat diproses urutan besarnya lebih cepat daripada jika berada di RAM.

Teknik lain yang disebutkan adalah mengkompilasi jalur yang paling sering digunakan dalam suatu kode sedemikian rupa sehingga versi yang dikompilasi adalah linier (ei memiliki jumlah "lompatan" yang sesedikit mungkin) dan memuat data ke dalam atau keluar dari memori sesering mungkin.

skrip
sumber