Apa itu Julia?

Bahasa tingkat tinggi yang dikompilasi menjadi instruksi native melalui LLVM, sehingga notasi matematis berjalan mendekati kecepatan C.

Daftar isi

Topik ini memperkenalkan Julia sebagai bahasa pemrograman untuk komputasi numerik, mulai dari asal dan tujuannya, cara kerja kompilasinya hingga menghasilkan kode mesin, sampai ciri notasinya yang menyerupai tulisan matematis. Beberapa contoh kode disertakan beserta keluaran aslinya, sehingga klaim tentang spesialisasi tipe dan notasi dapat diverifikasi langsung. Setelah topik ini dibaca, posisi Julia di antara bahasa pemrograman lain dan alasan di balik kecepatannya dapat dijelaskan secara runtut.

Asal dan tujuan Julia#

Julia adalah bahasa pemrograman tingkat tinggi untuk komputasi numerik yang dirancang di Massachusetts Institute of Technology dan dirilis secara terbuka pada tahun 2012. Perancangnya, yaitu Jeff Bezanson, Stefan Karpinski, Viral Shah, dan Alan Edelman, menyatakan tujuan desain bahasa ini dalam artikel berjudul Why We Created Julia. Julia didistribusikan sebagai perangkat lunak bebas dengan lisensi MIT, sehingga kode sumbernya dapat dibaca, diubah, dan digunakan ulang tanpa biaya lisensi. Rilis stabil terbarunya berasal dari seri 1.13, dan jalur ini menggunakan seri tersebut.

Sebelum Julia muncul, pekerjaan komputasi ilmiah lazimnya terbagi ke dalam dua bahasa berbeda. Prototipe ditulis dalam bahasa yang mudah seperti Python, R, atau MATLAB, sedangkan bagian yang lambat ditulis ulang dalam C atau Fortran agar cepat. Pembagian kerja ini dikenal sebagai two-language problem, yaitu kebutuhan menulis satu program dengan dua bahasa sekaligus. Tujuan utama Julia adalah menghapus kebutuhan itu, sehingga notasi yang sama untuk bereksperimen tetap layak digunakan untuk perhitungan yang berat.

Lingkup Julia saat ini tidak berhenti pada komputasi numerik. Ekosistemnya mencakup pengolahan data, pemodelan, dan pengembangan package, tetapi komputasi numerik tetap menjadi bidang yang paling diuntungkan oleh desainnya. Hal itu karena kecepatan eksekusi dan kepraktisan penulisan berada dalam satu bahasa yang sama, dan bagian berikutnya menjelaskan mekanisme yang mewujudkannya.

Kompilasi ke kode mesin melalui LLVM#

Julia mengeksekusi fungsi dengan mengompilasinya menjadi kode mesin melalui LLVM, yaitu pustaka kompiler yang juga menjadi dasar Clang dan Rust. Alurnya bertahap: kode sumber diurai menjadi pohon sintaks, tipe setiap operasi disimpulkan dari tipe argumennya, lalu LLVM menghasilkan instruksi native, yakni instruksi yang langsung dipahami prosesor tanpa lapisan penafsiran. Kompilasi berlangsung secara just-in-time, yaitu saat fungsi pertama kali dipanggil dengan kombinasi tipe argumen tertentu, dan hasilnya disimpan untuk pemanggilan berikutnya. Satu fungsi Julia dengan demikian dapat memiliki beberapa versi kode mesin, satu versi untuk setiap kombinasi tipe yang pernah digunakan.

Alur tersebut dirangkum pada diagram berikut. Diagram ini membedakan Julia dari bahasa yang menafsirkan setiap baris berulang kali, karena pekerjaan berat pada Julia selesai satu kali per kombinasi tipe.

Kode sumberberkas .jlParserpohon sintaksInferensi tipeper tipe argumenLLVM IRrepresentasi antaraKode mesininstruksi nativeKompilasi berlangsung sekali untuk setiap kombinasi tipe argumen, lalu hasilnya digunakan ulang.
Alur kompilasi Julia dari kode sumber hingga kode mesin

Efek spesialisasi tipe ini dapat dilihat langsung. Skrip berikut menampilkan LLVM IR, yaitu representasi antara sebelum menjadi instruksi mesin, untuk sebuah fungsi penjumlahan yang dipanggil dengan dua bilangan Float64. Hasilnya hanya satu instruksi penjumlahan titik mengambang, seperti kode mesin yang ditulis manual dalam C.

julia Contoh: LLVM IR untuk penjumlahan dua Float64
# menampilkan LLVM IR hasil kompilasi fungsi tambah untuk dua Float64
using InteractiveUtils

tambah(x, y) = x + y

io = IOBuffer()
code_llvm(io, tambah, (Float64, Float64); debuginfo=:none)
ir = String(take!(io))

# akhiran angka pada nama fungsi bisa berubah antar sesi, sehingga dinormalkan
println(replace(ir, r"_[0-9]+" => "_N"))
text Keluaran
; Function Signature: tambah(Float64, Float64)
define double @julia_tambah_N(double %"x::Float64", double %"y::Float64") local_unnamed_addr #0 {
top:
  %0 = fadd double %"x::Float64", %"y::Float64"
  ret double %0
}

