Artikel ini akan menjadi artikel kedua dalam seri optimasi kinerja JVM (artikel pertama: Portal), dan compiler Java akan menjadi konten inti yang dibahas dalam artikel ini.
Dalam artikel ini, penulis (Eva Andreasson) pertama kali memperkenalkan berbagai jenis kompiler dan membandingkan kinerja kompilasi sisi klien, kompiler sisi server, dan kompilasi multi-layer. Kemudian, di akhir artikel, beberapa metode optimasi JVM yang umum diperkenalkan, seperti penghapusan kode mati, penyematan kode, dan optimasi loop body.
Fitur Java yang paling membanggakan, "kemandirian platform", berasal dari kompiler Java. Pengembang perangkat lunak melakukan yang terbaik untuk menulis aplikasi Java terbaik, dan kompiler berjalan di belakang layar untuk menghasilkan kode yang dapat dieksekusi secara efisien berdasarkan platform target. Kompiler yang berbeda cocok untuk kebutuhan aplikasi yang berbeda, sehingga menghasilkan hasil optimasi yang berbeda. Oleh karena itu, jika Anda dapat lebih memahami cara kerja kompiler dan mengetahui lebih banyak jenis kompiler, maka Anda dapat mengoptimalkan program Java Anda dengan lebih baik.
Artikel ini menyoroti dan menjelaskan perbedaan antara berbagai kompiler mesin virtual Java. Pada saat yang sama, saya juga akan membahas beberapa solusi optimasi yang biasa digunakan oleh kompiler just-in-time (JIT).
Apa itu kompiler?
Sederhananya, kompiler mengambil program bahasa pemrograman sebagai masukan dan program bahasa lain yang dapat dieksekusi sebagai keluaran. Javac adalah kompiler yang paling umum. Itu ada di semua JDK. Javac mengambil kode Java sebagai output dan mengubahnya menjadi kode yang dapat dieksekusi JVM - bytecode. Bytecode ini disimpan dalam file yang diakhiri dengan .class dan dimuat ke lingkungan runtime java saat program java dimulai.
Bytecode tidak dapat dibaca langsung oleh CPU. Bytecode juga perlu diterjemahkan ke dalam bahasa instruksi mesin yang dapat dipahami oleh platform saat ini. Ada kompiler lain di JVM yang bertanggung jawab untuk menerjemahkan bytecode menjadi instruksi yang dapat dieksekusi oleh platform target. Beberapa kompiler JVM memerlukan beberapa tingkat tahapan kode bytecode. Misalnya, kompiler mungkin perlu melalui beberapa bentuk tahapan perantara yang berbeda sebelum menerjemahkan bytecode ke dalam instruksi mesin.
Dari perspektif platform agnostik, kami ingin kode kami sebisa mungkin agnostik platform.
Untuk mencapai hal ini, kami bekerja pada tingkat terjemahan terakhir—dari representasi bytecode terendah ke kode mesin nyata—yang benar-benar mengikat kode yang dapat dieksekusi ke arsitektur platform tertentu. Dari level tertinggi, kita dapat membagi compiler menjadi compiler statis dan compiler dinamis. Kita dapat memilih kompiler yang sesuai berdasarkan lingkungan eksekusi target, hasil pengoptimalan yang kita inginkan, dan batasan sumber daya yang harus kita penuhi. Pada artikel sebelumnya kita telah membahas secara singkat compiler statis dan compiler dinamis, dan pada bagian selanjutnya kita akan menjelaskannya lebih mendalam.
Kompilasi statis VS kompilasi dinamis
Javac yang kami sebutkan sebelumnya adalah contoh kompilasi statis. Dengan kompiler statis, kode masukan diinterpretasikan satu kali, dan keluarannya adalah bentuk program yang akan dieksekusi di masa mendatang. Kecuali Anda memperbarui kode sumber dan mengkompilasi ulang (melalui kompiler), hasil eksekusi program tidak akan pernah berubah: ini karena masukannya adalah masukan statis dan kompilernya adalah kompiler statis.
Dengan kompilasi statis, program berikut:
Copy kode kodenya sebagai berikut:
staticint add7(int x ){ kembalikan x+7;}
akan diubah menjadi bytecode seperti berikut:
Copy kode kodenya sebagai berikut:
iload0 bipush 7 ia menambahkan ireturn
Kompiler dinamis secara dinamis mengkompilasi satu bahasa ke bahasa lain. Yang disebut dinamis mengacu pada kompilasi saat program sedang berjalan - kompilasi saat berjalan! Keuntungan kompilasi dan optimasi dinamis adalah dapat menangani beberapa perubahan saat aplikasi dimuat. Runtime Java sering kali berjalan di lingkungan yang tidak dapat diprediksi atau bahkan berubah, sehingga kompilasi dinamis sangat cocok untuk runtime Java. Kebanyakan JVM menggunakan kompiler dinamis, seperti kompiler JIT. Perlu dicatat bahwa kompilasi dinamis dan pengoptimalan kode memerlukan penggunaan beberapa struktur data tambahan, thread, dan sumber daya CPU. Semakin canggih pengoptimal atau penganalisis konteks bytecode, semakin banyak sumber daya yang digunakan. Namun biaya ini tidak berarti apa-apa jika dibandingkan dengan peningkatan kinerja yang signifikan.
Jenis JVM dan Kemandirian Platform Java
Fitur umum dari semua implementasi JVM adalah mengkompilasi bytecode ke dalam instruksi mesin. Beberapa JVM menafsirkan kode ketika aplikasi dimuat dan menggunakan penghitung kinerja untuk menemukan kode "panas"; yang lain melakukannya melalui kompilasi. Masalah utama kompilasi adalah sentralisasi memerlukan banyak sumber daya, namun juga menghasilkan optimalisasi kinerja yang lebih baik.
Jika Anda masih baru mengenal Java, seluk-beluk JVM pasti akan membuat Anda bingung. Namun kabar baiknya adalah Anda tidak perlu memikirkannya! JVM akan mengelola kompilasi dan optimalisasi kode, dan Anda tidak perlu khawatir tentang instruksi mesin dan cara menulis kode agar paling sesuai dengan arsitektur platform tempat program dijalankan.
Dari bytecode Java hingga yang dapat dieksekusi
Setelah kode java Anda dikompilasi menjadi bytecode, langkah selanjutnya adalah menerjemahkan instruksi bytecode ke dalam kode mesin. Langkah ini dapat diimplementasikan melalui interpreter atau melalui compiler.
menjelaskan
Interpretasi adalah cara paling sederhana untuk mengkompilasi bytecode. Penerjemah menemukan instruksi perangkat keras yang sesuai dengan setiap instruksi bytecode dalam bentuk tabel pencarian, dan kemudian mengirimkannya ke CPU untuk dieksekusi.
Anda dapat menganggap penerjemah seperti kamus: untuk setiap kata tertentu (instruksi bytecode), ada terjemahan spesifik (instruksi kode mesin) yang sesuai dengannya. Karena interpreter langsung mengeksekusi instruksi setiap kali membacanya, metode ini tidak dapat mengoptimalkan sekumpulan instruksi. Pada saat yang sama, setiap kali bytecode dipanggil, bytecode tersebut harus segera diinterpretasikan, sehingga interpreter berjalan sangat lambat. Penerjemah mengeksekusi kode dengan cara yang sangat akurat, namun karena set instruksi keluaran tidak dioptimalkan, ini mungkin tidak menghasilkan hasil yang optimal untuk prosesor platform target.
menyusun
Kompiler memuat semua kode yang akan dieksekusi ke dalam runtime. Dengan cara ini ia dapat merujuk ke seluruh atau sebagian konteks runtime ketika menerjemahkan bytecode. Keputusan yang diambil didasarkan pada hasil analisis grafik kode. Seperti membandingkan cabang eksekusi yang berbeda dan mereferensikan data konteks runtime.
Setelah urutan bytecode diterjemahkan ke dalam set instruksi kode mesin, optimasi dapat dilakukan berdasarkan set instruksi kode mesin ini. Kumpulan instruksi yang dioptimalkan disimpan dalam struktur yang disebut buffer kode. Ketika bytecode ini dijalankan kembali, kode yang dioptimalkan dapat diperoleh langsung dari buffer kode ini dan dieksekusi. Dalam beberapa kasus, kompiler tidak menggunakan pengoptimal untuk mengoptimalkan kode, tetapi menggunakan urutan pengoptimalan baru - "penghitungan kinerja".
Keuntungan menggunakan cache kode adalah instruksi kumpulan hasil dapat segera dieksekusi tanpa perlu interpretasi ulang atau kompilasi!
Hal ini dapat sangat mengurangi waktu eksekusi, terutama untuk aplikasi Java dimana suatu metode dipanggil beberapa kali.
optimasi
Dengan diperkenalkannya kompilasi dinamis, kita mempunyai kesempatan untuk memasukkan penghitung kinerja. Misalnya, kompiler menyisipkan penghitung kinerja yang bertambah setiap kali blok bytecode (sesuai dengan metode tertentu) dipanggil. Kompiler menggunakan penghitung ini untuk menemukan "blok panas" sehingga dapat menentukan blok kode mana yang dapat dioptimalkan untuk memberikan peningkatan kinerja terbesar pada aplikasi. Data analisis kinerja runtime dapat membantu kompiler membuat lebih banyak keputusan pengoptimalan dalam keadaan online, sehingga semakin meningkatkan efisiensi eksekusi kode. Karena kami mendapatkan data analisis kinerja kode yang lebih akurat, kami dapat menemukan lebih banyak titik pengoptimalan dan membuat keputusan pengoptimalan yang lebih baik, seperti: cara mengurutkan instruksi dengan lebih baik, dan apakah akan menggunakan set instruksi yang lebih efisien apakah akan menghilangkan operasi yang berlebihan, dll.
Misalnya
Perhatikan kode java berikut. Salin kode tersebut sebagai berikut:
staticint add7(int x ){ kembalikan x+7;}
Javac akan menerjemahkannya secara statis ke dalam bytecode berikut:
Copy kode kodenya sebagai berikut:
iload0
bipush 7
ia menambahkan
kembali
Ketika metode ini dipanggil, bytecode akan dikompilasi secara dinamis ke dalam instruksi mesin. Metode ini dapat dioptimalkan ketika penghitung kinerja (jika ada) mencapai ambang batas yang ditentukan. Hasil yang dioptimalkan mungkin terlihat seperti set instruksi mesin berikut:
Copy kode kodenya sebagai berikut:
lea rax,[rdx+7] ret
Kompiler yang berbeda cocok untuk aplikasi yang berbeda
Aplikasi yang berbeda memiliki kebutuhan yang berbeda. Aplikasi sisi server perusahaan biasanya perlu dijalankan dalam waktu lama, sehingga biasanya menginginkan lebih banyak pengoptimalan kinerja; sementara applet sisi klien mungkin menginginkan waktu respons yang lebih cepat dan konsumsi sumber daya yang lebih sedikit. Mari kita bahas tiga kompiler berbeda serta kelebihan dan kekurangannya.
Kompiler sisi klien
C1 adalah kompiler pengoptimal yang terkenal. Saat memulai JVM, tambahkan parameter -client untuk memulai kompiler. Berdasarkan namanya kita dapat mengetahui bahwa C1 adalah kompiler klien. Ini ideal untuk aplikasi klien yang memiliki sedikit sumber daya sistem yang tersedia atau memerlukan startup cepat. C1 melakukan pengoptimalan kode dengan menggunakan penghitung kinerja. Ini adalah metode optimasi sederhana dengan sedikit intervensi pada kode sumber.
Kompiler sisi server
Untuk aplikasi yang berjalan lama (seperti aplikasi perusahaan sisi server), penggunaan kompiler sisi klien mungkin tidak cukup. Saat ini kita harus memilih kompiler sisi server seperti C2. Pengoptimal dapat dimulai dengan menambahkan server ke baris startup JVM. Karena sebagian besar aplikasi sisi server biasanya berjalan lama, Anda akan dapat mengumpulkan lebih banyak data pengoptimalan kinerja dengan menggunakan kompiler C2 dibandingkan aplikasi sisi klien ringan yang berjalan pendek. Oleh karena itu, Anda juga akan dapat menerapkan teknik dan algoritma optimasi yang lebih canggih.
Tip: Panaskan kompiler sisi server Anda
Untuk penerapan sisi server, kompiler mungkin memerlukan waktu untuk mengoptimalkan kode "panas" tersebut. Jadi penerapan di sisi server sering kali memerlukan fase "pemanasan". Jadi saat melakukan pengukuran kinerja pada penerapan sisi server, selalu pastikan aplikasi Anda telah mencapai kondisi stabil! Memberi kompiler waktu yang cukup untuk mengkompilasi akan membawa banyak manfaat bagi aplikasi Anda.
Kompiler sisi server dapat memperoleh lebih banyak data penyetelan kinerja dibandingkan kompiler sisi klien, sehingga dapat melakukan analisis cabang yang lebih kompleks dan menemukan jalur pengoptimalan dengan kinerja yang lebih baik. Semakin banyak data analisis kinerja yang Anda miliki, semakin baik pula hasil analisis aplikasi Anda. Tentu saja, melakukan analisis kinerja yang ekstensif memerlukan lebih banyak sumber daya kompiler. Misalnya, jika JVM menggunakan kompiler C2, JVM perlu menggunakan lebih banyak siklus CPU, cache kode yang lebih besar, dll.
Kompilasi bertingkat
Kompilasi multi-tingkat menggabungkan kompilasi sisi klien dan kompilasi sisi server. Azul adalah orang pertama yang mengimplementasikan kompilasi multi-layer di Zing JVM miliknya. Baru-baru ini, teknologi ini telah diadopsi oleh Oracle Java Hotspot JVM (setelah Java SE7). Kompilasi multi-level menggabungkan keunggulan kompiler sisi klien dan sisi server. Kompiler klien aktif dalam dua situasi: saat aplikasi dimulai, dan saat penghitung kinerja mencapai ambang batas tingkat yang lebih rendah untuk melakukan optimalisasi kinerja. Kompiler klien juga memasukkan penghitung kinerja dan menyiapkan set instruksi untuk digunakan nanti oleh kompiler sisi server untuk optimasi tingkat lanjut. Kompilasi multi-layer adalah metode analisis kinerja dengan pemanfaatan sumber daya yang tinggi. Karena mengumpulkan data selama aktivitas kompiler berdampak rendah, data ini nantinya dapat digunakan dalam pengoptimalan lebih lanjut. Pendekatan ini memberikan lebih banyak informasi daripada menganalisis penghitung menggunakan kode interpretatif.
Gambar 1 menjelaskan perbandingan kinerja interpreter, kompilasi sisi klien, kompilasi sisi server, dan kompilasi multi-layer. Sumbu X adalah waktu eksekusi (satuan waktu), dan sumbu Y adalah kinerja (jumlah operasi per satuan waktu)
Gambar 1. Perbandingan kinerja kompiler
Dibandingkan dengan kode yang diinterpretasikan murni, menggunakan kompiler sisi klien dapat menghasilkan peningkatan kinerja sekitar 5 hingga 10 kali lipat. Jumlah peningkatan kinerja yang Anda peroleh bergantung pada efisiensi kompiler, jenis pengoptimal yang tersedia, dan seberapa cocok desain aplikasi dengan platform target. Namun bagi pengembang program, hal terakhir seringkali diabaikan.
Dibandingkan dengan kompiler sisi klien, kompiler sisi server seringkali dapat memberikan peningkatan kinerja sebesar 30% hingga 50%. Dalam kebanyakan kasus, peningkatan kinerja sering kali mengorbankan konsumsi sumber daya.
Kompilasi multi-level menggabungkan keunggulan kedua kompiler. Kompilasi sisi klien memiliki waktu startup lebih pendek dan dapat melakukan optimasi cepat; kompilasi sisi server dapat melakukan operasi optimasi lebih lanjut selama proses eksekusi berikutnya.
Beberapa optimasi kompiler yang umum
Sejauh ini, kita telah membahas apa yang dimaksud dengan mengoptimalkan kode dan bagaimana serta kapan JVM melakukan optimasi kode. Selanjutnya, saya akan mengakhiri artikel ini dengan memperkenalkan beberapa metode optimasi yang sebenarnya digunakan oleh kompiler. Optimasi JVM sebenarnya terjadi pada tahap bytecode (atau tahap representasi bahasa tingkat rendah), namun bahasa Java akan digunakan di sini untuk mengilustrasikan metode optimasi ini. Tidak mungkin untuk mencakup semua metode optimasi JVM di bagian ini, tentu saja, saya berharap perkenalan ini akan menginspirasi Anda untuk mempelajari ratusan metode optimasi lebih lanjut dan berinovasi dalam teknologi compiler.
Penghapusan kode mati
Penghapusan kode mati, seperti namanya, adalah menghilangkan kode yang tidak akan pernah dieksekusi - yaitu kode "mati".
Jika kompiler menemukan beberapa instruksi yang berlebihan selama operasi, kompiler akan menghapus instruksi ini dari set instruksi eksekusi. Misalnya, dalam Listing 1, salah satu variabel tidak akan pernah digunakan setelah penugasan ke variabel tersebut, sehingga pernyataan penugasan dapat diabaikan sepenuhnya selama eksekusi. Sesuai dengan operasi pada tingkat bytecode, nilai variabel tidak perlu dimuat ke dalam register. Tidak harus memuat berarti lebih sedikit waktu CPU yang dikonsumsi, sehingga mempercepat eksekusi kode, yang pada akhirnya menghasilkan aplikasi yang lebih cepat - jika kode pemuatan dipanggil berkali-kali per detik, efek pengoptimalan akan lebih jelas.
Listing 1 menggunakan kode Java untuk mengilustrasikan contoh pemberian nilai ke variabel yang tidak akan pernah digunakan.
Listing 1. Kode kode salinan kode mati adalah sebagai berikut:
int timeToScaleMyApp(boolean endlessOfResources){
int arsitek ulang =24;
int patchByClustering =15;
int useZing =2;
jika (sumber daya tak berujung)
kembalikan Arsitek + useZing;
kalau tidak
kembalikan useZing;
}
Selama fase bytecode, jika variabel dimuat tetapi tidak pernah digunakan, kompiler dapat mendeteksi dan menghilangkan kode yang mati, seperti yang ditunjukkan pada Listing 2. Jika Anda tidak pernah melakukan operasi pemuatan ini, Anda dapat menghemat waktu CPU dan meningkatkan kecepatan eksekusi program.
Listing 2. Kode salinan kode yang dioptimalkan adalah sebagai berikut:
int timeToScaleMyApp(boolean endlessOfResources){
int reArchitect =24; //operasi yang tidak perlu dihapus di sini…
int useZing =2;
jika (sumber daya tak berujung)
kembalikan Arsitek + useZing;
kalau tidak
kembalikan useZing;
}
Penghapusan redundansi adalah metode optimasi yang meningkatkan kinerja aplikasi dengan menghapus instruksi duplikat.
Banyak optimasi mencoba menghilangkan instruksi lompat tingkat instruksi mesin (seperti JMP dalam arsitektur x86). Instruksi lompat akan mengubah register penunjuk instruksi, sehingga mengalihkan aliran eksekusi program. Instruksi lompat ini merupakan perintah yang sangat memakan sumber daya dibandingkan dengan instruksi ASSEMBLY lainnya. Makanya kami ingin mengurangi atau menghilangkan instruksi semacam ini. Penyematan kode adalah metode optimasi yang sangat praktis dan terkenal untuk menghilangkan instruksi transfer. Karena menjalankan instruksi lompat itu mahal, menyematkan beberapa metode yang sering disebut metode kecil ke dalam badan fungsi akan membawa banyak manfaat. Listing 3-5 menunjukkan manfaat penyematan.
Listing 3. Metode pemanggilan kode salin Kodenya adalah sebagai berikut:
int ketikaToEvaluateZing(int y){ return hariLeft(y)+ hariLeft(0)+ hariLeft(y+1);}
Listing 4. Kode kode salin metode yang dipanggil adalah sebagai berikut:
int hariKiri(int x){ if(x ==0) kembali0; jika tidak kembalikan x -1;}
Listing 5. Metode penyalinan kode kode sebaris adalah sebagai berikut:
int kapanToEvaluateZing(int y){
int suhu =0;
jika(y==0)
suhu +=0;
kalau tidak
suhu += y -1;
jika(0==0)
suhu +=0;
kalau tidak
suhu +=0-1;
jika(y+1==0)
suhu +=0;
kalau tidak
suhu +=(y +1)-1;
suhu kembali;
}
Pada Listing 3-5 kita dapat melihat bahwa sebuah metode kecil dipanggil tiga kali dalam badan metode yang lain, dan apa yang ingin kita ilustrasikan adalah: biaya untuk menyematkan metode yang dipanggil secara langsung ke dalam kode akan lebih kecil daripada mengeksekusi tiga lompatan. mentransfer instruksi.
Menyematkan metode yang jarang dipanggil mungkin tidak membuat perbedaan besar, namun menyematkan apa yang disebut metode "panas" (metode yang sering disebut) dapat membawa banyak peningkatan kinerja. Kode yang disematkan seringkali dapat dioptimalkan lebih lanjut, seperti yang ditunjukkan pada Listing 6.
Listing 6. Setelah kode tertanam, optimasi lebih lanjut dapat dilakukan dengan menyalin kode sebagai berikut:
int ketikaToEvaluateZing(int y){ if(y ==0)kembalikan y; elseif(y ==-1)kembalikan y -1; lain kembalikan y + y -1;}
Pengoptimalan lingkaran
Optimasi loop memainkan peran penting dalam mengurangi biaya tambahan untuk mengeksekusi badan loop. Biaya tambahan di sini mengacu pada lompatan yang mahal, banyak pemeriksaan kondisi, dan pipeline yang tidak dioptimalkan (yaitu, serangkaian set instruksi yang tidak melakukan operasi aktual dan menggunakan siklus CPU tambahan). Ada banyak jenis optimasi loop. Berikut adalah beberapa optimasi loop yang lebih populer:
Penggabungan badan perulangan: Ketika dua badan perulangan yang berdekatan mengeksekusi jumlah perulangan yang sama, kompilator akan mencoba menggabungkan kedua badan perulangan tersebut. Jika dua badan perulangan benar-benar independen satu sama lain, keduanya juga dapat dijalankan secara bersamaan (secara paralel).
Perulangan Inversi: Pada dasarnya, Anda mengganti perulangan while dengan perulangan do-sementara. Perulangan do-sementara ini ditempatkan di dalam pernyataan if. Penggantian ini akan mengurangi dua operasi lompatan; namun akan meningkatkan penilaian kondisional, sehingga meningkatkan jumlah kode. Pengoptimalan semacam ini adalah contoh bagus dalam memperdagangkan lebih banyak sumber daya untuk kode yang lebih efisien - kompiler mempertimbangkan biaya dan manfaat serta membuat keputusan secara dinamis saat runtime.
Atur ulang badan perulangan: Atur ulang badan perulangan sehingga seluruh badan perulangan dapat disimpan dalam cache.
Perluas badan perulangan: Kurangi jumlah pemeriksaan dan lompatan kondisi perulangan. Anda dapat menganggap ini sebagai menjalankan beberapa iterasi "sebaris" tanpa harus melakukan pemeriksaan bersyarat. Membuka gulungan badan perulangan juga membawa risiko tertentu, karena dapat mengurangi kinerja dengan memengaruhi alur dan sejumlah besar pengambilan instruksi yang berlebihan. Sekali lagi, kompilerlah yang memutuskan apakah akan membuka gulungan badan loop saat runtime, dan ada baiknya membuka gulungannya jika hal itu akan menghasilkan peningkatan kinerja yang lebih besar.
Di atas adalah gambaran umum tentang bagaimana kompiler pada tingkat bytecode (atau tingkat yang lebih rendah) dapat meningkatkan kinerja aplikasi pada platform target. Apa yang telah kita bahas adalah beberapa metode optimasi yang umum dan populer. Karena keterbatasan ruang, kami hanya memberikan beberapa contoh sederhana. Tujuan kami adalah membangkitkan minat Anda untuk mempelajari optimasi secara mendalam melalui pembahasan sederhana di atas.
Kesimpulan: Poin Refleksi dan Poin Penting
Pilih kompiler yang berbeda sesuai dengan tujuan yang berbeda.
1. Interpreter adalah bentuk paling sederhana untuk menerjemahkan bytecode ke dalam instruksi mesin. Implementasinya didasarkan pada tabel pencarian instruksi.
2. Kompiler dapat mengoptimalkan berdasarkan penghitung kinerja, tetapi memerlukan beberapa sumber daya tambahan (cache kode, thread pengoptimalan, dll.).
3. Kompiler klien dapat memberikan peningkatan kinerja 5 hingga 10 kali lipat dibandingkan dengan penerjemah.
4. Kompiler sisi server dapat menghasilkan peningkatan kinerja 30% hingga 50% dibandingkan dengan kompiler sisi klien, namun memerlukan lebih banyak sumber daya.
5. Kompilasi multi-layer menggabungkan keunggulan keduanya. Gunakan kompilasi sisi klien untuk waktu respons yang lebih cepat, lalu gunakan kompiler sisi server untuk mengoptimalkan kode yang sering dipanggil.
Ada banyak kemungkinan cara untuk mengoptimalkan kode di sini. Tugas penting kompiler adalah menganalisis semua metode pengoptimalan yang mungkin, dan kemudian mempertimbangkan biaya berbagai metode pengoptimalan terhadap peningkatan kinerja yang dihasilkan oleh instruksi mesin akhir.