Halo, Habr! Saya hadir untuk Anda terjemahan artikel " Memahami Cara Kerja Graal - Java JIT Compiler Ditulis di Jawa ".
Pendahuluan
Salah satu alasan mengapa saya menjadi peneliti dalam bahasa pemrograman adalah bahwa, dalam komunitas besar orang yang terlibat dalam teknologi komputer, hampir semua orang menggunakan bahasa pemrograman, dan banyak yang tertarik dengan cara kerjanya. Ketika saya pertama kali menemukan pemrograman sebagai seorang anak dan berkenalan dengan bahasa pemrograman, hal pertama yang ingin saya ketahui adalah cara kerjanya, dan hal pertama yang ingin saya lakukan adalah menciptakan bahasa saya sendiri.
Dalam presentasi ini, saya akan menunjukkan kepada Anda beberapa mekanisme kerja dari bahasa yang Anda semua gunakan - Java. Keunikannya adalah bahwa saya akan menggunakan proyek yang disebut Graal , yang mengimplementasikan konsep Java di Jawa .
Graal hanyalah salah satu komponen dalam pekerjaan Java - itu adalah kompiler just-in-time . Ini adalah bagian dari JVM yang mengubah bytecode Java ke kode mesin selama eksekusi program, dan merupakan salah satu faktor yang memastikan kinerja platform yang tinggi. Bagi saya, itu juga, apa yang dianggap sebagian besar orang sebagai salah satu bagian JVM yang paling kompleks dan tidak jelas, yang berada di luar pemahaman mereka. Ubah opini ini adalah tujuan dari pidato ini.
Jika Anda tahu apa itu JVM; umumnya mengerti apa arti istilah bytecode dan kode mesin ; dan mampu membaca kode yang ditulis dalam bahasa Jawa, maka, saya harap, ini akan cukup untuk memahami materi yang disajikan.
Saya akan mulai dengan membahas mengapa kita mungkin menginginkan kompiler JIT baru untuk JVM yang ditulis di Jawa, dan setelah itu saya akan menunjukkan bahwa tidak ada yang istimewa dalam hal ini, seperti yang mungkin Anda pikirkan, dengan memecah tugas menjadi perakitan compiler, menggunakan, dan menunjukkan bahwa bahwa kodenya sama dengan aplikasi lainnya.
Saya akan menyentuh sedikit teori, dan kemudian saya akan menunjukkan bagaimana itu diterapkan selama seluruh proses kompilasi dari bytecode ke kode mesin. Saya juga akan menunjukkan beberapa detail, dan pada akhirnya kita akan berbicara tentang manfaat dari fitur ini selain mengimplementasikan Java di Java untuk kepentingannya sendiri.
Saya akan menggunakan tangkapan layar kode di Eclipse, alih-alih meluncurkannya selama presentasi, untuk menghindari masalah koding langsung yang tidak terhindarkan.
Apa itu kompiler JIT?
Saya yakin banyak dari Anda yang tahu apa itu kompiler JIT, tetapi saya masih akan menyentuh dasar-dasarnya sehingga tidak ada yang duduk di sini takut untuk menanyakan pertanyaan utama ini.
Ketika Anda menjalankan perintah javac
atau compile-on-save dalam IDE, program Java Anda mengkompilasi dari kode Java ke bytecode JVM, yang merupakan representasi biner dari program tersebut. Ini lebih kompak dan sederhana daripada kode sumber Java. Namun, prosesor reguler laptop atau server Anda tidak bisa hanya menjalankan bytecode JVM.
Untuk pengoperasian program Anda, JVM mengartikan bytecode ini. Penerjemah biasanya jauh lebih lambat daripada kode mesin yang berjalan pada prosesor. Untuk alasan ini, JVM, ketika program sedang berjalan, dapat meluncurkan kompiler lain yang mengubah bytecode Anda menjadi kode mesin, yang sudah dapat dieksekusi prosesor.
Kompiler ini, biasanya jauh lebih canggih daripada javac
, melakukan optimasi kompleks untuk menghasilkan kode mesin berkualitas tinggi sebagai hasilnya.
Mengapa menulis kompiler JIT di Java?
Sampai saat ini, implementasi JVM yang disebut OpenJDK mencakup dua kompiler JIT utama. Kompiler klien, yang dikenal sebagai C1 , dirancang untuk operasi yang lebih cepat, tetapi pada saat yang sama menghasilkan kode yang kurang dioptimalkan. Kompiler server, yang dikenal sebagai opto atau C2 , membutuhkan sedikit lebih banyak waktu untuk bekerja, tetapi menghasilkan kode yang lebih optimal.
Idenya adalah bahwa kompiler klien lebih cocok untuk aplikasi desktop, di mana jeda panjang dalam kompiler JIT tidak diinginkan, dan kompiler server untuk aplikasi server lama-bermain, yang bisa menghabiskan lebih banyak waktu kompilasi.
Hari ini mereka dapat digabungkan sehingga kode pertama kali dikompilasi oleh C1, dan kemudian, jika terus dieksekusi secara intensif dan masuk akal untuk menghabiskan waktu tambahan, - C2. Ini disebut kompilasi berjenjang .
Mari kita bahas C2, kompiler server yang melakukan lebih banyak optimasi.
Kami dapat mengkloning OpenJDK dari mirror di GitHub , atau hanya membuka pohon proyek di situs.
$ git clone https://github.com/dmlloyd/openjdk.git
Kode C2 ada di openjdk / hotspot / src / share / vm / opto .

