Apa yang ada di bawah kap optimasi pengoptimalan GraalVM?

Kami terus berurusan dengan karya GraalVM, dan kali ini kami memiliki terjemahan artikel oleh Aleksandar Prokopec "Di bawah tenda optimalisasi JIT GraalVM", awalnya diterbitkan di blog di Medium . Artikel ini memiliki beberapa tautan yang menarik, kemudian kami akan mencoba menerjemahkan artikel-artikel ini juga.





Terakhir kali di Medium, kami melihat masalah kinerja Java Streams API di GraalVM dibandingkan dengan Java HotSpot VM. GraalVM dicirikan oleh kinerja tinggi, dan dalam percobaan tersebut kami mencapai akselerasi dari 1,7 menjadi 5 kali. Tentu saja, nilai spesifik kenaikan kinerja akan selalu bergantung pada kode yang dijalankan dan memuat data, jadi sebelum Anda membuat kesimpulan, Anda harus mencoba menjalankan kode Anda sendiri di GraalVM.


Pada artikel ini, kita akan masuk lebih dalam ke bagian dalam GraalVM dan melihat bagaimana kompilasi JIT terjadi.



Optimalisasi JIT di GraalVM


Mari kita lihat sejumlah optimasi tingkat tinggi yang digunakan kompilator GraalVM. Dalam artikel ini kami hanya akan menyentuh pada optimasi yang paling menarik bersama dengan contoh spesifik dari pekerjaan mereka. Jika Anda ingin menggali lebih dalam, tinjauan umum yang baik tentang optimisasi kompiler GraalVM ada dalam sebuah karya berjudul “Membuat operasi pengumpulan optimal dengan kompilasi JIT agresif” .


Sebaris


Jika Anda tidak menyentuh perakitan sebelumnya, maka sebagian besar kompiler JIT di mesin virtual modern melakukan analisis internal. Ini berarti bahwa pada setiap momen tertentu ada analisis dari satu metode. Untuk alasan ini, analisis intraprosedural jauh lebih cepat daripada analisis antarproedural seluruh program, yang biasanya tidak punya waktu untuk menyelesaikan waktu yang diberikan untuk pekerjaan kompiler JIT. Dalam kompiler yang menggunakan optimasi intra-prosedural (misalnya, mengoptimalkan satu metode pada satu waktu), salah satu optimasi mendasar yang paling penting adalah inlining. Inlining penting karena secara efektif meningkatkan metode, yang berarti bahwa kompiler dapat melihat lebih banyak peluang untuk mengoptimalkan secara bersamaan beberapa potong kode yang digunakan dalam metode yang tampaknya tidak terkait.


Ambil, misalnya, metode volleyballStars dari artikel sebelumnya:



 @Benchmark public double volleyballStars() { return Arrays.stream(people) .map(p -> new Person(p.hair, p.age + 1, p.height)) .filter(p -> p.height > 198) .filter(p -> p.age >= 18 && p.age <= 21) .mapToInt(p -> p.age) .average().getAsDouble(); } 

Dalam diagram ini, kita melihat bagian representasi perantara (IR) dari metode ini di GraalVM, saat ini segera setelah penguraian bytecode Java yang sesuai.



Anda dapat menganggap IR ini sebagai semacam sintaksis abstrak tentang steroid - berkat itu, beberapa optimasi lebih mudah dilakukan. Tidak masalah bagaimana IR ini bekerja, tetapi jika Anda ingin memahami topik ini lebih dalam, Anda dapat melihat pada dokumen yang disebut "Graal IR: Representasi Antara Deklaratif yang Dapat Diperpanjang" .


Kesimpulan utama di sini adalah bahwa aliran kontrol metode yang ditunjukkan oleh simpul kuning grafik dan garis merah secara berurutan mengeksekusi metode antarmuka Stream : Stream.filter , Stream.mapToInt , IntStream.average . Kurangnya pengetahuan yang akurat tentang apa yang ada dalam kode metode ini, kompiler tidak dapat menyederhanakan metode - dan di sini inlining datang untuk menyelamatkan!


Transformasi yang disebut inlining adalah hal yang sangat dimengerti: ia hanya mencari tempat untuk memanggil metode dan menggantikannya dengan badan metode inline yang sesuai, menanamkannya di dalam. Mari kita lihat IR metode volleyballStars setelah inlining bagian dari metode. Hanya bagian yang mengikuti panggilan IntStream.average :



Diagram menunjukkan bahwa panggilan untuk getAsDouble (nomor simpul 71) telah menghilang dari IR. Perhatikan bahwa metode getAsDouble objek getAsDouble dikembalikan dari IntStream.average (panggilan terakhir dalam metode volleyballStars ) didefinisikan dalam JDK sebagai berikut:



 public double getAsDouble() { if (!isPresent) { throw new NoSuchElementException("No value present"); } return value; } 

