@code_warntype dan Sejenisnya

Membaca keluaran @code_warntype, @code_llvm, dan @code_native untuk mendiagnosis tipe yang tidak stabil.

Daftar isi

Topik ini mendiagnosis tipe yang tidak stabil dengan tiga makro dari pustaka standar InteractiveUtils, yaitu @code_warntype untuk hasil inferensi tipe, @code_llvm untuk LLVM IR, dan @code_native untuk assembly. Pembahasannya mencakup cara membaca keluaran ketiganya, urutan pemakaian yang efisien, dan batasan setiap alat. Setelah membacanya, penyebab kode lambat dapat ditelusuri secara bertahap dari tingkat tipe hingga tingkat instruksi.

Membaca keluaran @code_warntype#

Makro @code_warntype tersedia langsung pada REPL karena REPL memuat InteractiveUtils sejak awal, sedangkan skrip perlu memuatnya dengan using InteractiveUtils terlebih dahulu. Makro ini menampilkan hasil inferensi tipe untuk satu pemanggilan, lengkap dengan setiap operasi beserta tipe yang disimpulkan. Contoh berikut memakai fungsi kecepatan yang menghitung laju dari jarak dan waktu, dengan cabang pengaman ketika waktu bernilai nol.

julia Contoh: @code_warntype pada fungsi dua cabang
using InteractiveUtils

kecepatan(jarak, waktu) = waktu > 0 ? jarak / waktu : 0
jarak = 120.0
waktu = 8
@code_warntype kecepatan(jarak, waktu)
text Transkrip terminal
MethodInstance for kecepatan(::Float64, ::Int64)
  from kecepatan(jarak, waktu) @ Main contoh.jl:3
Arguments
  #self#::Core.Const(Main.kecepatan)
  jarak::Float64
  waktu::Int64
Body::UNION{FLOAT64, INT64}
1 ─ %1 = Main.:>::Core.Const(>)
│   %2 = (%1)(waktu, 0)::Bool
└──      goto #3 if not %2
2 ─ %4 = Main.:/::Core.Const(/)
│   %5 = (%4)(jarak, waktu)::Float64
└──      return %5
3 ─      return 0

Bagian atas transkrip memuat header MethodInstance beserta baris from yang menunjuk berkas dan baris definisi, dan jalurnya dinormalkan menjadi contoh.jl pada transkrip ini. Bagian Arguments mendaftar argumen beserta tipenya, yaitu jarak::Float64 dan waktu::Int64. Baris Body::UNION{FLOAT64, INT64} adalah temuan utamanya, yaitu nilai kembalian berupa gabungan Float64 dan Int64 yang ditulis dengan huruf kapital dan berwarna merah pada terminal.

Isi transkrip memuat blok bernomor yang meniru alur program. Blok 2 mengembalikan hasil pembagian bertipe Float64, sedangkan blok 3 mengembalikan konstanta 0 yang tetap Int64. Pola pembacaannya selalu sama, yaitu temukan UNION atau tipe kapital pada Body, telusuri blok yang menghasilkan setiap tipenya, lalu perbaiki cabang yang berbeda. Penanda merah pada terminal memang paling mudah dilihat secara visual, tetapi pola ini bekerja juga pada transkrip polos seperti pada contoh ini.

Apabila pustakanya terlupa dimuat pada skrip, error yang muncul terbungkus LoadError sebagaimana contoh berikut. Potongan ini tidak dieksekusi pada rangkaian contoh modul ini karena keluarannya bergantung pada keadaan sesi, yaitu apakah InteractiveUtils sudah dimuat oleh blok sebelumnya. Berikut keluaran aslinya pada skrip baru yang belum memuat pustaka tersebut.

julia
# tidak-dijalankan
# keluarannya bergantung keadaan sesi: pada skrip baru tanpa
# `using InteractiveUtils`, muncul LoadError seperti di bawah.
try
    @eval @code_warntype identity(1)