Pertama-tama, perlu dicatat bahwa C2 ditulis dalam C ++ . Tentu saja, ini bukan sesuatu yang buruk, tetapi ada beberapa kelemahan. C ++ adalah bahasa yang tidak aman. Ini berarti bahwa kesalahan dalam C ++ dapat crash VM. Alasan untuk ini mungkin karena usia kode, tetapi kode C2 C ++ telah menjadi sangat sulit untuk dipertahankan dan dikembangkan.
Salah satu tokoh kunci di balik kompiler C2, Cliff Click mengatakan bahwa dia tidak akan pernah lagi menulis VM di C ++, dan kami mendengar tim Twitter JVM mengatakan bahwa C2 menjadi stagnan dan perlu diganti karena suatu alasan kesulitan pengembangan lebih lanjut.


https://www.youtube.com/watch?v=Hqw57GJSrac
Jadi, kembali ke pertanyaan, apa ini di Jawa yang dapat membantu menyelesaikan masalah ini? Hal yang sama yang memberikan penulisan program di Java bukannya C ++. Ini mungkin keamanan (pengecualian bukan crash, tidak ada kebocoran memori nyata atau petunjuk menggantung), pembantu yang baik (debugger, profiler, dan alat-alat seperti VisualVM ), dukungan IDE yang baik, dll.
Anda mungkin berpikir: Bagaimana saya bisa menulis sesuatu seperti kompiler Java JIT? , dan ini hanya dimungkinkan dalam bahasa pemrograman sistem tingkat rendah seperti C ++. Dalam presentasi ini, saya berharap dapat meyakinkan Anda bahwa ini sama sekali tidak! Pada dasarnya, kompiler JIT seharusnya hanya menerima bytecode JVM dan memberikan kode mesin - Anda memberinya byte[]
pada input, dan Anda juga ingin mendapatkan byte[]
. Dibutuhkan banyak pekerjaan rumit untuk melakukan ini, tetapi tidak mempengaruhi level sistem, dan karena itu tidak memerlukan bahasa sistem seperti C atau C ++.
Pengaturan Graal
Hal pertama yang kita butuhkan adalah Java 9. Antarmuka Graal yang disebut JVMCI digunakan ditambahkan ke Jawa sebagai bagian dari Antarmuka JVM Compiler JV 243 Java-Level, dan versi pertama yang memasukkannya adalah Java 9. Saya menggunakan 9 + 181 . Dalam hal ada persyaratan khusus, ada port (backports) untuk Java 8.
$ export JAVA_HOME=`pwd`/jdk9 $ export PATH=$JAVA_HOME/bin:$PATH $ java -version java version "9" Java(TM) SE Runtime Environment (build 9+181) Java HotSpot(TM) 64-Bit Server VM (build 9+181, mixed mode)
Hal berikutnya yang kita butuhkan adalah sistem build yang disebut mx
. Ini agak mirip Maven atau Gradle , tetapi kemungkinan besar Anda tidak akan memilihnya untuk aplikasi Anda. Ini mengimplementasikan fungsionalitas tertentu untuk mendukung beberapa kasus penggunaan yang kompleks, tetapi kami hanya akan menggunakannya untuk rakitan sederhana.
Anda dapat mengkloning mx
dengan GitHub. Saya menggunakan komit #7353064
. Sekarang tambahkan saja executable ke path.
$ git clone https://github.com/graalvm/mx.git $ cd mx; git checkout 7353064 $ export PATH=`pwd`/mx:$PATH
Sekarang kita perlu mengkloning Graal itu sendiri. Saya menggunakan distribusi yang disebut GraalVM versi 0.28.2 .
$ git clone https://github.com/graalvm/graal.git --branch vm-enterprise-0.28.2
Repositori ini berisi proyek-proyek lain yang tidak kami minati, jadi kami pergi saja ke sub-proyek kompiler , yang merupakan kompiler Graal JIT, dan kompilasi menggunakan mx
.
$ cd graal/compiler $ mx build
Untuk bekerja dengan kode Graal saya akan menggunakan IDE Eclipse . Saya menggunakan Eclipse 4.7.1. mx
dapat menghasilkan file proyek Eclipse untuk kita.
$ mx eclipseinit
Untuk membuka direktori graal sebagai ruang kerja, Anda perlu menjalankan File, Import ..., General, proyek yang ada dan pilih direktori graal lagi . Jika Anda tidak menjalankan Eclipse di Java 9, Anda mungkin perlu melampirkan sumber JDK.

