Penerbangan Babi, atau Optimalisasi Penerjemah Bytecode


"Tidak peduli seberapa keras Anda mencoba, Anda tidak dapat membuat kuda pacu keluar dari babi. Namun, Anda dapat membuat babi lebih cepat" (komentar dalam kode sumber Emax)

Semua orang tahu fakta bahwa babi tidak bisa terbang. Yang tidak kalah populer adalah pendapat bahwa penerjemah bytecode sebagai teknik untuk mengeksekusi bahasa tingkat tinggi tidak dapat dipercepat tanpa menggunakan kompilasi dinamis yang memakan waktu.


Pada bagian kedua dari serangkaian artikel tentang penerjemah bytecode, saya akan mencoba menunjukkan dengan contoh mesin virtual FDA kecil (Pig Virtual Machine) bahwa tidak semuanya hilang untuk anak babi pekerja keras dengan ambisi dan bahwa sangat mungkin untuk mempercepat dalam kerangka (kebanyakan) standar C pekerjaan penerjemah semacam itu setidaknya satu setengah kali.


Bagian Satu, Pengantar
Bagian dua, mengoptimalkan (saat ini)
Bagian Tiga, Diterapkan


Piglet


Mari berkenalan.


Piglet VM adalah mesin bertumpuk biasa berdasarkan contoh dari bagian pertama dari serangkaian artikel. Babi kami hanya tahu satu tipe data - kata mesin 64-bit, dan semua perhitungan (bilangan bulat) dilakukan pada tumpukan dengan kedalaman maksimum 256 kata mesin. Selain tumpukan, anak babi ini memiliki memori kerja 65.536 kata mesin. Hasil eksekusi program - satu kata mesin - dapat ditempatkan dalam register hasil, atau cukup output ke output standar (stdout).


Seluruh status dalam mesin Piglet VM disimpan dalam satu struktur:


static struct { /* Current instruction pointer */ uint8_t *ip; /* Fixed-size stack */ uint64_t stack[STACK_MAX]; uint64_t *stack_top; /* Operational memory */ uint64_t memory[MEMORY_SIZE]; /* A single register containing the result */ uint64_t result; } vm; 

Di atas memungkinkan kita untuk menghubungkan mesin ini ke mesin virtual level rendah, hampir semua overhead yang jatuh pada pemeliharaan siklus program utama:


 interpret_result vm_interpret(uint8_t *bytecode) { vm_reset(bytecode); for (;;) { uint8_t instruction = NEXT_OP(); switch (instruction) { case OP_PUSHI: { /* get the argument, push it onto stack */ uint16_t arg = NEXT_ARG(); PUSH(arg); break; } case OP_ADD: { /* Pop 2 values, add 'em, push the result back to the stack */ uint64_t arg_right = POP(); *TOS_PTR() += arg_right; break; } /* * ... * Lots of other instruction handlers here * ... */ case OP_DONE: { return SUCCESS; } default: return ERROR_UNKNOWN_OPCODE; } } return ERROR_END_OF_STREAM; } 

Kode menunjukkan bahwa untuk setiap opcode, piggy harus:


  1. Ambil opcode dari aliran instruksi.
  2. Pastikan opcode berada dalam kisaran nilai opcode yang valid (logika ini ditambahkan oleh kompiler C saat membuat kode switch).
  3. Pergi ke instruksi tubuh.
  4. Ekstrak argumen instruksi dari stack atau decode argumen instruksi yang terletak langsung di bytecode.
  5. Lakukan operasi.
  6. Jika ada hasil perhitungan, letakkan di tumpukan.
  7. Pindahkan penunjuk dari instruksi saat ini ke yang berikutnya.

Payload di sini hanya di paragraf kelima, sisanya di atas kepala: mendekode atau mengambil argumen instruksi dari stack (klausa 4), memeriksa nilai opcode (klausa 2), berulang kali kembali ke awal loop utama dan transisi kondisional berikutnya yang sulit diprediksi (klausul 3).


Singkatnya, babi jelas telah melebihi indeks massa tubuh yang direkomendasikan, dan jika kita ingin membuatnya menjadi sempurna, maka kita harus berurusan dengan semua ekses ini.


Bahasa perakitan babi dan ayakan Eratosthenes


Pertama, mari kita putuskan aturan mainnya.


Menulis program untuk mesin virtual langsung di C adalah ide yang buruk, tetapi membuat bahasa pemrograman adalah waktu yang lama, jadi kami memutuskan untuk membatasi diri pada bahasa rakitan celengan.