catch e
    println(typeof(e))
    println(split(sprint(showerror, e), "\n")[1])
end
text Transkrip terminal
LoadError
LoadError: UndefVarError: `@code_warntype` not defined in `Main`

Menilai kode mesin dengan @code_llvm#

Makro @code_llvm menampilkan LLVM IR, yaitu representasi antara yang mendekati instruksi mesin. Alat ini menunjukkan hal yang tidak terlihat pada @code_warntype, yaitu bagaimana hasil inferensi tipe diterjemahkan menjadi operasi. Contoh berikut membandingkan fungsi tidak stabil g dengan versi perbaikannya h, dan keduanya didefinisikan ulang agar blok berdiri sendiri.

julia Contoh: LLVM IR fungsi tidak stabil dan stabil
using InteractiveUtils

g(x) = x > 0 ? x : 0
h(x) = x > 0 ? x : zero(x)
x = 3.5

println("versi tidak stabil")
@code_llvm debuginfo=:none g(x)
println()
println("versi stabil")
@code_llvm debuginfo=:none h(x)
text Transkrip terminal
versi tidak stabil
; Function Signature: g(Float64)
define { ptr, i8 } @julia_g_1114(ptr noalias nocapture noundef nonnull align 8 dereferenceable(8) %union_bytes_return, double %"x::Float64") local_unnamed_addr #0 {
top:
  %0 = fcmp ule double %"x::Float64", 0.000000e+00
  br i1 %0, label %common.ret, label %L4

L4:                                               ; preds = %top
  store double %"x::Float64", ptr %union_bytes_return, align 8
  br label %common.ret

common.ret:                                       ; preds = %L4, %top
  %common.ret.op = phi { ptr, i8 } [ { ptr null, i8 1 }, %L4 ], [ { ptr @"jl_global#1117.jit", i8 -126 }, %top ]
  ret { ptr, i8 } %common.ret.op
}

versi stabil
; Function Signature: h(Float64)
define double @julia_h_1118(double %"x::Float64") local_unnamed_addr #0 {
top:
  %.inv = fcmp ogt double %"x::Float64", 0.000000e+00
  %".x::Float64" = select i1 %.inv, double %"x::Float64", double 0.000000e+00
  ret double %".x::Float64"
}

Tipe kembalian pada transkrip g terbaca { ptr, i8 }, yaitu wadah gabungan berisi pointer dan penanda tipe, bukan satu bilangan. Isinya memuat percabangan br dan simpul phi untuk menyusun wadah tersebut. Versi stabil h berbeda total, yaitu kembaliannya double murni dan isinya tinggal pemilihan nilai dengan select, tanpa wadah dan tanpa percabangan yang dijaga saat eksekusi. Perbandingan bentuk dan jumlah instruksi inilah cara memakai @code_llvm, yaitu menyandingkan versi yang dicurigai dengan versi yang sudah stabil. Akhiran angka pada nama fungsi seperti _1114 berasal dari penomoran internal dan dapat berbeda antar eksekusi, sebagaimana transkrip alat kompilasi lainnya pada modul ini.

Membaca assembly dengan @code_native#

Makro @code_native menampilkan instruksi mesin yang benar-benar dijalankan prosesor. Alat ini dipakai paling akhir karena paling sulit dibaca, tetapi menampilkan keadaan eksekusi apa adanya. Contoh berikut menampilkan assembly penjumlahan dua Float64 dengan sintaks Intel.

julia Contoh: membaca assembly dengan @code_native
using InteractiveUtils

tambah(x, y) = x + y
@code_native debuginfo=:none syntax=:intel tambah(2.0, 3.0)
text Transkrip terminal
; Function Signature: tambah(Float64, Float64)
# %bb.0:                                # %top
        push    rbp
        mov     rbp, rsp
        vaddsd  xmm0, xmm0, xmm1
        pop     rbp
        ret