Bagus Sekarang semuanya sudah siap, mari kita lihat cara kerjanya. Kami akan menggunakan kode yang sangat sederhana ini.
class Demo { public static void main(String[] args) { while (true) { workload(14, 2); } } private static int workload(int a, int b) { return a + b; } }
Pertama, kita kompilasi kode javac
ini, dan kemudian jalankan JVM. Pertama, saya akan menunjukkan kepada Anda bagaimana kompiler C2 JIT standar bekerja. Untuk melakukan ini, kami -XX:+PrintCompilation
menetapkan beberapa flag: -XX:+PrintCompilation
, yang diperlukan untuk JVM untuk menulis log ketika menyusun metode, dan -XX:CompileOnly=Demo::workload
, sehingga hanya metode ini yang dikompilasi. Jika tidak, maka terlalu banyak informasi akan ditampilkan, dan JVM akan lebih pintar dari yang kita butuhkan, dan akan mengoptimalkan kode yang ingin kita lihat.
$ javac Demo.java $ java \ -XX:+PrintCompilation \ -XX:CompileOnly=Demo::workload \ Demo ... 113 1 3 Demo::workload (4 bytes) ...
Saya tidak akan menjelaskan ini secara rinci, tetapi saya hanya akan mengatakan bahwa ini adalah output log yang menunjukkan bahwa metode workload
telah dikompilasi.
Sekarang, sebagai kompiler JIT Java 9 JVM kami, kami menggunakan Graal yang baru dikompilasi. Untuk melakukan ini, tambahkan beberapa flag lagi.
--module-path=...
dan --upgrade-module-path=...
tambahkan Graal ke path modul . Biarkan saya mengingatkan Anda bahwa lintasan modul muncul di Java 9 sebagai bagian dari sistem modul Jigsaw , dan untuk tujuan kami, kami dapat mempertimbangkannya dengan analogi dengan classpath .
Kita memerlukan -XX:+UnlockExperimentalVMOptions
karena fakta bahwa JVMCI (antarmuka yang digunakan oleh Graal) dalam versi ini adalah fitur eksperimental.
Bendera -XX:+EnableJVMCI
diperlukan untuk mengatakan bahwa kami ingin menggunakan JVMCI, dan -XX:+UseJVMCICompiler
- untuk mengaktifkan dan menginstal kompiler JIT baru.
Agar tidak menyulitkan contoh, dan, alih-alih menggunakan C1 dalam hubungannya dengan JVMCI, hanya memiliki kompiler JVMCI, tentukan flag -XX:-TieredCompilation
, yang akan menonaktifkan kompilasi bertahap.
Seperti sebelumnya, kami menetapkan flag -XX:+PrintCompilation
dan -XX:CompileOnly=Demo::workload
.
Seperti pada contoh sebelumnya, kita melihat bahwa satu metode dikompilasi. Tapi, kali ini, untuk kompilasi kami menggunakan Graal yang baru saja dirakit. Untuk sekarang, anggap saja kata-kataku.
$ java \ --module-path=graal/sdk/mxbuild/modules/org.graalvm.graal_sdk.jar:graal/truffle/mxbuild/modules/com.oracle.truffle.truffle_api.jar \ --upgrade-module-path=graal/compiler/mxbuild/modules/jdk.internal.vm.compiler.jar \ -XX:+UnlockExperimentalVMOptions \ -XX:+EnableJVMCI \ -XX:+UseJVMCICompiler \ -XX:-TieredCompilation \ -XX:+PrintCompilation \ -XX:CompileOnly=Demo::workload \ Demo ... 583 25 Demo::workload (4 bytes) ...
Antarmuka JVM Compiler
Tidakkah Anda berpikir bahwa kami melakukan sesuatu yang sangat tidak biasa? Kami memiliki JVM yang terinstal, dan kami mengganti kompiler JIT dengan yang baru dikompilasi tanpa mengubah apa pun di JVM itu sendiri. Fitur ini disediakan oleh antarmuka JVM baru yang disebut JVMCI, antarmuka kompiler JVM , yang, seperti yang saya katakan di atas, adalah JEP 243 dan datang di Java 9.
Idenya mirip dengan beberapa teknologi JVM lain yang ada.
Mungkin Anda pernah menemukan pemrosesan kode sumber tambahan di javac
menggunakan API pemrosesan anotasi Java . Mekanisme ini memungkinkan untuk mengidentifikasi anotasi dan model kode sumber di mana mereka digunakan, dan untuk membuat file baru berdasarkan mereka.
Juga, Anda mungkin telah menggunakan pemrosesan bytecode tambahan di JVM menggunakan agen Java . Mekanisme ini memungkinkan Anda untuk memodifikasi bytecode Java dengan memotongnya saat boot.
Gagasan JVMCI serupa. Ini memungkinkan Anda untuk menghubungkan kompiler Java JIT Anda sendiri yang ditulis dalam Java.
Sekarang saya ingin mengatakan beberapa kata tentang bagaimana saya akan menunjukkan kode selama presentasi ini. Pertama, untuk memahami gagasan itu, saya akan menunjukkan beberapa pengidentifikasi dan logika yang disederhanakan dalam bentuk teks pada slide, dan setelah itu saya akan beralih ke tangkapan layar Eclipse dan menunjukkan kode sebenarnya, yang dapat sedikit lebih rumit, tetapi gagasan utama akan tetap sama. Bagian utama dari presentasi ini dimaksudkan untuk menunjukkan bahwa sangat mungkin untuk bekerja dengan kode proyek yang sebenarnya, dan karena itu saya tidak ingin menyembunyikannya, walaupun itu bisa agak rumit.
Mulai sekarang, saya mulai menghilangkan pendapat bahwa Anda mungkin memiliki kompiler JIT yang sangat rumit.
Apa yang diterima oleh kompiler JIT untuk input? Ia menerima bytecode dari metode yang akan dikompilasi. Dan bytecode, seperti namanya, hanyalah sebuah array byte.
Apa yang dihasilkan oleh kompiler JIT? Ini memberikan kode mesin dari metode ini. Kode mesin juga hanya array byte.
Akibatnya, antarmuka yang harus diimplementasikan saat menulis kompiler JIT baru untuk menanamkannya di JVM akan terlihat seperti ini.
interface JVMCICompiler { byte[] compileMethod(byte[] bytecode); }
Oleh karena itu, jika Anda tidak dapat membayangkan bagaimana Java dapat melakukan sesuatu yang tingkat rendah seperti kompilasi JIT ke dalam kode mesin, sekarang Anda dapat melihat bahwa ini bukan pekerjaan tingkat rendah. Benar? Ini hanya fungsi dari byte[]
ke byte[]
.
Pada kenyataannya, semuanya agak lebih rumit. Hanya bytecode tidak cukup - kita juga memerlukan beberapa informasi lebih lanjut, seperti jumlah variabel lokal, ukuran stack yang diperlukan, dan informasi yang dikumpulkan oleh profiler juru bahasa untuk memahami bagaimana kode dijalankan pada kenyataannya. Oleh karena itu, bayangkan input dalam bentuk CompilationRequest
, yang akan memberi tahu kami tentang JavaMethod
yang perlu dikompilasi dan memberikan semua informasi yang diperlukan.
interface JVMCICompiler { void compileMethod(CompilationRequest request); } interface CompilationRequest { JavaMethod getMethod(); } interface JavaMethod { byte[] getCode(); int getMaxLocals(); int getMaxStackSize(); ProfilingInfo getProfilingInfo(); ... }
Juga, antarmuka tidak memerlukan pengembalian kode yang dikompilasi. Sebaliknya, API lain digunakan untuk menginstal kode mesin di JVM.
HotSpot.installCode(targetCode);
Sekarang, untuk menulis kompiler JIT baru untuk JVM, Anda hanya perlu mengimplementasikan antarmuka ini. Kami mendapatkan informasi tentang metode yang perlu dikompilasi, dan kami harus mengompilasinya menjadi kode mesin dan memanggil installCode
.
class GraalCompiler implements JVMCICompiler { void compileMethod(CompilationRequest request) { HotSpot.installCode(...); } }
Mari kita beralih ke Eclipse IDE dengan Graal dan melihat beberapa antarmuka dan kelas nyata. Seperti yang disebutkan sebelumnya, mereka akan sedikit lebih rumit, tetapi tidak banyak.



