Pengembangan kompiler JIT baru untuk PostgreSQL
(pinaraf.info)- Mesin JIT eksperimental pg-copyjit berfokus pada pembuatan kode yang “cukup cepat” bahkan untuk kueri PostgreSQL yang pendek, dengan biaya generasi yang lebih rendah dibanding LLVM
- Estimasi cost PostgreSQL tidak secara langsung selaras dengan waktu eksekusi nyata, sehingga LLVM JIT yang mahal dari sisi optimasi justru bisa merugikan pada kueri pendek
- Metode copy-and-patch mengompilasi lebih dulu stencil yang dibuat dengan C, lalu saat runtime menyalin dan menambal bagian yang diperlukan agar berjalan seperti fungsi baru
- pg-copyjit terpasang melalui antarmuka JIT provider PostgreSQL, dan jika menemui opcode yang belum diimplementasikan akan otomatis fallback ke interpreter PostgreSQL
- Saat ini masih berada pada tahap proof of concept yang berjalan di PostgreSQL 16 dan AMD64; pembuatan kode berada di kisaran ratusan mikrodetik, tetapi build, dokumentasi, dan dukungan masih belum siap
Titik yang dibidik pg-copyjit
- pg-copyjit adalah mesin JIT eksperimental untuk membuat server PostgreSQL lebih cepat
- Kode saat ini lebih dekat ke level yang layak diuji oleh peretas spesialis daripada untuk server produksi
- Umpan balik yang diharapkan adalah hasil eksperimen yang menarik, contoh peningkatan performa, dan ide implementasi; belum pada tahap yang layak mempertaruhkan downtime aplikasi inti bisnis
Mengapa LLVM JIT terasa memberatkan di PostgreSQL
- PostgreSQL sudah memiliki kompiler JIT berbasis LLVM yang diperkenalkan oleh Andres Freund
- LLVM dapat menghasilkan kode yang efisien, tetapi dalam cara pemakaiannya di PostgreSQL, biaya kompilasi dan optimasi JIT cukup besar
- Tanpa optimizer, hasilnya bisa lebih buruk daripada tidak mengompilasi; jika optimizer dipakai, biayanya bisa makin besar
- Penentuan apakah JIT akan diterapkan memakai estimasi cost kueri umum
- Cost di PostgreSQL adalah nilai untuk membandingkan kueri satu sama lain, bukan berarti waktu eksekusi nyata
- Kueri dengan cost 100 bisa memakan 1 detik, sementara kueri dengan cost 1000 bisa selesai dalam 100 ms
- Jika kueri menjadi 10 ms lebih cepat tetapi optimasinya memakan 50 ms, total waktu eksekusi justru rugi
- Salah satu cara membuat LLVM JIT lebih berguna adalah menyimpan cache dan menggunakan ulang kueri yang sudah dikompilasi, tetapi implementasinya bukan pekerjaan kecil
Struktur pendekatan copy-and-patch
- Copy-and-patch adalah metode penyusunan kompiler JIT yang diperkenalkan lewat makalah pada 2021, dan juga dipakai pada mesin JIT Python 3.13
- Unit intinya adalah stencil yang ditulis dalam C
- Stencil adalah fungsi yang memiliki lubang
- Dikompilasi lebih dulu dengan clang
- Dukungan gcc masih ditunda untuk saat ini
- Pada waktu kompilasi, stencil yang diperlukan dirangkai, lubangnya diisi, lalu eksekusi melompat ke fungsi “terkompilasi” yang baru dibuat
- Alur dasarnya sederhana
- Menyalin stencil ke area memori baru
- Menambal nilai yang diperlukan
- Menjalankan kode hasilnya
- Masih ada ruang untuk optimasi tambahan
- Nilai yang bisa dihitung saat kompilasi dihitung lebih dulu
- Loop dipecah menjadi beberapa stencil untuk di-unroll
- Beberapa stencil dapat digabung untuk membuat meta-stencil yang dioptimalkan sekaligus
Cara memasangnya ke PostgreSQL
- JIT PostgreSQL memiliki struktur provider yang bisa diperluas
- Berkas
.somenyediakan satu fungsi_PG_jit_provider_init, dan di fungsi ini tiga callback diinisialisasicompile_exprrelease_contextreset_after_error
- Callback yang paling penting adalah
compile_expr- Masukannya adalah pointer ekspresi
ExprState*yang terdiri dari opcode - Opcode dikompilasi dengan cara yang diinginkan
- Kode yang dihasilkan ditandai agar bisa dieksekusi
evalfuncdiubah dari interpreter PostgreSQL menjadi kode yang dihasilkan
- Masukannya adalah pointer ekspresi
- Jika menemui opcode yang belum diimplementasikan, sistem dapat otomatis fallback ke interpreter PostgreSQL
Prosedur kompilasi pg-copyjit
- Algoritme copy-and-patch milik pg-copyjit saat ini masih berupa bentuk sederhana yang hanya mencakup beberapa optimasi kecil
- Untuk setiap opcode, kompiler memeriksa koleksi stencil
- Jika ada stencil untuk opcode tersebut, stencil itu ditempelkan ke kode yang sedang dibangun
- Jika tidak ada stencil, kompilasi dihentikan dan interpreter PostgreSQL mengambil alih eksekusi
- Setelah stencil ditempelkan, setiap lubang ditambal dengan nilai yang diperlukan
- Stencil opcode
CONSTmendeklarasikanopsebagaiExprEvalStepeksternal, dan di dalam berkas.ohasil kompilasi disisakan lubang untuk alamatop - Koleksi stencil mempertahankan informasi relokasi ini, dan kompiler JIT mengisikan alamat struktur opcode saat ini untuk membuat kode yang bisa dieksekusi
- Alur build dimulai dengan membuat stencil menjadi satu berkas
.o, lalu dari sana kode assembly dan relokasinya diekstrak dan diubah menjadi struktur yang bisa dipakai di C
Status implementasi dan performa
- Pada awalnya, kode assembly diekstrak secara manual untuk membuat 3 opcode yang dibutuhkan oleh
SELECT 42;dapat berjalan - Setelah itu dibuat skrip DirtyPython untuk mengotomatisasi ekstraksi kode assembly, dan dalam beberapa jam fitur berikut ditambahkan
- Pemanggilan fungsi
- Kueri tabel tunggal
- Tipe data yang lebih kompleks
- Beberapa optimasi
- Status yang telah dipastikan saat ini adalah sebagai berikut
- Berjalan di PostgreSQL 16
- Diperkirakan juga baik-baik saja pada rilis sebelumnya, tetapi lingkungan yang telah terverifikasi adalah PostgreSQL 16
- Arsitektur yang didukung baru AMD64
- Ada rencana menambahkan dukungan ARM64
- Target seperti POWER64 dan S390x juga menarik, tetapi mungkin memerlukan patch kompiler dan akses ke mesin terkait
- Angka performa saat ini masih berasal dari kondisi yang nyaris belum dioptimalkan
- Pembuatan kode selesai dalam ratusan mikrodetik
- Sudah cukup untuk dipakai bahkan pada kueri pendek
- Untuk
SELECT 42;, tanpa JIT memerlukan 0,3 ms, copyjit 0,6 ms, LLVM tanpa optimasi 1,6 ms, dan LLVM dengan optimasi 6,6 ms
- LLVM bisa menghasilkan kode yang sangat cepat, tetapi tujuan pg-copyjit adalah membuat kode yang cukup cepat dengan cepat, sehingga sulit membandingkan kedua alat ini secara langsung
- Dua kueri dibenchmark pada tabel sederhana tanpa indeks berisi 90 ribu baris, dan ketika ada klausa
whereyang memuat pekerjaan CPU, performanya membaik dibanding interpreter - Benchmark dilakukan di laptop sehingga tingkat keandalannya terbatas, dan ke depan akan dilakukan benchmark yang lebih layak di desktop
- Meskipun opcode yang diimplementasikan masih sedikit, kueri apa pun tetap bisa dijalankan
- Mesin JIT akan mengeluarkan pesan untuk bagian yang belum diimplementasikan
- Eksekusi sebenarnya ditangani oleh interpreter
Kode yang dibuka dan pekerjaan yang tersisa
- Kodenya telah dipublikasikan di GitHub: pg-copyjit
- Saat ini fokus masih pada kode itu sendiri ketimbang merapikan riwayat git atau dokumentasi
- Untuk membangun, berkas
build-stencils.shharus dijalankan secara manual terlebih dahulu - Dalam kondisi saat ini belum bisa memberikan dukungan, sehingga dokumentasi pun belum disiapkan
- Pekerjaan yang tersisa relatif jelas
- Mengimplementasikan lebih banyak opcode
- Menjelajahi optimasi
- Meningkatkan kemudahan build
- Merapikan agar siap dipaketkan
- Skrip build saat ini dikhususkan untuk Debian dan PostgreSQL 16
Perkiraan penggunaan dan perluasan arsitektur
- Tujuannya adalah mencapai tingkat yang bisa dipaketkan dengan aman dan dideploy ke server produksi sendiri
- Ada juga gagasan untuk membagi penggunaan JIT per server
- Server GIS, tempat kueri layak menanggung biaya optimasi, menggunakan LLVM JIT
- Basis data aplikasi web, tempat waktu kueri pendek penting, menggunakan pg-copyjit
- Porting ke arsitektur lain juga merupakan tujuan yang serius
- Penulis merindukan era beragam arsitektur seperti Alpha, Itanium, Sparc, dan M68k, serta tidak ingin ikut memperkuat masalah monokultur yang berpusat pada satu arsitektur
1 komentar
Komentar Hacker News
Kompiler JIT harus menyuntikkan alamat memori secara langsung, sehingga kode yang dihasilkan terikat pada kueri dan proses tersebut
Rencana eksekusi juga mengalami masalah yang sama karena bukan struktur data memori yang dirancang untuk dibagikan antarproses berbeda
DB seperti MSSQL memakai arsitektur proses tunggal dengan thread, jadi tidak punya masalah ini, dan itu juga salah satu alasan mengapa ia bisa menangani lebih banyak koneksi serentak tanpa pooler eksternal
MSSQL juga bisa menserialisasi rencana eksekusi ke representasi yang tidak terikat pada proses lalu menyimpannya di DB, sehingga bisa dipakai untuk fitur seperti pembekuan rencana eksekusi
Cara kompilasi tradisional juga terlihat seperti “copy-and-patch” yang memakai bahasa antara selain C
Misalnya bagian seperti pass apa yang dipakai dan optimisasi backend apa yang digunakan
Dalam pengalaman kami [1], LLVM juga bisa dibuat cukup cepat jika optimisasi dimatikan dan disetel agar waktu kompilasinya sesuai dengan pipeline backend
-O0, tetapi tetap 10–20x lebih lambat daripada pendekatan lainKami juga punya pengalaman bahwa kode yang dihasilkan lewat copy-and-patch cukup lambat saat dijalankan dan sulit dioptimalkan. Kami sudah mencoba beberapa hal [2; Sec. 5], tetapi jaraknya masih besar. Lihat Fig. 3 untuk hasil evaluasi basis data
Saya juga penasaran apakah ada angka penurunan waktu eksekusi dibanding LLVM, dan apakah ada rencana menerapkan JIT bertingkat yang secara dinamis beralih dari kode yang dikompilasi cepat ke kode LLVM yang sudah dioptimalkan
[1]: https://home.in.tum.de/~engelke/pubs/2403-cgo.pdf
[2]: https://home.in.tum.de/~engelke/pubs/2403-cc.pdf
Saat belajar pemrograman sekitar 2010, khususnya interpreter, saya mengira sudah umum diketahui bahwa jika dipakai dengan hati-hati, potongan kode eksekusi yang dibuat kompiler bisa disalin dengan
memcpyJebakan besarnya saat itu adalah bit NX baru mulai tersebar luas. Bahkan di Linux, kebanyakan orang masih menganggap distribusi 32-bit sebagai hal yang wajar, dan ada juga yang kaget saat tahu CPU mereka mendukung 64-bit
Belakangan saya bahkan memakai netbook yang sama sekali tidak mendukung kode 64-bit
Sayangnya, saya terlalu banyak menghabiskan waktu untuk sisa kode lainnya, jadi tidak pernah cukup mendalaminya untuk membuat sesuatu yang benar-benar berguna
Sayangnya, pengumpulan proposal presentasi sudah ditutup, tetapi masih ada bagian “unconference”. Hanya saja topiknya ditentukan di lokasi acara, jadi tidak ada jaminan
Kalau ada cache, kueri yang sering dijalankan bisa di-cache dan dioptimalkan lebih agresif, sehingga dua pendekatan ini tampaknya bisa saling melengkapi untuk menutup biaya kompilasi
Tentu saja, itu sendiri akan menambahkan lapisan kompleksitas dan sumber sakit kepala yang benar-benar baru
Lihat bagian “notes” di https://www.postgresql.org/docs/current/sql-prepare.html
Jika sangat disederhanakan, ada pointer yang menunjuk ke bagian-bagian kueri yang bocor ke berbagai tempat di mesin eksekusi
Untuk menghilangkannya, dibutuhkan perombakan besar yang cukup menyeluruh, mencakup mesin eksekusi, planner, dan entah apa lagi
Bahkan dalam satu sesi tunggal pun, dua kueri yang dikompilasi akan punya kode kompilasi yang berbeda karena hal ini. Baik LLVM maupun copyjit buatan saya harus menyuntikkan alamat beberapa struct ke dalam kode assembly
Biasanya kita harus masuk langsung dan menambahkan hint atau memaksa rencana eksekusi
Bahkan kueri sederhana seperti
SELECT * FROM t WHERE x = TRUE;bisa menjadi mimpi buruk tergantung distribusi nilaixdi tabelDi Postgres saya hampir tidak pernah mengalami masalah seperti itu, walaupun saya akui belum pernah memakai Postgres bersama prepared statement
Saya pernah melihat kueri dengan waktu perencanaan lambat (lebih dari 100 ms), jadi cache mungkin akan berguna, tetapi saya tidak ingat pernah ada kasus yang benar-benar membutuhkan optimisasi itu
Mirip dengan stencil yang diajukan di sini, tetapi biasanya kodenya berupa satu instans tunggal, jadi jarang membuat salinannya
Misalnya Doom di DOS juga memakai teknik optimisasi seperti ini. Ini diperlukan untuk benar-benar mengeluarkan performa dari loop rendering yang ketat di CPU lama