Di sini kita dapat menemukan pemuatan isPresent bidang (nomor simpul 190, LoadField ) dan membaca bidang value . Namun, tidak ada jejak yang tersisa dari pengecualian NoSuchElementException , dan tidak ada lagi kode yang melemparnya.


Ini karena kompiler GraalVM menebak: metode volleyballStars tidak akan pernah memberikan pengecualian. Pengetahuan ini biasanya tidak tersedia selama kompilasi getAsDouble - dapat dipanggil dari berbagai tempat dalam program, dan dalam beberapa kasus lain pengecualian masih akan berfungsi. Namun, dalam metode volleyballStars tertentu, pengecualian tidak mungkin terjadi karena kumpulan bintang bola voli potensial tidak pernah kosong. Untuk alasan ini, GraalVM menghapus cabang dan menyisipkan FixedGuard - sebuah simpul yang tidak mengoptimalkan kode jika terjadi pelanggaran asumsi kami. Ini adalah contoh yang cukup minimalis, dan dalam kehidupan nyata ada banyak kasus yang lebih rumit tentang bagaimana inlining membantu optimasi lainnya.


Kita tahu bahwa pohon program panggilan biasanya sangat dalam atau bahkan tidak ada habisnya. Jadi, sebaris pada beberapa titik perlu dihentikan - ia memiliki batasan yang sangat spesifik pada waktu operasi dan ukuran memori. Mengetahui hal ini, menjadi jelas: untuk menentukan apa dan kapan harus berbaris sangat sulit.


Inlining polimorfik


Inlining hanya berfungsi jika kompilator dapat menentukan metode spesifik yang ditujukan untuk operasi pemanggilan metode. Tetapi di Jawa, biasanya ada banyak panggilan tidak langsung untuk metode yang implementasinya tidak diketahui dalam statika, yang dicari dalam runtime menggunakan pengiriman virtual.


Misalnya, ambil metode IntStream.average . Implementasinya yang khas terlihat seperti ini:



 @Override public final OptionalDouble average() { long[] avg = collect( () -> new long[2], (ll, i) -> { ll[0]++; ll[1] += i; }, (ll, rr) -> { ll[0] += rr[0]; ll[1] += rr[1]; }); return avg[0] > 0 ? OptionalDouble.of((double) avg[1] / avg[0]) : OptionalDouble.empty(); } 

Jangan biarkan kesederhanaan nyata dari kode menipu Anda! Metode ini didefinisikan dalam hal collect panggilan, dan keajaiban terjadi di sini. Pohon panggilan dari metode ini (misalnya, hierarki panggilan) tumbuh dengan cepat ketika kita masuk lebih dalam ke collect . Lihatlah diagram ini:



Mulai dari beberapa titik dalam proses melintasi pohon panggilan, inliner bersandar pada panggilan opWrapSink dari kerangka opWrapSink Java, yang merupakan metode abstrak:




 abstract<P_IN> Sink<P_IN> wrapSink(Sink<P_OUT> sink); 

Biasanya inliner tidak akan melangkah lebih jauh, karena itu adalah panggilan tidak langsung. Penentuan metode tertentu hanya akan terjadi selama pelaksanaan program, dan sekarang inlayner tidak tahu apa yang harus dikerjakan selanjutnya.


Dalam kasus GraalVM, sesuatu yang lain terjadi: ia menyimpan profil dari jenis metode target untuk setiap titik panggilan tidak langsung. Profil ini pada dasarnya hanya sebuah tabel yang memberitahukan seberapa sering setiap implementasi wrapSink . Dalam kasus kami, profil tahu tentang tiga implementasi berbeda di kelas anonim: ReferencePipeline$2 , ReferencePipeline$3 , ReferencePipeline$4 . Implementasi ini disebut dengan probabilitas masing-masing 50%, 25%, dan 25%.



 0.500000: Ljava/util/stream/ReferencePipeline$2; 0.250000: Ljava/util/stream/ReferencePipeline$4; 0.250000: Ljava/util/stream/ReferencePipeline$3; notRecorded: 0.000000 

Informasi ini memberikan bantuan yang sangat berharga kepada kompiler, memungkinkan Anda untuk menghasilkan typeswitch - switch pendek yang memeriksa jenis metode dalam runtime, kemudian memanggil metode tertentu untuk setiap kasus yang terdaftar. Gambar di bawah ini menunjukkan bagian dari tampilan perantara yang menunjukkan typeswitch (tiga if node) dengan tanda centang untuk melihat apakah tipe penerima adalah seseorang dari ReferencePipeline$2 , ReferencePipeline$3 atau ReferencePipeline$4 . Setiap panggilan langsung dalam percabangan yang sukses dari setiap pemeriksaan InstanceOf sekarang dapat sebaris atau menghubungkan beberapa optimasi tambahan untuk itu. Jika tidak ada tipe yang lulus tes, kode akan dioptimalkan dalam simpul Deopt (sebagai alternatif, Anda dapat menjalankan pengiriman virtual).