Sekarang saya ingin menunjukkan bahwa kita dapat membuat perubahan ke Graal, dan segera menggunakannya di Jawa 9. Saya akan menambahkan pesan log baru yang akan ditampilkan ketika mengkompilasi metode menggunakan Graal. Tambahkan ke metode antarmuka yang diimplementasikan, yang disebut oleh JVMCI.
class HotSpotGraalCompiler implements JVMCICompiler { CompilationRequestResult compileMethod(CompilationRequest request) { System.err.println("Going to compile " + request.getMethod().getName()); ... } }

Untuk saat ini, nonaktifkan kompilasi logging di HotSpot. Sekarang kita dapat melihat pesan kita dari versi kompiler yang dimodifikasi.
$ java \ --module-path=graal/sdk/mxbuild/modules/org.graalvm.graal_sdk.jar:graal/truffle/mxbuild/modules/com.oracle.truffle.truffle_api.jar \ --upgrade-module-path=graal/compiler/mxbuild/modules/jdk.internal.vm.compiler.jar \ -XX:+UnlockExperimentalVMOptions \ -XX:+EnableJVMCI \ -XX:+UseJVMCICompiler \ -XX:-TieredCompilation \ -XX:CompileOnly=Demo::workload \ Demo Going to compile workload
Jika Anda mencoba mengulanginya sendiri, Anda akan melihat bahwa Anda bahkan tidak perlu menjalankan sistem build kami - mx build
. Cukup, normal untuk Eclipse, kompilasi pada save . Dan tentunya kita tidak perlu membangun kembali JVM itu sendiri. Kami cukup menyematkan kompiler yang dimodifikasi di JVM yang ada.
Hitung Graal
Nah, kita tahu bahwa Graal mengubah satu byte[]
ke byte[]
lain byte[]
. Sekarang mari kita bicara tentang teori dan struktur data yang dia gunakan, karena mereka sedikit tidak biasa bahkan untuk kompiler.
Pada dasarnya, kompiler menangani program Anda. Untuk ini, program harus disajikan dalam bentuk semacam struktur data. Salah satu opsi adalah bytecode dan daftar instruksi serupa, tetapi mereka tidak terlalu ekspresif.
Sebagai gantinya, Graal menggunakan grafik untuk mewakili program Anda. Jika kita mengambil operator penjumlahan sederhana yang merangkum dua variabel lokal, maka grafik akan menyertakan satu simpul untuk memuat setiap variabel, satu simpul untuk penjumlahan, dan dua tepi yang menunjukkan bahwa hasil memuat variabel lokal adalah input ke operator penjumlahan.
Ini kadang-kadang disebut grafik ketergantungan program .
Memiliki ekspresi bentuk x + y
kami memperoleh node untuk variabel lokal x
dan y
, dan simpul dari jumlah mereka.