Blok IR di atas memuat baris fadd double, yaitu instruksi penjumlahan titik mengambang untuk bilangan pecahan pada arsitektur x86-64. Apabila fungsi yang sama dipanggil dengan dua bilangan Int64, LLVM menghasilkan add i64, instruksi penjumlahan bilangan bulat. Keduanya berbeda karena Julia mengompilasi ulang fungsi untuk setiap kombinasi tipe, dan itulah yang memungkinkan satu definisi fungsi tetap abstrak tanpa mengorbankan kecepatan. Akhiran angka pada nama fungsi diganti menjadi _N oleh skrip karena nilainya dapat berubah antarsesi.

Notasi matematis yang utuh di kode#

Notasi Julia dirancang agar rumus tetap dekat dengan bentuk aslinya. Bilangan π tersedia sebagai konstanta π, pangkat ditulis dengan ^, dan nama variabel boleh menggunakan karakter Unicode. Konstanta π dikenali Julia tanpa pemuatan pustaka apa pun, sehingga rumus luas lingkaran dapat ditulis apa adanya, termasuk interpolasi nilai r ke dalam string dengan notasi $r.

julia Contoh: notasi matematis pada kode Julia
# luas lingkaran berjari-jari 10
r = 10.0
luas = π * r^2
println("Luas lingkaran jari-jari $r: ", luas)
text Keluaran
Luas lingkaran jari-jari 10.0: 314.1592653589793

Pola yang sama berlaku untuk rumus yang lebih panjang. Akar persamaan kuadrat berikut dicari dengan rumus ABC.

x=−b±b2−4ac2a x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}

Skrip kuadrat.jl menerapkan rumus tersebut pada persamaan x^2 - 5x + 6 = 0. Skrip dijalankan dari terminal dengan perintah julia kuadrat.jl, dan seluruh baris keluaran diproduksi oleh println.

julia kuadrat.jl
# akar persamaan kuadrat x^2 - 5x + 6 = 0
a = 1.0
b = -5.0
c = 6.0

diskriminan = b^2 - 4 * a * c
x1 = (-b + sqrt(diskriminan)) / (2 * a)
x2 = (-b - sqrt(diskriminan)) / (2 * a)

println("Akar pertama: ", x1)
println("Akar kedua:   ", x2)
println("Cek: x1^2 - 5x1 + 6 = ", x1^2 - 5 * x1 + 6)
text Keluaran
Akar pertama: 3.0
Akar kedua:   2.0
Cek: x1^2 - 5x1 + 6 = 0.0

Keluarannya berupa 3.0 dan 2.0, bukan bilangan bulat, karena koefisiennya ditulis sebagai Float64 sehingga seluruh perhitungan berjalan pada bilangan pecahan. Baris pemeriksaan terakhir mengevaluasi kembali persamaannya pada x1 dan menghasilkan 0.0, yang menandakan akar yang ditemukan memenuhi persamaan. Notasi seperti π, ^, dan $r akan muncul berulang kali pada jalur ini karena memang menjadi cara standar menulis kode Julia.

Kesalahan umum#

Kesalahan pertama adalah menilai kecepatan Julia dari pemanggilan pertama sebuah fungsi. Pemanggilan itu mencakup waktu kompilasi just-in-time, sehingga terasa jauh lebih lambat daripada pemanggilan kedua yang sudah menggunakan kode mesin yang tersimpan. Perbaikannya, panggil fungsi sekali untuk menghangatkan, kemudian ukur pada pemanggilan-pemanggilan berikutnya.

Kesalahan kedua adalah menulis perulangan panjang di tingkat teratas skrip dengan variabel global. Tipe variabel global dapat berubah kapan saja, sehingga inferensi tipe tidak dapat mengunci satu instruksi mesin untuk seluruh perulangan, dan perhitungan berjalan jauh lebih lambat dari kemampuan Julia. Perbaikannya, bungkus perhitungan dalam fungsi dan terima datanya melalui argumen, karena kode di dalam fungsi memberi inferensi tipe ruang kerja yang jauh lebih baik.

Kesalahan ketiga adalah mengira setiap fungsi harus diberi anotasi tipe agar cepat. Spesialisasi berlangsung otomatis dari tipe argumen yang dikirim, sehingga fungsi tanpa anotasi tipe pun menghasilkan kode mesin yang sama cepatnya. Anotasi tipe tetap berguna untuk kejelasan kontrak fungsi, tetapi bukan syarat kecepatan pada kasus umum.

Ringkasan#

  • Julia adalah bahasa tingkat tinggi bebas dan terbuka dengan lisensi MIT untuk komputasi numerik, dirilis pertama kali pada tahun 2012.
  • Fungsi dikompilasi secara just-in-time melalui LLVM, dengan satu versi kode mesin untuk setiap kombinasi tipe argumen.
  • Hasil kompilasi disimpan dan digunakan ulang, sehingga notasi yang abstrak tidak mengorbankan kecepatan eksekusi.
  • Konstanta π, pangkat ^, dan interpolasi string $r menjaga rumus tetap dekat dengan bentuk matematisnya.

Referensi#