Program yang menghitung jumlah angka dari 1 hingga 65.536 di assembler ini terlihat seperti ini:


 # sum numbers from 1 to 65535 # init the current sum and the index PUSHI 1 PUSHI 1 # stack s=1, i=1 STOREI 0 # stack: s=1 # routine: increment the counter, add it to the current sum incrementandadd: # check if index is too big LOADI 0 # stack: s, i ADDI 1 # stack: s, i+1 DUP # stack: s, i+1, i+1 GREATER_OR_EQUALI 65535 # stack: s, i+1, 1 or 0 JUMP_IF_TRUE done # stack: s, i+1 DUP # stack: s, i+1, i+1 STOREI 0 # stack: s, i+1 ADD # stack: s+i+1 JUMP incrementandadd done: DISCARD PRINT DONE 

Bukan Python, tentu saja, tetapi ada semua yang Anda butuhkan untuk kebahagiaan babi: komentar, tag, lompatan bersyarat dan tanpa syarat kepada mereka, mnemonik untuk instruksi, dan kemampuan untuk menentukan argumen langsung ke instruksi.


Lengkap dengan mesin "Piglet VM" yang assembler dan disassembler, yang berani dalam semangat dan memiliki banyak waktu luang, pembaca dapat menguji secara mandiri dalam pertempuran.


Jumlahnya bertambah dengan sangat cepat, jadi untuk menguji kinerja saya menulis program lain - implementasi naif dari saringan Eratosthenes .


Bahkan, anak babi berjalan cukup cepat pula (instruksinya dekat dengan yang mesin), oleh karena itu, untuk mendapatkan hasil yang jelas, saya akan melakukan pengukuran masing-masing untuk seratus awal program.


Versi pertama dari babi kami yang tidak dioptimalkan berjalan seperti ini:


 > ./pigletvm runtimes test/sieve-unoptimized.bin 100 > /dev/null PROFILE: switch code finished took 545ms 

Setengah detik! Perbandingannya tentu saja tidak jujur, tetapi algoritma Python yang sama membuat seratus berjalan sedikit lebih lambat:


 > python test/sieve.py > /dev/null 4.66692185402 

4,5 detik, atau sembilan kali lebih lambat. Kita harus membayar upeti kepada anak babi - dia memiliki kemampuan! Nah, sekarang mari kita lihat apakah babi kita dapat memompa pers.


Latihan Satu: Superinstructions statis


Aturan pertama kode cepat adalah jangan melakukan terlalu banyak pekerjaan. Aturan kedua dari kode cepat adalah jangan pernah melakukan terlalu banyak pekerjaan. Jadi, jenis pekerjaan ekstra apa yang dilakukan Piglet VM?


Pengamatan satu: membuat profil program kami menunjukkan bahwa ada urutan instruksi yang lebih umum daripada yang lain. Kami tidak akan banyak menyiksa babi kami dan membatasi diri hanya pada beberapa instruksi:


  1. LOADI 0, ADD - letakkan di tumpukan nomor dari memori di alamat 0 dan tambahkan ke nomor di atas tumpukan.
  2. PUSHI 65536, GREATER_OR_EQUAL - taruh nomor di tumpukan dan bandingkan dengan nomor yang sebelumnya di atas tumpukan, masukkan hasil perbandingan (0 atau 1) kembali ke tumpukan.
  3. PUSHI 1, ADD - letakkan angka di tumpukan, tambahkan ke nomor yang sebelumnya di atas tumpukan, dan masukkan kembali hasil penambahan ke tumpukan.

