Benchmark Sonuçları
Tulpar'ın CPU ve HTTP iş yüklerinde C / Go / Node.js / FastAPI karşısında nasıl performans verdiği.
Tulpar’ın sloganı “Python kadar kolay, C kadar hızlı”. CPU’da fib ve
strcat’te C’yi — ona -O3 -march=native -flto verildiğinde bile — geçiyor,
sieve ve intloop’ta başa baş; saf kayan nokta aritmetiğinde C’den ayırt
edilemiyor, ama kayan nokta dizilerinde 5–27× geride (ölçüldü,
aşağıda); HTTP’de
çok-çekirdekli listen_pool sunucusu Go’nun net/http’sini geçiyor ve
FastAPI’yi çok geride bırakıyor — hepsi gönderilecek bir runtime olmadan, tek
bağımsız binary’den.
CPU benchmark’ları
Section titled “CPU benchmark’ları”Dokuz dil, sekiz mikro-kıyas, tek makine. Düşük olan hızlı; her sütunun en
hızlı girdisi kalın. Her dil iş yükü boyutunu BENCH_N ortam değişkeninden
okur, böylece hiçbir derleyici döngüyü kapalı forma katlayıp onu hiç
çalıştırmadan “kazanamaz”. 7 koşumun en iyisi, 2026-09-08, tek bir Linux
kutusunda (Tulpar LLVM 22 ile derlendi), commit 3edb4f4.
| Dil | fib(32) | sieve(5M) | strcat(2M) | arrayiter(5M) | intloop(50M) |
|---|---|---|---|---|---|
| Tulpar AOT | 0,6 | 7,7 | 13,2 | 1,2 | 134,5 |
| C (gcc -O2) | 1,6 | 7,6 | 37,6 | 2,2 | 134,4 |
| C++ (g++ -O2) | 1,9 | 8,0 | 14,7 | 2,7 | 134,9 |
| Rust (-O3) | 3,8 | 8,1 | 18,7 | 1,5 | 144,0 |
| Go | 6,7 | 8,5 | 24,3 | 4,3 | 134,5 |
| Java | 12,4 | 19,7 | 32,9 | 18,2 | 144,1 |
| C# (.NET) | 20,3 | 20,9 | 31,2 | 18,3 | 149,3 |
| Node.js | 24,6 | 28,2 | 96,4 | 18,5 | 712,2 |
| Python | 140,8 | 448,1 | 200,0 | 410,2 | 3127,3 |
Aynı makinede boş program taban çizgisi — süreç başlatma maliyeti, ve yukarıdaki her sayının içinde: C 0,17 · Tulpar 0,23 · C++ 0,44 · Python 5,6 · C# 8,2 · Node 10,7 ms.
Tulpar AOT fib’de C’yi geçiyor, sieve ve intloop’ta başa baş.
Yukarıdaki strcat ve arrayiter satırları, C’ye eşit bayraklar ve aynı
tamsayı biçimlendirme yordamı verilince kazanç değil — bu tablodan bir şey
alıntılamadan önce aşağıdaki iki bölümü oku.
Bir satırı alıntılamadan önce dikkatle okunması gereken üç şey:
intloopvesievefiilen dörtlü beraberlik. İkisinde de ilk dört dil birbirinin 0,5 ms içinde; bu aralıkta sıra numarası koşumdan koşuma değişiyor. Anlamlı olan bant, bandın içindeki sıra değil.strcatte C sondan üçüncü (37,6 ms) — elle yazılmışrealloc+snprintfdöngüsü, C++‘ınstd::stringinden 2,5 kat yavaş. Kap seçimi dil seçimini yeniyor; “C her zaman en hızlıdır” bir yasa değil.- Java ve C# bu sayılarda açılış maliyeti taşıyor. Tek başına C#‘ın boş programı 8,2 ms. Kıyas, kullanıcının hissettiği şey olduğu için duvar saatini ölçüyor; maliyet çıkarılmıyor, gösteriliyor — ama kısa iş yüklerinde o iki satırın çoğu bu.
”C’den hızlı” iddiası C’nin EN İYİ bayraklarına dayanıyor mu?
Section titled “”C’den hızlı” iddiası C’nin EN İYİ bayraklarına dayanıyor mu?”Tablo C’yi gcc -O2 ile derliyor — Tulpar’ın kendisinin hedeflediği jenerik
tabanla aynı (LLVM hedef CPU’su varsayılan "generic"; -march=native
karşılığı opt-in ve bu sayılarda kullanılmıyor). Adil bir varsayılan, ama
“C’den hızlı” sıra dışı bir iddia; o yüzden C’ye en iyi bayrakları verip yeniden
ölçüldü. 12 dönüşümlü tekrar, ortanca ± MAD, AMD Ryzen 7 9800X3D (Zen 5,
5,27 GHz):
| Kıyas | C -O2 | C -O3 -march=native -flto | Tulpar (generic) | Sonuç |
|---|---|---|---|---|
fib(32) | 2,49 | 2,08 ± 0,04 | 0,77 ± 0,02 | Tulpar 2,7× — duruyor |
strcat(2M) | 37,95 | 36,98 ± 0,37 | 13,27 ± 0,21 | Tulpar 2,8× — duruyor |
sieve(5M) | 7,86 | 7,75 ± 0,14 | 7,77 ± 0,10 | berabere (MAD içinde) |
arrayiter(5M) | 2,50 | 1,85 | 2,03 | C native ile geri alıyor |
Yani dürüst ifade tek bir “C’yi geçtik”ten dar: fib ve strcat kazançları
sağlam ve C’nin en iyi bayraklarına karşı da duruyor; sieve/intloop
berabere; arrayiter yalnız eşit jenerik bayrakta Tulpar’ın. Tek başına
-O2 → -O3 neredeyse hiçbir şey değiştirmiyor (fib 2491 → 2290 µs, strcat
37951 → 38133); arrayiter’i çeviren şey -march=native.
Sayıların tek başına taşımadığı iki açıklama:
-
strcatyalnız derleyicileri değil, farklı araçları kıyaslıyor. C tarafı naif değil — geometrik büyümeli tampon (cap *= 2) — ama her sayıyı genel amaçlısnprintfile biçimliyor; Tulpar tam bu yol için optimize edilmiş özel bir tamsayı→dizgi yordamı kullanıyor. Elleitoayazan bir C programcısı farkın çoğunu kapatır. -
fibkazancı mikro-mimari değil, ALGORİTMİK (2026-09-11’de ölçülüp adlandırıldı). Süreç açılışı aynı ikiliden çıkarılıp büyüme üssü ölçüldü (naif ağaç φ = 1,618,n’deki her +1 için):üs clang -O3 1,604 gcc -O2 1,607 Tulpar, zincir kapalı 1,612 Tulpar 1,488 gcc’nin üssü de naif — gcc’nin
fibgövdesi 264 komut (clang 20), yani agresif açma, ama sabit çarpan kazancı. Tulpar’ın klon zincirifibi bir kez satır içine alıyor;fib(n) = fib(n-2) + 2·fib(n-3) + fib(n-4)oluyor ve ortak alt ifade eliminasyonu yinelenenfib(n-3)ü birleştiriyor. Geriye 3 çağrı kalıyor (makine kodunda doğrulandı), bağıntıT(n) = T(n-2)+T(n-3)+T(n-4)oluyor ve kökü 1,4656 — ölçülen 1,488.⚠ Bunun sonucu:
fiboranı sabit değil,nile büyüyor. Açılış çıkarılmış, gcc-O2ye karşı: n=34’te 12,3× · n=36’da 12,0× · n=38’de 14,5× · n=40’ta 17,3×. Yukarıdaki tablodakifib(32)satırı ıraksayan bir eğrinin üstünde bir nokta — daha küçükn’de fark kapanır, daha büyükte açılır. Dürüst cümle: aynı karmaşıklık sınıfı, daha küçük taban — her iki taraf da üstel; fark tabanda (1,488 vs 1,607), sınıfta değil. “Farklı sınıf” polinom-vs-üstel demek olurdu; burada olan o değil. Oran(1,607/1,488)ⁿile büyüyor, yani sınırsız — ama büyüme aynı ailenin içinde.
Kayan nokta: tek cevap yok, iki ayrı cevap var
Section titled “Kayan nokta: tek cevap yok, iki ayrı cevap var”Yukarıdaki beş çekirdeğin hepsi tam sayı ya da dizgi. “Peki ya float?”
sorusu 2026-09-11’de üç yeni çekirdekle ölçüldü — ve tek sayıyla
cevaplanamayacağı ortaya çıktı, çünkü iki ayrı maliyet var:
- değer temsili — her işlemde kutu aç/kapa
- depolama temsili — dizide eleman başına kaç bayt
Çekirdekler bunları ayıracak şekilde seçildi:
| Çekirdek | Ne ölçer | Dizi kullanır mı | C (gcc -O2) | Tulpar | Oran |
|---|---|---|---|---|---|
mandelbrot(2000) | saf kayan nokta aritmetiği | hayır | 158,6 | 158,4 | 1,00× |
nbody(3M) | aritmetik + küçük dizi + sqrt | evet, küçük | 114,8 | 1326,1 | 11,6× |
matmul(640) | kayan nokta dizisi | yalnız o | 31,0 | 824,5 | 26,6× |
Saf kayan nokta aritmetiğinde Tulpar C’den ayırt edilemiyor — 158,4 ms’ye
karşı 158,6 ms. Aynı koşuda Python 15 721 ms. Yani float değerleri
kutulanmıyor; aritmetik yerel değişkenlerde ham double olarak yürüyor.
Dizilerde durum tersine dönüyor. Sebebi ölçüldü — aynı döngü, yalnız eleman tipi değişiyor (20M eleman):
| süre | bayt/eleman | |
|---|---|---|
int[], C (long long*) | 13,9 ms | 8,1 |
int[], Tulpar | 20,6 ms | 4,1 |
float[], C (double*) | 16,0 ms | 8,1 |
float[], Tulpar | 87,0 ms | 16,1 |
Tulpar’ın tam sayı dizisi C’ninkinden dar (eleman başına 4,1 bayt — 32
bite sığan değerler otomatik daraltılıyor). Aynı motorda kayan nokta dizisi
iki katı yer tutuyor, çünkü kutulanmamış depolama şu an yalnız tam sayılar için
var. matmul’un 26,6בi düz taramanın 4,2בinden büyük: kutulu elemanlar
“kanıtlı erişim” hızlı yoluna giremiyor ve 16 baytlık elemanlar derleyicinin iç
döngüyü vektörleştirmesini de engelliyor.
⚠ Bu tablo, tek çekirdekle ölçmenin neden yanıltıcı olduğunun da kaydı:
yalnız mandelbrot’a bakan “float bedava”, yalnız matmul’a bakan
“float 26× yavaş” derdi. İkisi de yanlış, çünkü ikisi de gerçek.
Bu takımın KANITLAMADIĞI şeyler
Section titled “Bu takımın KANITLAMADIĞI şeyler”Tek makinede sekiz mikro-çekirdek “en hızlı dil” iddiasını taşıyamaz.
Kapsam dışı: elle yazılmış SIMD, tahsis baskısı ve hash-map/JSON yükleri —
arena modelinin bedeli ancak orada görünür — işaretçi takibi, sıralama, çok
iş parçacıklı ölçekleme, RSS, ve sürekli yük altında p99.
Savunulabilir okuma: Tulpar tam sayı, dizgi ve skaler kayan nokta
çekirdeklerinde C sınıfında, Node/Python/Java/C#‘ın açık ara önünde, fib ile
strcat’te özellikle C’nin de önünde — ve kayan nokta dizilerinde ölçülmüş
biçimde geride (yukarıdaki FP bölümü).
Makine: AMD Ryzen 7 9800X3D (Zen 5, 8Ç/16İP, 96 MB 3D V-Cache), Linux. Araç zincirleri: gcc 16.2.1 · rustc 1.89.0 · go 1.27.1 · Tulpar AOT (LLVM 22).
Bu sayılar nasıl dürüst tutuluyor
Section titled “Bu sayılar nasıl dürüst tutuluyor”Bu sayfanın daha önceki bir sürümü Tulpar’ı “C’nin 1,37–1,9 katı sürede” diye
gösteriyordu. O tablo Eylül 2026’da denetlendi ve çöpe atıldı — her biri
tek başına sonucu geçersiz kılan üç kusuru vardı; en ağırı, iş yükü boyutunu
çalışma zamanında yalnızca Tulpar’ın okuması, gcc -O2 ve rustc -O3’ün ise
kendilerininkini sabit-katlayıp yok etmesiydi. Yerine gelen takım
benchmarks/fair/
altında ve üçünü de düzeltiyor:
- İş yükü boyutu her dilde ortamdan geliyor, yani derleme zamanında hiçbir şey katlanamıyor.
- Aynı algoritma, aynı veri yapısı — her dil kendi deyimsel aracını
kullanarak (
StringBuilder/std::string/strings.Builder/Int32Array/int[]). Birine naif yolu dayatmak dili değil, o tuzağı ölçerdi. - Çıktılar diller arasında karşılaştırılıyor. Uyuşmazlarsa satır yayınlanmıyor, geçersiz raporlanıyor. Yukarıdaki beş satırın hepsi uyuşuyor.
- Isıtma koşumu atılıyor, en iyi ve ortanca kaydediliyor, boş program taban çizgisi yanına yazılıyor ki bir sayının ne kadarının açılış olduğunu görebilesin.
Yeniden üretmek için: cd benchmarks/fair && python3 run.py. Eksik araç
zincirleri kendi satırlarını düşürür, kalanlar normal koşar.
HTTP throughput
Section titled “HTTP throughput”benchmarks/loadtest (native C yük üreteci), her sunucuyu keep-alive bağlantılar
üzerinden GET / → JSON {"hello":"world"} ile döver; concurrency 1–12 arasında
taranır (kutunun çekirdek sayısının altında tutulur, böylece yük üreteci sunucuyu
aç bırakmaz), seviye başına 4 sn, en iyi koşu ikinci geçişle doğrulanır.
Kutu: 14-vCPU WSL2. Her runtime kendi önerilen tek-process yapılandırmasında.
Bu HTTP rakamları, yukarıdaki CPU tablosundan ayrı ve daha eski bir koşumdan, farklı bir makineden geliyor — satırları kendi tablosu içinde karşılaştır, tablolar arasında değil.
| Sunucu | istek/sn | p50 latency | Yapılandırma |
|---|---|---|---|
Tulpar listen_pool | ~36k | 0.32 ms | 14 çekirdek, 1 process |
Go net/http | ~30k | 0.38 ms | tüm çekirdekler (default), 1 process |
Node.js http | ~8.7k | 1.06 ms | 1 thread (default) |
| FastAPI (uvicorn) | ~3.5k | 3.31 ms | 1 worker (default) |
Tulpar listen | ~4–4.7k | 0.22 ms | 1 thread, seri accept döngüsü |
Thread modelleri farklıdır ve yukarıda etiketlenmiştir: listen_pool ve Go’nun
net/http’si kutudan çıkarken tüm çekirdekleri kullanır, Node ve tek bir uvicorn
worker’ı ise default’ta tek. Tulpar’ın tek-thread listen()’i seri bir
accept döngüsüdür — en düşük request-başı latency’ye sahip (0.22 ms p50) ama
keep-alive bağlantıları serileştirir; bu yüzden throughput için listen_pool
(veya listen_async) kullan. Dikkat: tek-thread listen() burada tek-thread
Node’un gerisinde kalıyor; Tulpar’ın üstünlüğü listen_pool’un çekirdekler
arasında temiz ölçeklenmesinden geliyor (36k istek/sn’de p50 0.32 ms’de kalıyor,
temiz sub-millisecond tail ile).
Özellikle FastAPI’ye karşı Tulpar latency (~10× daha düşük p50) ve ayak izinde de net kazanıyor — ayrıntılı Wings vs FastAPI yazısına bak (yük altında p50 0.31 ms vs 28 ms, 6.7 MB vs 54 MB RSS, 2 MB bağımsız binary vs Python + ~50 MB bağımlılık).
Bizi buraya getirenler
Section titled “Bizi buraya getirenler”İlk adil ölçümden (2026-09-02) bu yana — ki o ölçümde Tulpar beş kıyasın
hiçbirinde ilk üçe girmiyordu: strcat 233,5 → 13,2 ms (17,7×), fib(32)
5,8 → 0,6 ms (9,7×), sieve 60,4 → 7,7 ms (7,8×), arrayiter
6,6 → 1,2 ms (5,5×). intloop beklendiği gibi hiç kıpırdamadı — seri
bağımlılık zinciri olduğu için derleyiciyi değil, CPU’yu ölçüyor.
Derleyici / CPU
Section titled “Derleyici / CPU”- Kutulanmamış sayısal diziler. Bir dizi ya kutulu
VMValuevektörü ya da ham tamsayı tamponu; eleman erişimi codegen’de satır içi GEP + load, üstüne eleman deposunu dizi başlığından ayıran TBAA ve döngü-değişmezi şekil önbelleği — işaretçi ile uzunluk yineleme başına değil, döngü başına bir kez okunuyor. - Döngü sürümleme.
for/whilegövdesi iki kez üretiliyor; hızlı sürümde sınır denetimi hiç yok, çünkü döngü girişindeki tek bir sınav onu gereksiz kanıtlıyor. Kanıt sözdizimsel değil, anlamsal. - 32-bit eleman deposu. C/Rust/Go elekte 4 baytlık eleman kullanıyor, biz 8
kullanıyorduk. Diziler artık 32-bit başlıyor ve sığmayan bir değer yazılınca
genişliyor (asla kutulanmıyor) — dilin
inti 64-bit kalıyor. Genişlik dalı eleman erişiminde değil sürüm seçiminde, yani iki genişlik de sıcak yolda dalsız. - Özyineleme zinciri. LLVM bir fonksiyonu kendi içine satır içi almaz (gcc
alır —
fibde bütün LLVM dillerini 2,4 kat geçmesinin tek sebebi buydu). Arka uç dört kopya üretip halkaya diziyor; artık her kenar iki farklı fonksiyon arası çağrı olduğu için sıradan satır içi alıcı onları açıyor. - Açılış maliyeti. Her ikili OpenSSL yüklüyor ve libstdc++‘ı dinamik bağlıyordu: boş program 1,15 → 0,23 ms.
- Dizgi kurma.
StringBuilder+ kutulamasızsb_append+ hızlıitoa; ayrıca dizgi sabitleri internleniyor. - Tipsiz yol.
%operatörünün kutulu satır içi hızlı yolu yoktu ve her kutulu global ataması koşulsuz bir çalışma zamanı çağrısı ödüyordu; ikisi de kapatıldı. Kutulu fonksiyonlar ayrıca değer ABI’sine geçti (argüman ve dönüş yazmaçta): tipsizfib11,9 → 7,7 ms.
Bunların hepsi ölçümle bulundu; ölçülüp elenen denemeler de yanlarında
docs/mindmap/Performance.md
içinde kayıtlı.
Sunucu sıcak yolu (HTTP)
Section titled “Sunucu sıcak yolu (HTTP)”call(handler_name)dlsym cache (256-slot FNV-1a hash) — request başı sembol-tablosu walk’unu eliyor.- Accept’te TCP_NODELAY — Nagle’ın 40ms gecikmesini kaldırır (+%13).
- Static thread-local recv buffer — keep-alive request başı 64KB malloc/free pair’ini düşürür.
- Per-request arena reset + per-request malloc region — uzun süreli sunucularda sızıntısız bounded memory.
- Built-in’lerde thread-local scratch buffer — TLS olmayan static’ler
listen_poolaltında yarışıyordu (birtoStringbuffer’ı düzeltilene dek ~%1.1 hatalı 404’e yol açtı). break/continuereal codegen — eskiden silently no-op’tu; LLVM induction-variable update’lerinde suboptimal phi node üretmesini engeller.
Bilinçli atladığımız trade-off’lar (analiz edildi, düşük değer):
- Object-key inline caching (
req["method"]HTTP path’in ~%0.3’ü). - String concat coalescing (
a + b + cHTTP path’in ~%0.1’i).
Metodoloji
Section titled “Metodoloji”- CPU: tek Linux kutusunda dokuz araç zinciri —
gcc -O2/g++ -O2/rustc -O3/ Go / Java / .NET / Node / CPython / Tulpar AOT (LLVM 22). Isıtma koşumu atılır, 7 duvar saati koşumunun en iyisi alınır, iş yükü boyutu çalışma zamanındaBENCH_N’den okunur (hiçbir derleyici sabit-katlayamaz) ve çıktılar diller arasında karşılaştırılır. - HTTP: native
benchmarks/loadtest, tek kutu, keep-alive, concurrency çekirdek sayısı ≤ tutulur, böylece yük üreteci ve sunucu CPU için yarışmaz. Toolchain’ler: Go 1.23, Node 22, Python 3.14 + FastAPI/uvicorn. Sunucular default tek-process yapılandırmasında çalışır (çekirdek kullanımı satır başına etiketli). - Mutlak rakamlar kutuya özeldir; taşınabilir sinyal olarak oranları ve
latency’yi baz al. CPU’yu
cd benchmarks/fair && python3 run.pyile yeniden üret (sonuçları emekliye ayrılan takım eskirun_benchmarks.sh’tir); buradaki HTTP sunucuları + sürücü, aynı JSON’u döndüren minimal eşdeğerlerdir. - Bunlar mikro-kıyaslar — tek makinede sıkı döngüler. Derleyici ve çalışma zamanı maliyetini yalıtır, dilleri aynı iş şekli üzerinde karşılaştırılabilir kılar. Üretim trafiğini modellemezler: soğuk başlangıç, büyük yükler, dağıtık istemciler, p99 kuyruk, sürekli yük altında GC baskısı.