
Mesin virtual bahasa pemrograman telah menjadi sangat luas dalam beberapa dekade terakhir. Cukup banyak waktu telah berlalu sejak presentasi Java Virtual Machine di paruh kedua tahun 90-an, dan aman untuk mengatakan bahwa byte-code interpreter bukanlah masa depan, tetapi masa kini.
Tetapi teknik ini, menurut pendapat saya, hampir universal, dan memahami prinsip-prinsip dasar pengembangan juru bahasa berguna tidak hanya untuk pencipta penantang berikutnya untuk judul "Bahasa of the Year" menurut TIOBE , tetapi untuk setiap programmer pada umumnya.
Singkatnya, jika Anda tertarik untuk mempelajari bagaimana bahasa pemrograman favorit kami menambah angka, pengembang mesin virtual apa yang masih berdebat dan bagaimana cara mencocokkan string dan ekspresi reguler tanpa rasa sakit, saya minta kucing.
Bagian satu, pengantar (saat ini)
Bagian Dua, Mengoptimalkan
Bagian Tiga, Diterapkan
Latar belakang
Salah satu sistem yang ditulis sendiri dari departemen Business Intelligence perusahaan kami memiliki antarmuka dalam bentuk bahasa permintaan yang sederhana. Dalam versi pertama sistem, bahasa ini ditafsirkan dengan cepat, tanpa kompilasi, langsung dari baris input dengan permintaan. Versi parser kedua akan sudah bekerja dengan bytecode perantara, yang akan memungkinkan Anda untuk memisahkan bahasa query dari eksekusi dan sangat menyederhanakan kode.
Dalam proses mengerjakan versi kedua sistem, saya berlibur selama satu atau dua jam setiap hari, saya teralihkan dari urusan keluarga untuk mempelajari materi tentang arsitektur dan kinerja penerjemah bytecode. Saya memutuskan untuk membagikan catatan dan contoh penerjemah yang dihasilkan kepada pembaca Habr sebagai serangkaian artikel.
Yang pertama dari mereka menyajikan lima mesin virtual kecil (hingga ratusan baris kode C sederhana), yang masing-masing mengungkapkan aspek tertentu dari pengembangan penerjemah tersebut.
Di mana kode byte digunakan dalam bahasa pemrograman?
Banyak sekali mesin virtual, set instruksi virtual yang paling beragam selama beberapa dekade terakhir, telah ditemukan. Wikipedia mengklaim bahwa bahasa pemrograman pertama mulai dikompilasi ke dalam berbagai representasi menengah yang disederhanakan pada tahun 60an di abad lalu. Beberapa kode byte pertama dikonversi menjadi kode mesin dan dieksekusi oleh prosesor nyata, sementara yang lain ditafsirkan dengan cepat oleh prosesor virtual.
Popularitas set instruksi virtual sebagai representasi perantara kode disebabkan oleh tiga alasan:
- Program Bytecode mudah dipindahkan ke platform baru.
- Penerjemah bytecode lebih cepat daripada penafsir dari pohon kode sintaks.
- Anda dapat mengembangkan mesin virtual sederhana hanya dalam beberapa jam.
Mari kita membuat beberapa mesin virtual C sederhana dan menggunakan contoh-contoh ini untuk menyoroti aspek teknis utama penerapan mesin virtual.
Kode sampel lengkap tersedia di GitHub . Contoh dapat dikompilasi dengan GCC yang relatif segar:
gcc interpreter-basic-switch.c -o interpreter ./interpreter
Semua contoh memiliki struktur yang sama: pertama datang kode mesin virtual itu sendiri, lalu fungsi utama dengan pernyataan yang memeriksa operasi kode. Saya mencoba mengomentari dengan jelas opcodes dan tempat-tempat utama dari penerjemah. Saya harap artikel ini dapat dimengerti bahkan oleh orang-orang yang tidak menulis dalam C setiap hari.
Penerjemah bytecode termudah di dunia
Seperti yang saya katakan, juru bahasa paling sederhana sangat mudah dibuat. Komentar tepat di belakang daftar, tetapi mari kita mulai langsung dengan kode:
struct { uint8_t *ip; uint64_t accumulator; } vm; typedef enum { OP_INC, OP_DEC, OP_DONE } opcode; typedef enum interpret_result { SUCCESS, ERROR_UNKNOWN_OPCODE, } interpret_result; void vm_reset(void) { puts("Reset vm state"); vm = (typeof(vm)) { NULL }; } interpret_result vm_interpret(uint8_t *bytecode) { vm_reset(); puts("Start interpreting"); vm.ip = bytecode; for (;;) { uint8_t instruction = *vm.ip++; switch (instruction) { case OP_INC: { vm.accumulator++; break; } case OP_DEC: { vm.accumulator--; break; } case OP_DONE: { return SUCCESS; } default: return ERROR_UNKNOWN_OPCODE; } } return SUCCESS; }
Ada kurang dari seratus baris, tetapi semua atribut karakteristik mesin virtual diwakili. Mesin memiliki register tunggal ( vm.accumulator
), tiga operasi (register increment, register decrement, dan penyelesaian eksekusi program) dan sebuah pointer ke instruksi saat ini ( vm.ip
).
Setiap operasi (eng. Kode operasi , atau opcode ) dikodekan dengan satu byte, dan penjadwalan dilakukan dengan menggunakan switch
biasa dalam fungsi vm_interpret
. Cabang-cabang dalam switch
berisi logika operasi, yaitu, mereka mengubah keadaan register atau menyelesaikan eksekusi program.
Operasi ditransfer ke fungsi vm_interpret
dalam bentuk array byte - bytecode (Eng. Bytecode ) - dan secara berurutan dieksekusi sampai operasi OP_DONE
mesin virtual ( OP_DONE
) OP_DONE
.
Aspek kunci dari mesin virtual adalah semantik, yaitu serangkaian operasi yang mungkin ada di dalamnya. Dalam hal ini, hanya ada dua operasi, dan mereka mengubah nilai register tunggal.
Beberapa peneliti ( Teknik Abstraksi dan Optimasi Mesin-Virtual , 2009) mengusulkan membagi mesin virtual menjadi yang tingkat tinggi dan tingkat rendah sesuai dengan kedekatan semantik mesin virtual dengan semantik mesin fisik di mana bytecode akan dieksekusi.
Dalam kasus ekstrem, bytecode mesin virtual level rendah dapat sepenuhnya mengulangi kode mesin dari mesin fisik dengan RAM yang disimulasikan, satu set register penuh, instruksi untuk bekerja dengan stack, dan sebagainya. Mesin virtual Bochs , misalnya, mengulangi set instruksi arsitektur x86.
Dan sebaliknya: operasi mesin virtual tingkat tinggi sangat mencerminkan semantik bahasa pemrograman khusus yang dikompilasi menjadi bytecode. Jadi, bekerja, misalnya, SQLite, Gawk dan banyak versi Prolog.
Posisi menengah ditempati oleh penerjemah bahasa pemrograman serba guna yang memiliki elemen level tinggi dan rendah. Java Virtual Machine paling populer memiliki instruksi level rendah untuk bekerja dengan stack dan dukungan bawaan untuk pemrograman berorientasi objek dengan alokasi memori otomatis.
Kode di atas lebih cenderung menjadi yang paling primitif dari mesin virtual tingkat rendah: setiap instruksi virtual adalah pembungkus lebih dari satu atau dua instruksi fisik, register virtual sepenuhnya konsisten dengan satu register prosesor "besi".
Argumen instruksi bytecode
Kita dapat mengatakan bahwa satu-satunya register dalam contoh mesin virtual kami adalah argumen dan nilai balik dari semua instruksi yang dieksekusi. Namun, kami mungkin merasa berguna untuk menyampaikan argumen dalam instruksi. Salah satu caranya adalah dengan meletakkannya dalam bytecode secara langsung.
Kami akan memperluas contoh dengan memperkenalkan instruksi (OP_ADDI, OP_SUBI) yang mengambil argumen dalam bentuk byte segera setelah opcode:
struct { uint8_t *ip; uint64_t accumulator; } vm; typedef enum { OP_INC, OP_DEC, OP_ADDI, OP_SUBI, OP_DONE } opcode; typedef enum interpret_result { SUCCESS, ERROR_UNKNOWN_OPCODE, } interpret_result; void vm_reset(void) { puts("Reset vm state"); vm = (typeof(vm)) { NULL }; } interpret_result vm_interpret(uint8_t *bytecode) { vm_reset(); puts("Start interpreting"); vm.ip = bytecode; for (;;) { uint8_t instruction = *vm.ip++; switch (instruction) { case OP_INC: { vm.accumulator++; break; } case OP_DEC: { vm.accumulator--; break; } case OP_ADDI: { uint8_t arg = *vm.ip++; vm.accumulator += arg; break; } case OP_SUBI: { uint8_t arg = *vm.ip++; vm.accumulator -= arg; break; } case OP_DONE: { return SUCCESS; } default: return ERROR_UNKNOWN_OPCODE; } } return SUCCESS; }
Instruksi baru (lihat fungsi vm_interpret
) membaca argumen mereka dari bytecode dan menambahkannya ke register / kurangi dari register.
Argumen seperti itu disebut argumen langsung , karena terletak langsung di array opcode. Keterbatasan utama dalam implementasi kami adalah bahwa argumennya adalah satu byte dan hanya dapat mengambil 256 nilai.
Di mesin virtual kami, kisaran nilai argumen instruksi yang mungkin tidak memainkan peran besar. Tetapi jika mesin virtual akan digunakan sebagai juru bahasa nyata, maka masuk akal untuk menyulitkan bytecode dengan menambahkan tabel konstanta yang terpisah dari array opcode dan instruksi dengan argumen langsung yang sesuai dengan alamat argumen ini di tabel konstanta.
Mesin tumpukan
Instruksi dalam mesin virtual kami yang sederhana selalu bekerja dengan satu register dan tidak dapat mengirimkan data satu sama lain dengan cara apa pun. Selain itu, argumen ke instruksi hanya bisa langsung, dan, katakanlah, operasi penambahan atau perkalian mengambil dua argumen.
Sederhananya, kami tidak memiliki cara untuk mengevaluasi ekspresi kompleks. Untuk mengatasi masalah ini, diperlukan mesin bertumpuk, yaitu mesin virtual dengan tumpukan terintegrasi:
#define STACK_MAX 256 struct { uint8_t *ip; uint64_t stack[STACK_MAX]; uint64_t *stack_top; uint64_t result; } vm; typedef enum { OP_PUSHI, OP_ADD, OP_SUB, OP_DIV, OP_MUL, OP_POP_RES, OP_DONE, } opcode; typedef enum interpret_result { SUCCESS, ERROR_DIVISION_BY_ZERO, ERROR_UNKNOWN_OPCODE, } interpret_result; void vm_reset(void) { puts("Reset vm state"); vm = (typeof(vm)) { NULL }; vm.stack_top = vm.stack; } void vm_stack_push(uint64_t value) { *vm.stack_top = value; vm.stack_top++; } uint64_t vm_stack_pop(void) { vm.stack_top--; return *vm.stack_top; } interpret_result vm_interpret(uint8_t *bytecode) { vm_reset(); puts("Start interpreting"); vm.ip = bytecode; for (;;) { uint8_t instruction = *vm.ip++; switch (instruction) { case OP_PUSHI: { uint8_t arg = *vm.ip++; vm_stack_push(arg); break; } case OP_ADD: { uint64_t arg_right = vm_stack_pop(); uint64_t arg_left = vm_stack_pop(); uint64_t res = arg_left + arg_right; vm_stack_push(res); break; } case OP_SUB: { uint64_t arg_right = vm_stack_pop(); uint64_t arg_left = vm_stack_pop(); uint64_t res = arg_left - arg_right; vm_stack_push(res); break; } case OP_DIV: { uint64_t arg_right = vm_stack_pop(); if (arg_right == 0) return ERROR_DIVISION_BY_ZERO; uint64_t arg_left = vm_stack_pop(); uint64_t res = arg_left / arg_right; vm_stack_push(res); break; } case OP_MUL: { uint64_t arg_right = vm_stack_pop(); uint64_t arg_left = vm_stack_pop(); uint64_t res = arg_left * arg_right; vm_stack_push(res); break; } case OP_POP_RES: { uint64_t res = vm_stack_pop(); vm.result = res; break; } case OP_DONE: { return SUCCESS; } default: return ERROR_UNKNOWN_OPCODE; } } return SUCCESS; }
Dalam contoh ini, sudah ada lebih banyak operasi, dan hampir semuanya hanya bekerja dengan stack. OP_PUSHI mendorong argumen langsungnya ke tumpukan. Instruksi OP_ADD, OP_SUB, OP_DIV, OP_MUL muncul dari setumpuk nilai, menghitung hasilnya, dan mendorongnya kembali ke stack. OP_POP_RES menghapus nilai dari tumpukan dan menempatkannya dalam register hasil, yang dimaksudkan untuk hasil mesin virtual.
Untuk operasi pembagian (OP_DIV), pembagian dengan kesalahan nol ditangkap, yang menghentikan mesin virtual.
Kemampuan mesin seperti itu jauh lebih luas daripada yang sebelumnya dengan register tunggal dan memungkinkan, misalnya, untuk menghitung ekspresi aritmatika yang kompleks. Keuntungan lain (dan penting!) Adalah kesederhanaan mengkompilasi bahasa pemrograman ke dalam kode byte mesin stack.
Daftarkan mesin
Karena kesederhanaannya, mesin virtual yang ditumpuk paling banyak digunakan di kalangan pengembang bahasa pemrograman; JVM dan Python VM yang sama menggunakan persisnya.
Namun, mesin tersebut memiliki kelemahan: mereka harus menambahkan instruksi khusus untuk bekerja dengan stack, ketika menghitung ekspresi, semua argumen berulang kali melewati struktur data tunggal, banyak instruksi tambahan pasti akan muncul dalam kode stack.
Sementara itu, pelaksanaan setiap instruksi tambahan memerlukan biaya penjadwalan, yaitu, decoding opcode dan beralih ke badan instruksi.
Alternatif untuk mesin bertumpuk adalah mendaftarkan mesin virtual. Mereka memiliki bytecode yang lebih kompleks: jumlah argumen register dan jumlah hasil register secara eksplisit dikodekan dalam setiap instruksi. Dengan demikian, alih-alih tumpukan, set register yang diperluas digunakan sebagai penyimpanan nilai-nilai perantara.
#define REGISTER_NUM 16 struct { uint16_t *ip; uint64_t reg[REGISTER_NUM]; uint64_t result; } vm; typedef enum { OP_LOADI, OP_ADD, OP_SUB, OP_DIV, OP_MUL, OP_MOV_RES, OP_DONE, } opcode; typedef enum interpret_result { SUCCESS, ERROR_DIVISION_BY_ZERO, ERROR_UNKNOWN_OPCODE, } interpret_result; void vm_reset(void) { puts("Reset vm state"); vm = (typeof(vm)) { NULL }; } void decode(uint16_t instruction, uint8_t *op, uint8_t *reg0, uint8_t *reg1, uint8_t *reg2, uint8_t *imm) { *op = (instruction & 0xF000) >> 12; *reg0 = (instruction & 0x0F00) >> 8; *reg1 = (instruction & 0x00F0) >> 4; *reg2 = (instruction & 0x000F); *imm = (instruction & 0x00FF); } interpret_result vm_interpret(uint16_t *bytecode) { vm_reset(); puts("Start interpreting"); vm.ip = bytecode; uint8_t op, r0, r1, r2, immediate; for (;;) { uint16_t instruction = *vm.ip++; decode(instruction, &op, &r0, &r1, &r2, &immediate); switch (op) { case OP_LOADI: { vm.reg[r0] = immediate; break; } case OP_ADD: { vm.reg[r2] = vm.reg[r0] + vm.reg[r1]; break; } case OP_SUB: { vm.reg[r2] = vm.reg[r0] - vm.reg[r1]; break; } case OP_DIV: { if (vm.reg[r1] == 0) return ERROR_DIVISION_BY_ZERO; vm.reg[r2] = vm.reg[r0] / vm.reg[r1]; break; } case OP_MUL: { vm.reg[r2] = vm.reg[r0] * vm.reg[r1]; break; } case OP_MOV_RES: { vm.result = vm.reg[r0]; break; } case OP_DONE: { return SUCCESS; } default: return ERROR_UNKNOWN_OPCODE; } } return SUCCESS; }
Contoh menunjukkan mesin register dengan 16 register. Instruksi menempati 16 bit masing-masing dan dikodekan dalam tiga cara:
- 4 bit per opcode + 4 bit per nama register + 8 bit per argumen.
- 4 bit per opcode + tiga kali 4 bit per nama register.
- 4 bit per opcode + 4 bit per nama register tunggal + 8 bit yang tidak digunakan.
Mesin virtual kecil kami memiliki operasi yang sangat sedikit, sehingga empat bit (atau 16 operasi yang mungkin) per opcode cukup. Operasi menentukan apa yang sebenarnya mewakili bit sisa instruksi.
Jenis pengkodean pertama (4 + 4 + 8) diperlukan untuk memuat data ke dalam register dengan operasi OP_LOADI. Tipe kedua (4 + 4 + 4 + 4) digunakan untuk operasi aritmatika, yang harus tahu di mana harus mengambil sepasang argumen dan di mana menambahkan hasil perhitungan. Dan akhirnya, bentuk terakhir (4 + 4 + 8 bit yang tidak perlu) digunakan untuk instruksi dengan register tunggal sebagai argumen, dalam kasus kami adalah OP_MOV_RES.
Untuk menyandikan dan mendekode instruksi, kita sekarang memerlukan logika khusus (fungsi decode
). Di sisi lain, logika instruksi, berkat indikasi eksplisit lokasi argumen, menjadi lebih mudah - operasi dengan tumpukan menghilang.
Fitur utama: dalam bytecode mesin register terdapat lebih sedikit instruksi, instruksi individual lebih luas, kompilasi ke bytecode seperti itu lebih sulit - kompiler harus memutuskan bagaimana menggunakan register yang tersedia.
Perlu dicatat bahwa dalam prakteknya dalam mendaftar mesin virtual biasanya ada tumpukan di mana, misalnya, argumen fungsi ditempatkan; register digunakan untuk menghitung ekspresi individu. Bahkan jika tidak ada stack eksplisit, array digunakan untuk membangun stack, memainkan peran yang sama dengan RAM di mesin fisik.
Tumpuk dan daftarkan mesin, perbandingan
Ada penelitian yang menarik ( showdown mesin Virtual: Stack versus register , 2008) yang telah memiliki pengaruh besar pada semua perkembangan selanjutnya di bidang mesin virtual untuk bahasa pemrograman. Penulisnya telah mengusulkan metode terjemahan langsung dari kode tumpukan JVM standar ke dalam kode register dan membandingkan kinerja.
Metode ini tidak sepele: kode pertama kali diterjemahkan, dan kemudian dioptimalkan dengan cara yang agak rumit. Tetapi perbandingan selanjutnya dari kinerja program yang sama menunjukkan bahwa siklus prosesor tambahan yang dihabiskan untuk instruksi decoding sepenuhnya dikompensasi oleh penurunan jumlah total instruksi. Secara umum, singkatnya, mesin register lebih efisien daripada yang stack.
Seperti yang telah disebutkan di atas, efisiensi ini memiliki harga yang cukup nyata: kompiler harus mengalokasikan register itu sendiri dan pengoptimal lanjutan juga diinginkan.
Perdebatan tentang arsitektur mana yang lebih baik masih belum berakhir. Jika kita berbicara tentang kompiler Java, maka bytecode Dalvik VM, yang hingga saat ini bekerja pada setiap perangkat Android, terdaftar; tetapi judul JVM tetap memiliki setumpuk instruksi. Mesin virtual Lua menggunakan mesin register, tetapi Python VM masih bisa ditumpuk. Dan sebagainya.
Bytecode dalam juru bahasa ekspresi reguler
Akhirnya, untuk mengalihkan perhatian kita dari mesin virtual level rendah, mari kita lihat juru bahasa khusus yang memeriksa string untuk pencocokan ekspresi reguler:
typedef enum { OP_CHAR, OP_OR, OP_JUMP, OP_MATCH, } opcode; typedef enum match_result { MATCH_OK, MATCH_FAIL, MATCH_ERROR, } match_result; match_result vm_match_recur(uint8_t *bytecode, uint8_t *ip, char *sp) { for (;;) { uint8_t instruction = *ip++; switch (instruction) { case OP_CHAR:{ char cur_c = *sp; char arg_c = (char)*ip ; if (arg_c != cur_c) return MATCH_FAIL; ip++; sp++; continue; } case OP_JUMP:{ uint8_t offset = *ip; ip = bytecode + offset; continue; } case OP_OR:{ uint8_t left_offset = *ip++; uint8_t right_offset = *ip; uint8_t *left_ip = bytecode + left_offset; if (vm_match_recur(bytecode, left_ip, sp) == MATCH_OK) return MATCH_OK; ip = bytecode + right_offset; continue; } case OP_MATCH:{ return MATCH_OK; } } return MATCH_ERROR; } } match_result vm_match(uint8_t *bytecode, char *str) { printf("Start matching a string: %s\n", str); return vm_match_recur(bytecode, bytecode, str); }
Instruksi utama adalah OP_CHAR. Dia mengambil argumen langsungnya dan membandingkannya dengan karakter saat ini dalam string ( char *sp
). Dalam hal kebetulan dari karakter yang diharapkan dan saat ini di baris, transisi ke instruksi berikutnya dan karakter berikutnya terjadi.
Mesin juga memahami operasi lompatan (OP_JUMP), yang membutuhkan argumen langsung. Argumen berarti offset absolut dalam bytecode, dari tempat untuk melanjutkan perhitungan.
Operasi penting terakhir adalah OP_OR. Dia mengambil dua offset, mencoba menerapkan kode pertama pada yang pertama, lalu, jika ada kesalahan, yang kedua. Dia melakukan ini dengan panggilan rekursif, yaitu, instruksi berjalan ke kedalaman pohon dari semua varian yang mungkin dari ekspresi reguler.
Anehnya, empat opcode dan tujuh puluh baris kode sudah cukup untuk mengekspresikan ekspresi reguler seperti "abc", "a? Bc", "(ab | bc) d", "a * bc". Mesin virtual ini bahkan tidak memiliki keadaan eksplisit, karena semua yang Anda butuhkan - penunjuk ke awal aliran instruksi, instruksi saat ini dan karakter saat ini - diteruskan sebagai argumen untuk fungsi rekursif.
Jika Anda tertarik pada detail kerja mesin ekspresi reguler, Anda dapat terlebih dahulu membaca serangkaian artikel oleh Russ Cox, penulis mesin ekspresi reguler dari Google RE2 .
Ringkasan
Mari kita simpulkan.
Untuk bahasa pemrograman tujuan umum, sebagai aturan, dua arsitektur digunakan: stack dan register.
Dalam model tumpukan, struktur data utama dan metode menyampaikan argumen antara instruksi adalah tumpukan. Dalam model register, satu set register digunakan untuk menghitung ekspresi, tetapi tumpukan eksplisit atau implisit masih digunakan untuk menyimpan argumen fungsi.
Kehadiran tumpukan eksplisit dan satu set register membawa mesin seperti itu lebih dekat ke tingkat rendah dan bahkan yang fisik. Banyaknya instruksi tingkat rendah dalam bytecode seperti itu berarti bahwa pengeluaran sumber daya yang signifikan dari prosesor fisik jatuh pada decoding dan penjadwalan instruksi virtual.
Di sisi lain, instruksi tingkat tinggi memainkan peran besar dalam mesin virtual populer. Di Jawa, misalnya, ini adalah instruksi untuk panggilan fungsi polimorfik, alokasi objek, dan pengumpulan sampah.
Mesin virtual tingkat tinggi murni - misalnya, penerjemah byte-kode bahasa dengan bahasa maju dan jauh dari semantik besi - sebagian besar waktu dihabiskan bukan di dispatcher atau decoder, tetapi di badan instruksi dan, karenanya, relatif efisien.
Rekomendasi praktis:
- Jika Anda perlu menjalankan bytecode apa pun dan melakukannya dalam jumlah waktu yang wajar, maka cobalah untuk beroperasi dengan instruksi yang paling dekat dengan tugas Anda; semakin tinggi tingkat semantik, semakin baik. Ini akan mengurangi biaya penjadwalan dan menyederhanakan pembuatan kode.
- Jika Anda membutuhkan lebih banyak fleksibilitas dan semantik heterogen, Anda setidaknya harus mencoba untuk menyoroti penyebut umum dalam bytecode sehingga instruksi yang dihasilkan berada pada tingkat rata-rata kondisional.
- Jika di masa depan mungkin diperlukan untuk menghitung ekspresi apa pun, membuat mesin bertumpuk, ini akan mengurangi sakit kepala saat menyusun kode byte.
- Jika ekspresi tidak diharapkan, maka buat mesin register sepele, yang akan menghindari biaya tumpukan dan menyederhanakan instruksi itu sendiri.
Dalam artikel berikut, saya akan membahas implementasi praktis mesin virtual dalam bahasa pemrograman populer dan menjelaskan mengapa departemen Badoo Intelijen Bisnis memerlukan bytecode.