Ada lebih dari 20 instruksi dalam mesin Piglet VM, dan seluruh byte digunakan untuk encoding - 256 nilai. Memperkenalkan instruksi baru bukanlah masalah. Apa yang akan kita lakukan:


 for (;;) { uint8_t instruction = NEXT_OP(); switch (instruction) { /* * Other instructions here * */ case OP_LOADADDI: { /* get immediate argument as an memory address , add it to value from the address to the top * of the stack */ uint16_t addr = NEXT_ARG(); uint64_t val = vm.memory[addr]; *TOS_PTR() += val; break; } case OP_GREATER_OR_EQUALI:{ /* get the immediate argument, compare it with the value from the address to the top of the stack */ uint64_t arg_right = NEXT_ARG(); *TOS_PTR() = PEEK() >= arg_right; break; } case OP_ADDI: { /* Add immediate value to the top of the stack */ uint16_t arg_right = NEXT_ARG(); *TOS_PTR() += arg_right; break; } /* * Other instructions here * */ } 

Tidak ada yang rumit. Mari kita lihat apa yang terjadi:


 > ./pigletvm runtimes test/sieve.bin 100 > /dev/null PROFILE: switch code finished took 410ms 

Wow! Kode ini hanya untuk tiga instruksi baru, dan kami memenangkan satu setengah ratus milidetik!


Keuntungan di sini tercapai karena fakta bahwa piggy kami tidak melakukan gerakan yang tidak perlu ketika mengeksekusi instruksi tersebut: utas eksekusi tidak jatuh ke loop utama, tidak ada yang diterjemahkan, dan argumen instruksi tidak melewati stack sekali lagi.


Ini disebut superinstructions statis, karena instruksi tambahan didefinisikan secara statis, yaitu, oleh programmer mesin virtual pada tahap pengembangan. Ini adalah teknik sederhana dan efektif yang digunakan semua mesin virtual bahasa pemrograman dalam satu bentuk atau lainnya.


Masalah utama dengan superinstructions statis adalah bahwa tanpa program tertentu tidak mungkin untuk menentukan instruksi mana yang harus digabungkan. Program yang berbeda menggunakan urutan instruksi yang berbeda, dan Anda dapat mengetahui urutan ini hanya pada tahap peluncuran kode tertentu.


Langkah selanjutnya adalah kompilasi dinamis dari superinstructions dalam konteks program tertentu, yaitu superinstructions dinamis (di tahun 90-an dan awal 2000-an, teknik ini memainkan peran kompilasi JIT primitif).


Tidak mungkin membuat instruksi dengan cepat dalam kerangka C biasa, dan anak babi kami dengan benar tidak menganggap ini sebagai kompetisi yang jujur. Untungnya, saya punya beberapa latihan yang lebih baik untuknya.


Latihan dua: memeriksa rentang nilai opcode


Mengikuti aturan kode cepat kami, sekali lagi kami bertanya pada diri sendiri pertanyaan abadi: apa yang tidak bisa Anda lakukan?


Ketika kami berkenalan dengan perangkat mesin Piglet VM, saya mencantumkan semua tindakan yang dilakukan mesin virtual untuk setiap opcode. Dan poin 2 (memeriksa nilai opcode agar sesuai dengan rentang nilai sakelar yang valid) adalah yang paling mencurigakan.


Mari kita lihat bagaimana GCC mengkompilasi switch switch:


  1. Tabel transisi dibangun, yaitu, tabel yang menampilkan nilai opcode ke alamat kode yang mengeksekusi tubuh instruksi.
  2. Kode dimasukkan yang memeriksa apakah opcode yang diterima berada dalam kisaran semua kemungkinan nilai sakelar dan mengirimkannya ke label default jika tidak ada penangan untuk opcode tersebut.
  3. Kode yang masuk ke pawang dimasukkan.

Tetapi mengapa memeriksa interval nilai untuk setiap instruksi? Kami percaya bahwa opcode adalah benar - mengakhiri eksekusi dengan instruksi OP_DONE, atau salah - melampaui bytecode. Ekor aliran opcode ditandai dengan nol, dan nol adalah opcode dari instruksi OP_ABORT, yang menyelesaikan eksekusi bytecode dengan kesalahan.


Ternyata pemeriksaan ini tidak diperlukan sama sekali! Dan anak babi harus bisa menyampaikan ide ini ke kompiler. Mari kita coba sedikit memperbaiki saklar utama:


 uint8_t instruction = NEXT_OP(); /* Let the compiler know that opcodes are always between 0 and 31 */ switch (instruction & 0x1f) { /* All the instructions here */ case 26 ... 0x1f: { /*Handle the remaining 5 non-existing opcodes*/ return ERROR_UNKNOWN_OPCODE; } } 

Mengetahui bahwa kami hanya memiliki 26 instruksi, kami mengenakan bit mask (nilai oktal 0x1f adalah biner 0b11111 yang mencakup rentang nilai dari 0 hingga 31) pada opcode dan menambahkan penangan ke nilai yang tidak digunakan dalam rentang dari 26 hingga 31.


Instruksi bit adalah beberapa yang termurah dalam arsitektur x86, dan mereka tentu lebih murah daripada cabang kondisional bermasalah seperti yang menggunakan pengecekan interval. Secara teoritis, kita harus memenangkan beberapa siklus pada setiap instruksi yang dapat dieksekusi jika kompiler memahami petunjuk kita.


By the way, cara untuk menentukan rentang nilai dalam kasus ini bukan standar C, tetapi ekstensi GCC. Tetapi untuk tujuan kita, kode ini cocok, terutama karena tidak sulit untuk membuatnya kembali menjadi beberapa penangan untuk masing-masing nilai yang tidak perlu.


Kami mencoba:


 > ./pigletvm runtimes test/sieve.bin 100 > /dev/null PROFILE: switch code finished took 437ms PROFILE: switch code (no range check) finished took 383ms 

50 milidetik lagi! Piglet, seolah-olah Anda mendengar diri Anda di pundak Anda! ..


Latihan Tiga: Lintasan


Latihan apa lagi yang bisa membantu anak babi kita? Penghematan waktu terbesar yang kami dapatkan berkat instruksi super. Dan mereka mengurangi jumlah jalan keluar ke siklus utama dan memungkinkan Anda untuk menyingkirkan overhead yang sesuai.


Saklar sentral adalah titik masalah utama untuk setiap prosesor dengan eksekusi instruksi yang luar biasa. Prediktor cabang modern telah belajar memprediksikan bahkan transisi tidak langsung yang rumit seperti itu dengan baik, tetapi “mengolesi” titik-titik cabang di sepanjang kode dapat membantu prosesor dengan cepat beralih dari instruksi ke instruksi.


Masalah lain adalah pembacaan byte-by-byte dari opcodes instruksi dan argumen langsung dari bytecode. Mesin fisik beroperasi dengan kata mesin 64-bit dan tidak benar-benar menyukainya ketika kode beroperasi dengan nilai yang lebih rendah.


Compiler sering beroperasi dengan blok-blok dasar , yaitu urutan instruksi tanpa cabang dan label di dalamnya. Blok dasar dimulai baik dari awal program atau dari label, dan berakhir dengan akhir program, percabangan bersyarat atau lompatan langsung ke label yang memulai blok dasar berikutnya.


Ada banyak keuntungan untuk bekerja dengan unit dasar, tetapi babi kami tertarik dengan fitur utamanya: instruksi dalam unit dasar dijalankan secara berurutan. Akan sangat bagus untuk mengisolasi blok-blok dasar ini dan mengikuti instruksi di dalamnya tanpa membuang waktu untuk masuk ke loop utama.


Dalam kasus kami, Anda bahkan dapat memperluas definisi unit dasar ke trek. Track dalam hal mesin Piglet VM akan mencakup semua blok dasar yang terhubung secara berurutan (yaitu, menggunakan lompatan tanpa syarat).


Selain eksekusi berurutan dari instruksi, alangkah baiknya untuk memecahkan kode argumen langsung dari instruksi di muka.


Semuanya terdengar sangat menakutkan dan menyerupai kompilasi dinamis, yang kami putuskan untuk tidak digunakan. Babi bahkan sedikit meragukan kekuatannya, tetapi dalam praktiknya ternyata tidak terlalu buruk.


Pertama-tama mari kita pikirkan bagaimana Anda bisa membayangkan instruksi yang termasuk dalam trek:


 struct scode { uint64_t arg; trace_op_handler *handler; }; 

Di sini arg adalah argumen pra-dekode instruksi, dan handler adalah pointer ke fungsi yang mengeksekusi logika instruksi.


Sekarang tampilan setiap jejak terlihat seperti ini:


 typedef scode trace[MAX_TRACE_LEN]; 

Artinya, jejak adalah urutan kode-s dengan panjang terbatas. Cache jejak itu sendiri di dalam mesin virtual terlihat seperti ini:


 trace trace_cache[MAX_CODE_LEN]; 

Ini hanya array jejak dengan panjang tidak melebihi panjang bytecode yang mungkin. Solusinya malas, untuk menghemat memori masuk akal untuk menggunakan tabel hash.


Di awal penerjemah, penangan pertama dari setiap jejak akan mengkompilasi dirinya sendiri:


 for (size_t trace_i = 0; trace_i < MAX_CODE_LEN; trace_i++ ) vm_trace.trace_cache[trace_i][0].handler = trace_compile_handler; 

Lingkaran juru bahasa utama sekarang terlihat seperti ini:


 while(vm_trace.is_running) { scode *code = &vm_trace.trace_cache[vm_trace.pc][0]; code->handler(code); } 

Sebuah kompiler jejak sedikit lebih rumit, dan selain membangun jejak mulai dari instruksi saat ini, ia melakukan hal berikut:


 static void trace_compile_handler(scode *trace_head) { scode *trace_tail = trace_head; /* * Trace building here */ /* now, run the chain that has a trace_compile_handler replaced with proper instruction handler * function pointer */ trace_head->handler(trace_head); } 

Penangan instruksi normal:


 static void op_add_handler(scode *code) { uint64_t arg_right = POP(); *TOS_PTR() += arg_right; /* * Call the next trace handler * */ /* scodes are located in an array so we can use pointer arithmetic to get the next handler */ code++; code->handler(code); } 

Pawang yang tidak melakukan panggilan apa pun di ujung fungsi mengakhiri setiap jejak:


 static void op_done_handler(scode *code) { (void) code; vm_trace.is_running = false; vm_trace.error = SUCCESS; } 

Semua ini, tentu saja, lebih rumit daripada menambahkan superinstructions, tetapi mari kita lihat apakah itu memberi kita sesuatu:


 > ./pigletvm runtimes test/sieve.bin 100 > /dev/null PROFILE: switch code finished took 427ms PROFILE: switch code (no range check) finished took 395ms PROFILE: trace code finished took 367ms 

Hore, 30 milidetik lagi!


Bagaimana bisa begitu? Alih-alih hanya menavigasi melalui label, kami membuat rantai panggilan penangan instruksi, menghabiskan waktu untuk panggilan dan melewati argumen, tapi celengan kami masih berjalan di sepanjang trek lebih cepat daripada saklar sederhana dengan labelnya.


Keuntungan dalam kinerja trek ini dicapai karena tiga faktor:


  1. Memprediksi cabang yang tersebar di berbagai tempat dalam kode itu mudah.
  2. Argumen penangan selalu disandikan ke dalam kata mesin lengkap, dan ini dilakukan hanya sekali - selama kompilasi jejak.
  3. Kompiler mengubah rantai fungsi menjadi satu panggilan ke fungsi penangan pertama, yang dimungkinkan karena optimalisasi panggilan ekor .

Sebelum merangkum hasil pelatihan kami, anak babi itu dan saya memutuskan untuk mencoba teknik kuno lain untuk menafsirkan program - kode dijahit.


Latihan Empat: kode "dijahit"


Setiap babi yang tertarik dengan sejarah penerjemah mendengar kode ulir. Ada banyak opsi untuk teknik ini, tetapi semuanya mendidih alih-alih melalui array opcode, misalnya, pointer ke fungsi atau label, mengikuti secara langsung tanpa opcode perantara.


Fungsi panggilan adalah bisnis yang mahal dan tidak berarti akhir-akhir ini; sebagian besar versi lain dari kode dijahit tidak dapat diwujudkan dalam kerangka standar C. Bahkan teknik, yang akan dibahas di bawah ini, menggunakan ekstensi C - pointer ekstensi yang tidak standar untuk label.


Dalam versi kode dijahit (kode token berbahasa Inggris) yang saya pilih untuk mencapai tujuan babi kami, kami menyimpan bytecode, tetapi sebelum memulai interpretasi, kami membuat tabel yang menampilkan opcodes instruksi ke alamat label penangan instruksi:


 const void *labels[] = { [OP_PUSHI] = &&op_pushi, [OP_LOADI] = &&op_loadi, [OP_LOADADDI] = &&op_loadaddi, [OP_STORE] = &&op_store, [OP_STOREI] = &&op_storei, [OP_LOAD] = &&op_load, [OP_DUP] = &&op_dup, [OP_DISCARD] = &&op_discard, [OP_ADD] = &&op_add, [OP_ADDI] = &&op_addi, [OP_SUB] = &&op_sub, [OP_DIV] = &&op_div, [OP_MUL] = &&op_mul, [OP_JUMP] = &&op_jump, [OP_JUMP_IF_TRUE] = &&op_jump_if_true, [OP_JUMP_IF_FALSE] = &&op_jump_if_false, [OP_EQUAL] = &&op_equal, [OP_LESS] = &&op_less, [OP_LESS_OR_EQUAL] = &&op_less_or_equal, [OP_GREATER] = &&op_greater, [OP_GREATER_OR_EQUAL] = &&op_greater_or_equal, [OP_GREATER_OR_EQUALI] = &&op_greater_or_equali, [OP_POP_RES] = &&op_pop_res, [OP_DONE] = &&op_done, [OP_PRINT] = &&op_print, [OP_ABORT] = &&op_abort, }; 

Perhatikan simbol && - ini adalah petunjuk ke label dengan isi instruksi, ekstensi paling tidak standar dari GCC.


Untuk mulai mengeksekusi kode, cukup klik pada pointer yang sesuai dengan opcode pertama program:


 goto *labels[NEXT_OP()]; 

Tidak ada siklus di sini dan tidak akan ada, masing-masing instruksi itu sendiri membuat lompatan ke penangan berikut:


 op_pushi: { /* get the argument, push it onto stack */ uint16_t arg = NEXT_ARG(); PUSH(arg); /* jump to the next instruction*/ goto *labels[NEXT_OP()]; } 

Tidak adanya saklar “menyebar” menunjukkan titik-titik di sepanjang badan-badan instruksi, yang secara teori harus membantu prediktor cabang dalam hal pelaksanaan instruksi yang luar biasa. Seolah-olah kami membuat saklar langsung ke dalam instruksi dan secara manual membentuk tabel transisi.


Itulah keseluruhan tekniknya. Dia menyukai anak babi karena kesederhanaannya. Mari kita lihat apa yang terjadi dalam praktik:


 > ./pigletvm runtimes test/sieve.bin 100 > /dev/null PROFILE: switch code finished took 443ms PROFILE: switch code (no range check) finished took 389ms PROFILE: threaded code finished took 477ms PROFILE: trace code finished took 364ms 

Ups! Ini adalah yang paling lambat dari semua teknik kami! Apa yang terjadi Mari kita jalankan tes yang sama, matikan semua optimasi GCC:


 > ./pigletvm runtimes test/sieve.bin 100 > /dev/null PROFILE: switch code finished took 969ms PROFILE: switch code (no range check) finished took 940ms PROFILE: threaded code finished took 824ms PROFILE: trace code finished took 1169ms 

Di sini, kode yang dijahit berkinerja lebih baik.


Tiga faktor berperan di sini:


  1. Kompilator pengoptimal itu sendiri akan membuat tabel konversi tidak lebih buruk dari plat label manual kami.
  2. Kompiler modern sangat menyingkirkan panggilan fungsi tambahan.
  3. Mulai sekitar generasi prosesor Intel Haswell, prediktor cabang telah belajar untuk memprediksi transisi secara akurat di satu titik cabang.

Menurut memori lama, teknik ini masih digunakan dalam kode, misalnya, interpreter Python VM, tetapi, terus terang, hari ini sudah arkaisme.


Mari kita meringkas dan mengevaluasi keberhasilan yang dicapai oleh babi kita.


Tanya jawab



Saya tidak yakin apa ini bisa disebut penerbangan, tapi mari kita hadapi itu, babi kami telah menempuh perjalanan jauh dari 550 milidetik selama seratus kali berjalan di atas "saringan" hingga 370 milidetik terakhir. Kami menggunakan teknik yang berbeda: instruksi super, menghilangkan interval waktu nilai, mekanisme rumit jejak dan, akhirnya, bahkan kode dijahit. Pada saat yang sama, kami, secara umum, bertindak dalam kerangka hal-hal yang diterapkan dalam semua kompiler populer C. Akselerasi sebanyak satu setengah kali, seperti yang menurut saya, adalah hasil yang baik, dan anak babi itu layak mendapat porsi ekstra dedak di bak.


Salah satu kondisi implisit yang kami tetapkan untuk diri sendiri dengan babi adalah untuk melestarikan arsitektur tumpukan mesin VM Piglet. Transisi untuk mendaftarkan arsitektur, sebagai suatu peraturan, mengurangi jumlah instruksi yang diperlukan untuk logika program dan, dengan demikian, dapat membantu menyingkirkan pintu keluar yang tidak perlu ke manajer instruksi. Saya pikir 10-20% lagi dari waktu ini dapat terputus.


Kondisi utama kami - kurangnya kompilasi dinamis - juga bukan hukum alam. Memompa babi dengan steroid dalam bentuk kompilasi JIT sangat mudah akhir-akhir ini: di perpustakaan seperti GNU Lightning atau LibJIT, semua pekerjaan kotor telah dilakukan. Tetapi waktu pengembangan dan jumlah total kode bahkan menggunakan perpustakaan tumbuh sangat pesat.


Tentu saja ada trik-trik lain yang tidak dapat dicapai oleh babi kecil kita. , — - — - . , .


PS , , , , ( https://www.instagram.com/vovazomb/ ), .


PPS , . true-grue - — PigletC . !


PPPS iliazeus : . ; . .

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


All Articles