Inti transkripnya hanya lima instruksi, yaitu prolog push dan mov yang menyiapkan frame, satu instruksi vaddsd yang menjumlahkan register xmm0 dan xmm1, lalu epilog pop dan ret. Baris direktif assembler, komentar #DEBUG_VALUE, serta alamat metadata pada awal dan akhir keluaran difilter dari transkrip karena panjang dan isinya berubah antar eksekusi. Instruksi vaddsd adalah penjumlahan titik mengambang skalar dari set instruksi vektor prosesor, dan kemunculannya menandakan pekerjaan benar-benar dilakukan oleh kode mesin tanpa lapisan penafsiran.

Kapan alat mana dipakai#

Ketiga alat menjawab pertanyaan yang berbeda, dan urutan pemakaiannya mengikuti biaya membacanya. Tabel berikut merangkum perannya.

AlatYang ditampilkanKapan dipakai
@code_warntypeTipe setiap nilai dan kembalianTitik awal diagnosis, yaitu mencari Union pada tipe
@code_llvmLLVM IR hasil kompilasiMemastikan akibat tipe tidak stabil berupa wadah dan operasi ekstra
@code_nativeInstruksi mesinKasus khusus seperti memeriksa instruksi vektor atau pemanggilan fungsi

Pemeriksaan cepat tanpa membaca transkrip panjang juga dimungkinkan. Fungsi Base.return_types mengembalikan tipe kembalian hasil inferensi untuk signature tertentu, sehingga cocok dipanggil dari skrip pengujian.

julia Contoh: tipe kembalian tanpa membaca transkrip
using InteractiveUtils

g(x) = x > 0 ? x : 0
h(x) = x > 0 ? x : zero(x)

println(Base.return_types(g, (Float64,)))
println(Base.return_types(h, (Float64,)))
text Keluaran
Any[Union{Float64, Int64}]
Any[Float64]

Keluarannya dua baris yang menjelaskan semuanya, yaitu g mengembalikan Union{Float64, Int64} sedangkan h mengembalikan Float64. Dengan demikian, urutan kerja yang disarankan berlapis: mulai dari tipe melalui @code_warntype atau return_types, lanjut ke LLVM IR apabila akibatnya perlu dilihat, dan buka @code_native hanya apabila keduanya belum menjelaskan.

Kesalahan umum#

Kesalahan dalam memakai alat diagnostik biasanya bukan pada alatnya, melainkan pada pembacaan hasilnya. Tiga kesalahan berikut perlu diwaspadai.

  1. Menyimpulkan kecepatan dari panjang LLVM IR saja. Baris yang panjang dapat berasal dari logika yang memang kompleks, bukan dari tipe tidak stabil, sehingga pembanding yang tepat adalah versi stabil dari fungsi yang sama.
  2. Menganggap setiap Union harus dihilangkan. Pemisahan Union menangani Union kecil dengan baik, sehingga fokus perbaikan sebaiknya diarahkan pada Union yang masuk perulangan ketat dengan banyak tipe.
  3. Memakai @code_native sebagai alat pertama. Assembly hanya menunjukkan akibat terakhir, sedangkan penyebabnya hampir selalu terlihat lebih awal pada tipe dan LLVM IR.

Ringkasan#

  • @code_warntype menampilkan tipe setiap operasi, dan Union dengan huruf kapital pada Body adalah tanda tipe tidak stabil.
  • @code_llvm memperlihatkan akibatnya pada kode mesin, misalnya wadah gabungan { ptr, i8 } dibandingkan dengan double murni pada fungsi stabil.
  • @code_native menampilkan instruksi prosesor dan dipakai paling akhir karena paling sulit dibaca.
  • Urutan pemakaian yang efisien adalah tipe terlebih dahulu, lalu LLVM IR, dan assembly hanya apabila keduanya belum menjelaskan.

Referensi#