Tepi biru pada grafik ini menunjukkan arah aliran data dari membaca variabel lokal hingga menjumlahkan.
Juga, kita dapat menggunakan edge untuk mencerminkan urutan eksekusi program. Jika, alih-alih membaca variabel lokal, kami memanggil metode, maka kami harus mengingat urutan panggilan, dan kami tidak dapat mengatur ulang mereka (tanpa mengetahui tentang kode di dalamnya). Untuk melakukan ini, ada tepi tambahan yang menentukan urutan ini. Mereka ditampilkan dalam warna merah.

Jadi, grafik Graal, pada kenyataannya, adalah dua grafik yang digabungkan menjadi satu. Node-node itu sama, tetapi beberapa sisi mengindikasikan arah aliran data, sementara yang lain menunjukkan transfer kontrol di antara mereka.
Untuk melihat grafik Graal, Anda dapat menggunakan alat yang disebut IdealGraphVisualiser atau IGV . Startup dilakukan menggunakan perintah mx igv
.

Setelah itu, jalankan JVM dengan flag -Dgraal.Dump
.
Aliran data sederhana dapat dilihat dengan menulis ekspresi sederhana.
int average(int a, int b) { return (a + b) / 2; }

Anda dapat melihat bagaimana parameter 0
( P(0
) dan 1
( P(1)
) pergi ke input dari operasi penjumlahan, yang, bersama dengan konstanta 2
( C(2)
) pergi ke input dari operasi pembagian, setelah itu nilai ini dikembalikan.
Untuk melihat aliran data dan kontrol yang lebih kompleks, kami memperkenalkan siklus.
int average(int[] values) { int sum = 0; for (int n = 0; n < values.length; n++) { sum += values[n]; } return sum / values.length; }


Dalam hal ini, kita memiliki simpul awal dan akhir loop, membaca elemen array, dan membaca panjang array. Seperti sebelumnya, garis biru menunjukkan arah aliran data, dan garis merah menunjukkan aliran kontrol.
Sekarang Anda dapat melihat mengapa struktur data ini kadang-kadang disebut lautan node atau sup node .
Saya ingin mengatakan bahwa C2 menggunakan struktur data yang sangat mirip, dan, pada kenyataannya, C2 yang mempopulerkan gagasan penyusun lautan simpul , jadi ini bukan inovasi Graal.
Saya tidak akan menunjukkan proses pembuatan grafik ini sampai bagian selanjutnya dari presentasi, tetapi ketika Graal menerima program dalam format ini, optimasi dan kompilasi dilakukan dengan memodifikasi struktur data ini. Dan ini adalah salah satu alasan mengapa menulis kompiler JIT di Jawa masuk akal. Java adalah bahasa berorientasi objek, dan grafik adalah kumpulan objek yang dihubungkan oleh tepi dalam bentuk tautan.
Dari bytecode ke kode mesin
Mari kita lihat bagaimana ide-ide ini terlihat dalam praktek, dan ikuti beberapa langkah dari proses kompilasi.
Mendapatkan Bytecode
Kompilasi dimulai dengan bytecode. Mari kita kembali ke contoh penjumlahan kecil kami.
int workload(int a, int b) { return a + b; }
Kami akan menampilkan bytecode yang diterima pada input tepat sebelum dimulainya kompilasi.
class HotSpotGraalCompiler implements JVMCICompiler { CompilationRequestResult compileMethod(CompilationRequest request) { System.err.println(request.getMethod().getName() + " bytecode: " + Arrays.toString(request.getMethod().getCode())); ... } }
workload bytecode: [26, 27, 96, -84]
Seperti yang Anda lihat, input ke compiler adalah bytecode.
Bytecode parser dan pembuat grafik
Builder , yang menganggap array byte ini sebagai bytecode JVM, mengubahnya menjadi grafik Graal. Ini adalah semacam interpretasi abstrak - pembangun menafsirkan bytecode Java, tetapi, alih-alih memberikan nilai, memanipulasi ujung bebas tepi dan secara bertahap menghubungkannya satu sama lain.
Mari kita manfaatkan fakta bahwa Graal ditulis di Jawa dan lihat cara kerjanya menggunakan alat navigasi Eclipse. Kita tahu bahwa dalam contoh kita ada simpul tambahan, jadi mari kita cari di mana simpul itu dibuat.



Dapat dilihat bahwa mereka dibuat oleh parser bytecode, dan ini membawa kita ke IADD
pemrosesan IADD
( 96
, yang kita lihat dalam array input yang dicetak).
private void genArithmeticOp(JavaKind kind, int opcode) { ValueNode y = frameState.pop(kind); ValueNode x = frameState.pop(kind); ValueNode v; switch (opcode) { ... case LADD: v = genIntegerAdd(x, y); break; ... } frameState.push(kind, append(v)); }
Saya katakan di atas bahwa ini adalah interpretasi abstrak, karena semua ini sangat mirip dengan penerjemah bytecode. Jika itu adalah juru JVM nyata, maka itu akan mengambil dua nilai dari stack, melakukan penambahan, dan mengembalikan hasilnya. Dalam hal ini, kami menghapus dua node dari stack, yang, ketika program dimulai, akan menjadi perhitungan, tambahkan, yang merupakan hasil dari penjumlahan, node baru untuk penambahan, dan letakkan di stack.
Dengan demikian grafik dibangun Graal.
Mendapatkan kode mesin
Untuk mengonversi grafik Graal ke kode mesin, Anda perlu membuat byte untuk semua simpulnya. Ini dilakukan secara terpisah untuk setiap node dengan memanggil metode generate
.
void generate(Generator gen) { gen.emitAdd(a, b); }
Saya ulangi, di sini kita bekerja pada tingkat abstraksi yang sangat tinggi. Kami memiliki kelas yang dengannya kami mengeluarkan instruksi kode mesin tanpa merinci cara kerjanya.
emitAdd
, , , , . .
int workload(int a) { return a + 1; }
, .
void incl(Register dst) { int encode = prefixAndEncode(dst.encoding); emitByte(0xFF); emitByte(0xC0 | encode); } void emitByte(int b) { data.put((byte) (b & 0xFF)); }


, , ByteBuffer
β .
β .
class HotSpotGraalCompiler implements JVMCICompiler { CompilationResult compileHelper(...) { ... System.err.println(method.getName() + " machine code: " + Arrays.toString(result.getTargetCode())); ... } }

. HotSpot. . OpenJDK, , -, JVM, .
$ cd openjdk/hotspot/src/share/tools/hsdis $ curl -O http://ftp.heanet.ie/mirrors/gnu/binutils/binutils-2.24.tar.gz $ tar -xzf binutils-2.24.tar.gz $ make BINUTILS=binutils-2.24 ARCH=amd64 CFLAGS=-Wno-error $ cp build/macosx-amd64/hsdis-amd64.dylib ../../../../../..
: -XX:+UnlockDiagnosticVMOptions
-XX:+PrintAssembly
.
$ java \ --module-path=graal/sdk/mxbuild/modules/org.graalvm.graal_sdk.jar:graal/truffle/mxbuild/modules/com.oracle.truffle.truffle_api.jar \ --upgrade-module-path=graal/compiler/mxbuild/modules/jdk.internal.vm.compiler.jar \ -XX:+UnlockExperimentalVMOptions \ -XX:+EnableJVMCI \ -XX:+UseJVMCICompiler \ -XX:-TieredCompilation \ -XX:+PrintCompilation \ -XX:+UnlockDiagnosticVMOptions \ -XX:+PrintAssembly \ -XX:CompileOnly=Demo::workload \ Demo
.
workload machine code: [15, 31, 68, 0, 0, 3, -14, -117, -58, -123, 5, ...] ... 0x000000010f71cda0: nopl 0x0(%rax,%rax,1) 0x000000010f71cda5: add %edx,%esi ;\*iadd {reexecute=0 rethrow=0 return_oop=0} ; - Demo::workload@2 (line 10) 0x000000010f71cda7: mov %esi,%eax ;\*ireturn {reexecute=0 rethrow=0 return_oop=0} ; - Demo::workload@3 (line 10) 0x000000010f71cda9: test %eax,-0xcba8da9(%rip)
. , . generate
, .
class AddNode { void generate(...) { ... gen.emitSub(op1, op2, false) ...

, , , .
workload mechine code: [15, 31, 68, 0, 0, 43, -14, -117, -58, -123, 5, ...] 0x0000000107f451a0: nopl 0x0(%rax,%rax,1) 0x0000000107f451a5: sub %edx,%esi ;\*iadd {reexecute=0 rethrow=0 return_oop=0} ; - Demo::workload@2 (line 10) 0x0000000107f451a7: mov %esi,%eax ;\*ireturn {reexecute=0 rethrow=0 return_oop=0} ; - Demo::workload@3 (line 10) 0x0000000107f451a9: test %eax,-0x1db81a9(%rip)
, ? Graal ; ; ; . , Graal.
[26, 27, 96, -84] β [15, 31, 68, 0, 0, 43, -14, -117, -58, -123, 5, ...]
, , . Graal , .
β . .
interface Phase { void run(Graph graph); }
(canonicalisation)
. , , ( constant folding ) .
β canonical
.
interface Node { Node canonical(); }
, , , . β . -(-x)
x
.
class NegateNode implements Node { Node canonical() { if (value instanceof NegateNode) { return ((NegateNode) value).getValue(); } else { return this; } } }

Graal . , .
Java, canonical
.
Global value numbering
Global value numbering (GVN) β . a + b
, β .
int workload(int a, int b) { return (a + b) * (a + b); }
Graal . β . GVN . hash map , , .


, β , , - . , , , , β .
int workload() { return (getA() + getB()) * (getA() + getB()); }

(lock coarsening)
. . , , , ( inlining ).
void workload() { synchronized (monitor) { counter++; } synchronized (monitor) { counter++; } }
, , , , .
void workload() { monitor.enter(); counter++; monitor.exit(); monitor.enter(); counter++; monitor.exit(); }
. .
void workload() { monitor.enter(); counter++; counter++; monitor.exit(); }
Graal LockEliminationPhase
. run
, . , , .
void run(StructuredGraph graph) { for (monitorExitNode monitorExitNode : graph.getNodes(MonitorExitNode.class)) { FixedNode next = monitorExitNode.next(); if (next instanceof monitorEnterNode) { AccessmonitorNode monitorEnterNode = (AccessmonitorNode) next; if (monitorEnterNode.object() ## monitorExitNode.object()) { monitorExitNode.remove(); monitorEnterNode.remove(); } } } }

, , , , 2
.
void workload() { monitor.enter(); counter += 2; monitor.exit(); }
IGV . , , \ , , , 2
.


Graal , , , . , , , .
Graal , , , , , , .
Graal , , . ? , ?
, , . . , , , , . , , , , , .
( register allocation ). Graal , JIT-, β ( linear scan algorithm ).
, , , - , .
, , , , (.. ), . , , .
( graph scheduling ). . , . , , , .
, .
Graal?
, , , Graal β , Oracle . , Graal?
(final-tier compiler)
C JVMCI Graal HotSpot β , . ( HotSpot) Graal , .
Twitter Graal , Java 9 . : -XX:+UseJVMCICompiler
.
JVMCI , Graal JVM. (deploy) - JVM, Graal. Java-, Graal, JVM.
OpenJDK Metropolis JVM Java. Graal .

http://cr.openjdk.java.net/\~jrose/metropolis/Metropolis-Proposal.html
Graal . Graal JVM, Graal . , Graal . , - , , JNI.
Charles Nutter JRuby Graal Ruby. , - .
AOT (ahead-of-time)
Graal β Java. JVMCI , Graal, , , Graal . , Graal , JIT-.
JIT- AOT- , Graal . AOT Graal.
Java 9 JIT-, . JVM, .
AOT Java 9 Graal, Linux. , , .
. SubstrateVM β AOT-, Java- JVM . , - (statically linked) . JVM , . SubstrateVM Graal. ( just-in-time ) SubstrateVM, , Graal . Graal AOT- .
$ javac Hello.java $ graalvm-0.28.2/bin/native-image Hello classlist: 966.44 ms (cap): 804.46 ms setup: 1,514.31 ms (typeflow): 2,580.70 ms (objects): 719.04 ms (features): 16.27 ms analysis: 3,422.58 ms universe: 262.09 ms (parse): 528.44 ms (inline): 1,259.94 ms (compile): 6,716.20 ms compile: 8,817.97 ms image: 1,070.29 ms debuginfo: 672.64 ms write: 1,797.45 ms [total]: 17,907.56 ms $ ls -lh hello -rwxr-xr-x 1 chrisseaton staff 6.6M 4 Oct 18:35 hello $ file ./hello ./hellojava: Mach-O 64-bit executable x86_64 $ time ./hello Hello! real 0m0.010s user 0m0.003s sys 0m0.003s
Truffle
Graal Truffle . Truffle β JVM.
, JVM, , JIT- (, , , JIT- JVM , ). Truffle β , , Truffle, , ( partial evaluation ).
, ( inlining ) ( constant folding ) . Graal , Truffle .
Graal β Truffle. Ruby, TruffleRuby Truffle , , Graal. TruffleRuby β Ruby, 10 , , , .
https://github.com/graalvm/truffleruby
Kesimpulan
, , , JIT- Java . JIT- , , , - . , , . JIT- , byte[]
JVM byte[]
.
, Java. , C++.
Java- Graal - . , , .
. , Eclipse . (definitions), .. .
JIT JIT- JVM, JITWatch , , Graal , . , , - , Graal JVM. IDE, hello-world .
SubstrateVM Truffle, Graal, , Java . , Graal Java. , , - LLVM , , , , .
, , Graal JVM. Karena JVMCI Java 9, Graal , , Java-.
Graal β . , Graal. , !
More information about TruffleRuby
Low Overhead Polling For Ruby
Top 10 Things To Do With GraalVM
Ruby Objects as C Structs and Vice Versa
Understanding How Graal Works β a Java JIT Compiler Written in Java
Flip-Flops β the 1-in-10-million operator
Deoptimizing Ruby
Very High Performance C Extensions For JRuby+Truffle
Optimising Small Data Structures in JRuby+Truffle
Pushing Pixels with JRuby+Truffle
Tracing With Zero Overhead in JRuby+Truffle
How Method Dispatch Works in JRuby+Truffle
A Truffle/Graal High Performance Backend for JRuby