Jika Anda ingin memahami inlining polimorfik lebih dalam, saya merekomendasikan karya klasik tentang topik ini, "Inlining Metode Virtual" .


Analisis Pelarian Sebagian


Mari kita kembali ke contoh bola voli kita. Perhatikan bahwa tidak satu pun objek Person dialokasikan di dalam lambda yang dilewatkan ke fungsi map lolos dari cakupan metode volleyballStars . Dengan kata lain, saat metode volleyballStars berakhir, tidak ada area memori yang akan menunjuk ke objek bertipe Person . Khususnya, catatan nilai getHeight selanjutnya hanya digunakan untuk pemfilteran tinggi.


Di beberapa titik selama kompilasi metode volleyballStars , kita sampai pada IR yang ditunjukkan pada diagram di bawah ini. Blok dimulai dengan Begin simpul -1621 dimulai dengan alokasi objek Person (dalam simpul Alloc ), yang diinisialisasi dengan nilai bidang age dengan kenaikan 1 dan nilai sebelumnya dari bidang height . Bidang height sebelumnya dibaca di simpul LoadField -1539. Hasil alokasi dienkapsulasi di AllocatedObject -2137 dan dikirim ke pemanggilan metode accept -1625. Compiler tidak dapat melakukan apa-apa lagi saat ini - dari sudut pandangnya, objek tersebut melarikan diri dari metode volleyballStars . ( Catatan Penerjemah: "melarikan diri suatu objek" disebut "melarikan diri" dalam bahasa Inggris, maka nama pengoptimalannya adalah "analisis pelarian" ).



Setelah itu, kompiler memutuskan untuk inline panggilan accept - ini masuk akal. Akibatnya, kami tiba di IR berikut:



Dan di sini JIT compiler memulai analisis pelarian parsial: ia mengetahui bahwa AllocatedObject hanya digunakan untuk membaca bidang height (ingat, height hanya digunakan dalam kondisi penyaringan, periksa bahwa tingginya lebih dari 198). Oleh karena itu, kompiler dapat menetapkan ulang pembacaan bidang -2167 height sehingga dapat langsung bekerja dengan node yang sebelumnya ditulis ke objek Person ( Alloc -2136 node), dan ini adalah LoadField -1539 kami. Selain itu, node Alloc selanjutnya tidak pergi ke input dari node lain, jadi Anda bisa menghapusnya - ini adalah kode mati!


Optimalisasi ini, pada kenyataannya, alasan utama mengapa contoh volleyballStars mengalami akselerasi lima kali lipat setelah beralih ke GraalVM. Meskipun semua objek Person tidak diperlukan dan dibuang segera setelah pembuatan, mereka masih perlu dialokasikan di heap, ingatan mereka masih perlu diinisialisasi. Analisis pelarian sebagian memungkinkan Anda untuk menghilangkan alokasi atau menundanya dengan memindahkannya ke cabang kode di mana objek benar-benar lari dan yang terjadi jauh lebih jarang.


Anda bisa mendapatkan pemahaman yang lebih dalam tentang analisis pelarian parsial dalam sebuah makalah yang disebut Analisis Pelepasan Sebagian dan Penggantian Skalar untuk Jawa .


Ringkasan


Dalam artikel ini, kami melihat tiga optimasi GraalVM: inlining, inline polimorfik, dan analisis pelarian parsial. Ada banyak lagi optimisasi yang berbeda: promosi dan pemisahan siklus, duplikasi jalur, penomoran nilai-nilai global, konvolusi konstanta, penghapusan kode mati, eksekusi spekulatif dan sebagainya.


Jika Anda ingin mempelajari lebih lanjut tentang cara kerja GraalVM, jangan ragu untuk membuka halaman publikasi . Jika Anda ingin memastikan sendiri apakah GraalVM dapat mempercepat kode Anda, Anda dapat mengunduh binari dan mencobanya sendiri.




Dari penerjemah: bahan tambahan


Di konferensi, JPoint dan Joker sering berbicara tentang GraalVM. Misalnya, pada JPoint 2019 terakhir, Thomas Wuerthinger (Direktur Riset di Oracle Labs, yang bertanggung jawab untuk GraalVM) dan Oleg Shelaev, salah satu dari dua penginjil teknologi resmi, mengunjungi kami.

Anda dapat menonton ini dan video lainnya di saluran YouTube kami:


Kami mengingatkan Anda bahwa JPoint berikutnya akan diadakan 15-16 Mei 2020 di Moskow, dan tiket sudah dapat dibeli di situs web resmi .

Source: https://habr.com/ru/post/id485024/


All Articles