TypeScript İçin Neden LLVM?
Bir AOT derleyici, bir JIT'ten farklı bir rejimde yaşar. Bir JIT, kullanıcı beklerken derler, bu yüzden kısıtlayıcı olan derleme gecikmesidir. Perry gibi bir AOT derleyici ise bir kez derler — geliştiricinin kendi makinesinde veya CI'da — ve ikili dosya bundan sonra milyonlarca kez çalıştırılır. Bu asimetri, tam olarak ağır bir optimize edicinin kendini amorti ettiği yerdir.
LLVM, yirmi yıllık middle-end çalışmasını beraberinde getirir: loop vectorization, loop-invariant code motion, global value numbering, sparse conditional constant propagation, agresif satır içi alma, alias analysis. Perry'nin işi, bu makineye gerçekten optimize edebileceği IR'ı vermektir — ve burada devreye TypeScript'in tip bilgisi girer.
İndirgeme hattı
Kaynak kod SWC ile ayrıştırılır, ardından tipli bir yüksek seviyeli IR'a (HIR) indirgenir; asıl ilginç kararlar LLVM kodu hiç görmeden önce burada verilir:
- Monomorphization. Generic fonksiyonlar ve sınıflar, her somut örnekleme (instantiation) için özelleştirilir — Rust ve C++'ın kullandığı stratejinin aynısı.
Stack<number>veStack<string>iki bağımsız, tamamen tipli fonksiyona dönüşür — böylece optimize edici generic bir dispatch blob'u yerine somut tiplerle çalışır ve generic'ler runtime'da hiçbir şeye mal olmaz. - Static dispatch. Alıcı (receiver) tipinin derleme zamanında bilindiği durumlarda, metot çağrıları hash tablosu aramaları değil, LLVM'in satır içine alabileceği doğrudan çağrılara derlenir.
- Doğrudan alan erişimi. Nesne alanları derleme zamanı indekslerine çözümlenir, böylece bir özellik okuma işlemi bir sözlük araması değil, sabit ofsetli bir yükleme (load) işlemidir.
NaN-boxing ve satır içi indirgemeler
Değerlerin dinamik olduğu durumlarda Perry, NaN-boxing kullanır: her değer 64-bit'lik bir word'dür. Double'lar doğrudan saklanır; nesneler, dizeler, boolean'lar, null ve undefined ise bir IEEE 754 quiet NaN'ın kullanılmayan bit kalıplarına kodlanır. Sayılar sıfır maliyetlidir (zero-cost) — aritmetik için boxing yok, allocation yok.
İşin can alıcı noktası şu: sayı olmayan değerler üzerindeki işlemler unpack-operate-repack bit dizilerine ihtiyaç duyar. Bu diziler ayrı derlenmiş bir runtime'a yapılan çağrılar olarak var olursa, LLVM onları opak kara kutular olarak görür ve aralarında optimizasyon yapamaz. Bu yüzden Perry, sık çalışan işlemleri — özellik okumaları, metot dispatch'i, nesne allocation'ı — optimize edicinin birleştirip sadeleştirebileceği satır içi LLVM IR olarak üretir. Örneğin nesne allocation'ı, satır içi thread-local bir bump allocation'a derlenir:
%off_ptr = getelementptr i8, ptr %state, i64 8
%offset = load i64, ptr %off_ptr ; current bump offset
%new_off = add i64 %offset, 96 ; headers + 8 fields
%sz_ptr = getelementptr i8, ptr %state, i64 16
%size = load i64, ptr %sz_ptr ; block capacity
%fits = icmp ule i64 %new_off, %size
br i1 %fits, label %fast, label %slowNeden Cranelift Değil?
Perry'nin ilk backend'i Cranelift'ti — wasmtime'ın arkasındaki kod üretimi, hızlı ve öngörülebilir derleme için tasarlanmış. Doğru bir başlangıç noktasıydı ve JIT'ler ile yalıtılmış runtime'lar için hâlâ mükemmel bir seçim. İki şey bu geçişi zorunlu kıldı:
- Optimize edicinin tavanı. Cranelift kasıtlı olarak hızlı, tek katmanlı bir derleyicidir: “çabucak makul kod”, bu da bir JIT için doğru takas iken, satış noktası tepe yerel performans olan bir AOT derleyici için yanlış takastır.
- arm64_32. Apple Watch, Cranelift'in desteklemediği bir ABI (64-bit komutlar, 32-bit işaretçiler) kullanır. watchOS'un bir hedef olarak var olabilmesi için LLVM gerekliydi — ve iki backend'i sürdürmek, iki kat hata, test ve performans referans noktası (baseline) demekti.
Bu geçiş bedelsiz olmadı: yalnızca LLVM kullanan ilk sürüm, sık çalışan işlemler başlangıçta opak runtime yardımcı fonksiyon çağrıları üzerinden geçtiği için bazı benchmark'ları 70 kata kadar yavaşlattı. Toparlanma — satır içi indirgemeler, yukarıdaki bump allocator, daha iyi satır içi alma sınırları — backend'i Cranelift'in rakamlarının ötesine taşıdı ve iş yerine oturduğunda Perry, kendi test paketindeki her benchmark'ta Node.js'i 1,7 kattan 24,6 kata kadar (iki eşitlikle) geride bıraktı (Nisan 2026). Tüm post-mortem yazısı okumaya değer: Cranelift'ten LLVM'ye.
Daha derine inmek
Derleyici iç yapısı sayfası NaN-boxing, monomorphization ve static dispatch konularını daha ayrıntılı ele alır. Blogda, Her Şeyi Optimize Etmek yazısı optimizasyon çalışmasını sürüm sürüm anlatır ve Kuşaksal GC, lazy JSON ve savunulabilir benchmark'lar yazısı benchmark metodolojisinin nasıl işlediğini açıklar (RUNS=11, medyan + p95). Daha büyük resim için TypeScript yerel derleyicisi genel bakışıyla başlayın.