Stabilitas tipe adalah kondisi ketika tipe setiap nilai di dalam fungsi, termasuk tipe nilai kembaliannya, dapat dipastikan dari tipe argumennya. Topik ini membahas cara mengenali fungsi yang stabil dan tidak stabil, memperbaiki cabang yang mengembalikan tipe berbeda, serta biaya tersembunyi dari field struct tanpa anotasi. Seluruh contoh disertai keluaran aslinya, sehingga perbedaan keduanya terlihat langsung dari tipe hasilnya.
Definisi dan istilah signature#
Sebuah fungsi dikatakan stabil (type-stable) apabila inferensi tipe selalu menghasilkan tipe yang sama untuk setiap jalur eksekusi pada satu kombinasi tipe argumen tertentu. Kondisi ini penting karena kecepatan Julia bergantung pada kemampuan menyiapkan instruksi mesin yang spesifik sejak kompilasi, dan persiapan itu hanya mungkin apabila tipe setiap operasi pasti.
Fungsi yang tidak stabil tetap berjalan dan tetap menghasilkan jawaban yang benar. Biayanya muncul pada kode mesin, yaitu compiler tidak dapat memilih satu instruksi sehingga menyiapkan jalur yang memeriksa tipe saat eksekusi, melakukan dispatch dinamis, dan mengemas hasil dalam wadah yang menampung lebih dari satu tipe. Perbedaannya terukur jelas pada perulangan yang ketat, dan klaim itu tidak dibiarkan sebagai kesan karena alat pengukurannya dibahas pada topik Perangkat Performa di modul ini.
Kata signature pada konteks ini merujuk pada kombinasi tipe argumen yang dipakai untuk memilih spesialisasi. Signature yang sama selalu menjalankan kode mesin yang sama, sehingga stabilitas dinilai per signature, bukan per fungsi secara keseluruhan.
Fungsi yang stabil dan tidak stabil#
Contoh berikut menyandingkan dua fungsi yang mirip bentuknya. Fungsi f menormalkan tanda bilangan titik mengambang, sedangkan fungsi g mengganti nilai negatif dengan nol. Keduanya dijalankan dari berkas dengan perintah julia stabil-tidak-stabil.jl.
# f stabil: kedua cabang mengembalikan Float64
# g tidak stabil: cabang mengembalikan Float64 dan Int64
f(x::Float64) = x > 0.0 ? x : -x
g(x) = x > 0 ? x : 0
println(typeof(f(3.5)), " dan ", typeof(f(-3.5)))
println(typeof(g(3.5)), " dan ", typeof(g(-3.5)))Float64 dan Float64
Float64 dan Int64Fungsi f mengembalikan Float64 pada kedua pemanggilan karena kedua cabangnya bertipe sama. Fungsi g tampak mirip, tetapi cabang negatifnya mengembalikan bilangan bulat 0 yang bertipe Int64, sedangkan cabang positifnya mengembalikan Float64. Hasil typeof memperlihatkan perbedaannya, yaitu g mengembalikan Int64 untuk argumen negatif dan Float64 untuk argumen positif.
Contoh g sekaligus menunjukkan jebakan yang paling sering ditemui, yaitu menulis literal 0 tanpa titik desimal pada fungsi yang bekerja pada bilangan titik mengambang. Niat penulisnya sama dengan zero, tetapi tipe hasilnya berbeda, dan inferensi tipe mencatat keduanya sebagai kemungkinan.
Membaca tanda Union pada @code_warntype#
Makro @code_warntype menampilkan hasil inferensi tipe secara terperinci untuk satu pemanggilan. Pada fungsi tidak stabil, tipe kembaliannya muncul sebagai Union, yaitu gabungan lebih dari satu tipe. Contoh berikut memanggil g melalui variabel alih-alih literal, karena pemanggilan dengan literal dapat terlipat konstan (constant folding) oleh compiler sehingga Union-nya tidak muncul.
using InteractiveUtils
g(x) = x > 0 ? x : 0
x = 3.5
@code_warntype g(x)MethodInstance for g(::Float64)
from g(x) @ Main contoh.jl:3
Arguments
#self#::Core.Const(Main.g)
x::Float64
Body::UNION{FLOAT64, INT64}
1 ─ %1 = Main.:>::Core.Const(>)
│ %2 = (%1)(x, 0)::Bool
└── goto #3 if not %2
2 ─ %4 = x::Float64
└── return %4
3 ─ return 0Baris Body::UNION{FLOAT64, INT64} adalah kesimpulan seluruh transkrip, yaitu tipe kembalian berupa gabungan Float64 dan Int64. Mulai Julia 1.11, tipe yang tidak stabil ditulis dengan huruf kapital dan diberi warna merah pada terminal, sehingga penyebabnya mudah ditemukan. Isinya juga menunjukkan jalannya dua cabang, yaitu blok 2 mengembalikan x bertipe Float64, sedangkan blok 3 mengembalikan konstanta 0 yang tetap Int64. Baris from pada bagian atas menunjuk berkas dan baris definisi fungsi, dan jalurnya dinormalkan menjadi contoh.jl pada transkrip ini karena folder eksekusi tidak berarti bagi pembaca.
Memperbaiki tipe kembalian#
Perbaikannya dilakukan dengan menyamakan tipe nilai pada kedua cabang. Fungsi zero(x) mengembalikan nilai nol dengan tipe yang sama dengan x, sehingga kedua cabang h berikut mengembalikan Float64 ketika x bertipe Float64.
h(x) = x > 0 ? x : zero(x)
println(typeof(h(3.5)), " dan ", typeof(h(-3.5)))Float64 dan Float64Prinsipnya, setiap cabang percabangan dan setiap titik kembalian mengembalikan satu tipe untuk satu signature. Konstanta zero(x), one(x), atau hasil konversi eksplisit menjadi pilihan yang tepat karena tipe hasilnya mengikuti tipe argumen. Apabila cabang memang sengaja mengembalikan tipe berbeda, pertimbangkan menyatukannya ke satu tipe tertentu, misalnya selalu Float64, karena tipe tunggal selalu lebih murah daripada Union.
Field struct tanpa anotasi#
Stabilitas tipe tidak hanya berlaku pada argumen dan nilai kembalian. Field struct yang dibiarkan tanpa anotasi bertipe Any, yaitu tipe yang dapat menampung apa pun. Struct berikut menyimpan catatan dengan field isi tanpa tanda :: sama sekali.
struct Catatan
isi
end
println(fieldtype(Catatan, :isi))
println(typeof(Catatan(7).isi), " dan ", typeof(Catatan(7.0).isi))Any
Int64 dan Float64Fungsi fieldtype menegaskan bahwa tipe field tersebut adalah Any, dan isi dua objek Catatan memang bertipe berbeda. Fleksibilitas ini membawa biaya, yaitu tipe hasil setiap pembacaan isi baru diketahui saat eksekusi. Akibatnya, operasi yang memakainya tidak dapat disiapkan sejak kompilasi, dan perulangan yang menyentuh field ini berjalan dengan dispatch dinamis per iterasi. Memberi anotasi tipe konkret pada field menghilangkan biaya tersebut.
Union kecil tetap dapat dioptimalkan#
Tidak semua Union berarti bencana. Inferensi tipe Julia menyertakan teknik pemisahan Union (union splitting) yang memeriksa kemungkinan tipe satu per satu apabila jumlahnya sedikit, sehingga kode dengan Union kecil kadang tetap cepat. Kondisi ini menjelaskan mengapa struktur seperti Union{Nothing, Float64} yang lazim pada data yang bisa kosong tetap layak dipakai tanpa perubahan.
Patokan praktisnya, anggap Union sebagai tanda periksa, bukan vonis. Periksa dengan @code_warntype apakah Union-nya masuk ke perulangan yang ketat dan apakah jumlah tipenya masih sedikit. Apabila keduanya ya, perbaiki tipe kembaliannya, sedangkan apabila tidak, biayanya dapat diabaikan dibandingkan dengan pekerjaan lain.
Kesalahan umum#
Kesalahan pada stabilitas tipe jarang berupa program yang gagal, melainkan program yang berjalan dengan benar tetapi lambat. Tiga kesalahan berikut paling sering muncul.
- Mengira anotasi tipe pada argumen otomatis menstabilkan kembalian. Anotasi seperti
g(x::Float64)membantu memilih metode, tetapi Union pada kembalian tetap muncul apabila cabangnya mengembalikan tipe berbeda, sehingga hasil inferensi tetap perlu diperiksa. - Menambahkan cabang baru yang mengembalikan tipe lain saat fungsi direvisi. Fungsi yang stabil hari ini dapat menjadi tidak stabil karena satu cabang baru, sehingga
@code_warntypeperlu dijalankan ulang setelah perubahan besar. - Membiarkan field struct tanpa anotasi karena tampak lebih fleksibel. Tipe Any menyembunyikan tipe nyata dari inferensi, dan biayanya muncul pada setiap pembacaan field dalam perulangan.
Ringkasan#
- Fungsi stabil memiliki satu tipe untuk setiap nilai dan kembalian pada satu signature, sedangkan fungsi tidak stabil mengembalikan Union antar cabangnya.
@code_warntypemenampilkan tipe tidak stabil sebagai Union dengan huruf kapital dan warna merah pada terminal sejak Julia 1.11.- Perbaikan dilakukan dengan menyamakan tipe cabang, misalnya memakai
zero(x)sebagai pengganti literal0. - Field struct tanpa anotasi bertipe Any dan memicu dispatch dinamis pada setiap pembacaannya.
Referensi#
- Performance Tips — Julia Documentation — Panduan resmi stabilitas tipe, deklarasi tipe, dan pemisahan Union.
- Interactive Utilities — Julia Documentation
— Dokumentasi makro
@code_warntypeyang menampilkan hasil inferensi tipe.