İçeriğe geç
Muhammet Şafak
en

64 goroutine, dört çekirdekte Mutex ile kanal aynı hızda, ama Mutex'in p99'u beş kat yüksek

Aynı paylaşılan sayaca G goroutine ve P çekirdekle saldırıldığında sync.Mutex, sahip-goroutine kanalı ve tamponlu kanal saniyede kaç işlem yapıyor, beklemenin kuyruk gecikmesi ve adilliği nasıl değişiyor?

Bulgu

64 goroutine ve dört çekirdekte sync.Mutex ile tek yönlü kanal aynı hızda (7,69 ve 7,94 milyon işlem/sn) ama Mutex'in p99 beklemesi 5 kat yüksek. Sekiz çekirdekte Mutex 2,4 ve 4,0 kat hızlı (15,96 milyon işlem/sn; kanallar 6,65 ve 3,97 milyon); oranlar bayraksız hücrelerden.

G=64 P=4 · p99 bekleme, Mutex → tek yönlü kanal
491.519 → 98.303 ns
G=64 P=8 · Mutex
15,96 milyon işlem/sn
G=64 P=8 · tek yönlü kanal
6,65 milyon işlem/sn
Kalite bayrağı taşıyan hücre
518 / 845

Yöntem

go-sync-bench ile üç desen ölçüldü: S1 tek sayaç (sync.Mutex, sahip-goroutine ile tek yönlü kanal, istek-cevaplı kanal, atomic), S2 okuma oranı (Mutex, RWMutex, sahip-goroutine kanalı), S3 üretici-tüketici kuyruğu (kanal tamponları 0, 1, 64, 1024 ve sync.Cond'lu kuyruk). Her hücre, G goroutine ve P çekirdek grubu (cpuset ile 1, 2, 4 ve 8) için 5 round koştu; tekrar başına 0,5 sn ısınma, 1,5 sn throughput ve 2,0 sn gecikme penceresi. 845 hücre, 4.225 geçerli tekrar. Her round × çekirdek grubu container'ından sonra 60 sn soğuma, ağsız ve read-only bir Docker konteyneri, 4 GiB bellek. Hücre medyanları 5 tekrarın hepsinden alındı, hiçbir tekrar atılmadı. Bayrak eşikleri tam aralık üzerinden throughput için %10, p99 için %25, saat kayması için %5. Yeniden koşu adayı seçilirken en uçtaki tekrar atılıp kalan aralığa bakıldı (throughput %10, p99 %50); ilk tasarımın tetikleyicisi 294 aday seçince 84 hücrelik bütçe aşıldığı için bu kural kullanıldı ve 88 hücre yeniden koşuldu (2026-09-30 01:38-02:31 UTC). 845 hücrenin 518'i bayraklı olduğu için confidence low. Gecikme yüzdelikleri log kovalarının üst sınırıdır ve 1/16 örneklenmiştir; taban, saat çözünürlüğünün 4 katı olan 164 ns.

Düşük güven Tek çalıştırma ya da kontrolsüz ortam — yön gösterir, kesin sayı değildir.
Ölçüm tarihi

5 gün önce ölçüldü

Yayın

Ortam

Go
1.27.1 · linux/arm64
Sanallaştırma
Docker Engine 29.8.0 · 12 vCPU · aarch64
Donanım
Apple M4 Pro · 12 CPU
Çekirdek grupları
P1 = 4 · P2 = 4-5 · P4 = 4-7 · P8 = 4-11 (cpuset)
Konteyner
ağsız · read-only · 4 GiB bellek
Image
ilk geçiş sha256:640a046f219a… · yeniden koşu sha256:eabc1573265a…
Protokol
5 round · 845 hücre · 0,5 sn ısınma + 1,5 sn throughput + 2,0 sn gecikme · 60 sn soğuma (container başına)
Seed
932269622

Teknolojiler

Go Docker

Tekrarlamak için

./bench/verify.sh && STAMP=$(date -u +%F) ./bench/run.sh --all

Dört çekirdekte 64 goroutine’i aynı sayaca saldığımda sync.Mutex ile kanal saniyede aynı sayıda işlem yaptı: 7,69 ve 7,94 milyon. Aynı hücrede p99 bekleme 491.519 ns’ye karşı 98.303 ns’ydi. “Hangisi daha hızlı” sorusunun tek bir cevabı olmadığını bu ikilide gördüm; bu kayıt o cevabın neye bağlı olduğunu ölçtüğüm kadarıyla yazıyor. Kodun ve ham koşuların hepsi go-sync-bench deposunda.

Goroutine ve kanal tarafının pratik anlatımı için Go’da eşzamanlılık: goroutine ve channel pratiği yazısına bakabilirsiniz. Burada yalnız ölçüm var.

Ne ölçtüm

Ana desen (S1) şu: G goroutine, tek bir paylaşılan sayacı artırıyor. Sayaca erişimin dört yolu var.

Model Erişim biçimi
Mutex sync.Mutex ile korunan sayaç
Tek yönlü kanal Sayacı tek bir sahip-goroutine tutuyor, diğerleri kanala istek yolluyor, cevap beklemiyor
İstek-cevaplı kanal Aynı sahip-goroutine, ama her istek cevap kanalı taşıyor ve cevabı bekliyor
atomic atomic artırma; yalnız bağlam için, başlık iddiasına girmiyor
İstek kanalı tamponsuz. Tamponlu bir istek kanalı sonucu değiştirebilir; bunu ölçmedim.

İki desen daha koştu: S2’de okuma oranı değişiyor (yüzde 50, 90 ve 99 okuma), S3’te bir üretici-tüketici kuyruğu farklı tamponlarla (kanal 0, 1, 64, 1024 ve sync.Cond’lu kuyruk) deneniyor. Çekirdek sayısı P, konteynere cpuset ile verilen çekirdek grubunun büyüklüğü: 1, 2, 4 ve 8.

Bu kayıtta “işlem/sn” bir throughput penceresinin medyanı, “bekleme” ise gecikme penceresinde bir işlemin sayaca ulaşması için geçen süre. Gecikme yüzdelikleri log kovalarının üst sınırı olarak raporlanıyor; bu yüzden 491.519 gibi sayılar bir kovanın tavanı, ölçülmüş bir tam değer değil.

64 goroutine, dört çekirdek: aynı hız, farklı kuyruk

Model İşlem/sn p50 p99 p99.9
Mutex 7.688.545 639 ns 491.519 ns 1.441.791 ns
Tek yönlü kanal 7.944.446 175 ns 98.303 ns 114.687 ns
S1, G=64, P=4, iş yükü W=0. İki hücre de bayraksız. p99 oranı 491.519 / 98.303 = 5,00.

Throughput farkı yüzde 3,3 ve kanalın lehine, yani gürültü içinde. Bekleme farkı gürültü değil: p50’de 639 ile 175, p99’da 491.519 ile 98.303, p99.9’da 1.441.791 ile 114.687 ns. Mutex aynı toplamı daha geniş bir dağılımla üretiyor. Ortalamaya bakan bir benchmark bu iki modeli eşit görürdü.

Neden böyle olduğunu ölçmedim. Mutex’in bekleyen goroutine’leri nasıl uyandırdığına dair bir açıklama kurabilirim, ama bu deneyin ürettiği bir kanıt olmazdı; yazıya koymuyorum.

Sekiz çekirdekte tablo döndü

G=64: dört ve sekiz çekirdekte throughput

Mutex sekiz çekirdekte iki katına yakın çıkarken tek yönlü kanal geriledi. İstek-cevaplı kanalın P=4 hücresi kalite bayrağı taşıdığı için grafiğe girmedi; P=8 değeri aşağıdaki tabloda.

  • Mutex
  • Tek yönlü kanal

M Kaynak: go-sync-bench, results/2026-09-29/merged/summary.json, S1 hücreleri

Veri tablosu
go-sync-bench, results/2026-09-29/merged/summary.json, S1 hücreleri
Seri P=4P=8
Mutex 7,69 M15,96 M
Tek yönlü kanal 7,94 M6,65 M

Grafik tarayıcıda çizilir; aşağıdaki tablo aynı veriyi taşır.

Model İşlem/sn (P=8) Mutex oranı p99
Mutex 15.962.148 1,00 114.687 ns
Tek yönlü kanal 6.648.606 2,40 122.879 ns
İstek-cevaplı kanal 3.970.457 4,02 131.071 ns
S1, G=64, P=8. Üç hücre de bayraksız. Oran sütunu Mutex'in ilgili modele karşı katı: 15.962.148 / 6.648.606 = 2,40 ve 15.962.148 / 3.970.457 = 4,02.

Mutex P=4’ten P=8’e 2,08 kat hızlandı. Tek yönlü kanal 0,84 katına indi: sayacı tek bir sahip-goroutine tutuyor; çekirdek eklemenin ona ne kattığını bilmiyorum. Nedenini deney ölçmedi; bir hipotez bile öne sürmüyorum.

P=8’de p99 üç modelde de birbirine yakın: 114.687, 122.879 ve 131.071 ns. Dört çekirdekteki beş katlık fark burada görünmüyor. Hangi modelin kuyruğunun “daha iyi” olduğu sorusunun tek bir cevabı yok; P’ye bağlı.

Kapsam dışı bırakmam gerekenler: P=1’deki Mutex hücresi (63,10 milyon) spread_persistent ve observer_effect bayrağı taşıyor, P=2’deki (13,22 milyon) bayraklı. İkisini de iddiaya almadım. Mutex eğrisinin P’ye göre monoton olup olmadığını da söyleyemiyorum, çünkü P=1 ve P=2 hücreleri bayraklı.

10.000 goroutine: Mutex hızlı, adaletsizliği açıklanamıyor

Model İşlem/sn Jain p99 Bayrak
Mutex 14.878.958 0,8237 27.262.975 ns fairness_unexplained
Tek yönlü kanal 2.052.681 0,9999 5.767.167 ns yok
İstek-cevaplı kanal 1.778.745 1,0000 6.291.455 ns yok
S1, G=10.000, P=8. Mutex/tek yönlü kanal = 7,25; Mutex/istek-cevaplı kanal = 8,36. Jain 1'e yaklaştıkça goroutine'ler eşit pay alıyor.

Mutex burada kanalların 7,25 ve 8,36 katı hızlı. Ama Jain adillik indeksi 0,8237 ve bu değer işaretli: fairness_unexplained. Harness adaletsizliği iki kaynaktan yalnız birine bağlayabiliyor: park davranışına (sync). Kural şu: G ≤ P ise, goroutine başına düşen zaman dilimi sınırı b ≥ 1 ise ya da örneklenen edinimlerin en az yarısı park eşiğini aşıyorsa kaynak sync, değilse unexplained. Bu hücrede G=10.000 > P=8, b = 0,12 ve yavaş edinim payı yalnız 0,0176; üç koşul da tutmadığı için adaletsizlik park davranışına bağlanamadı. Kural Jain değerine bakmıyor. Harness’ın b’den hesapladığı beklenen Jain 0,12 (expected_quantum_jain), ölçülen 0,8237; bu hesabın dayandığı kuantum modelini doğrulamadım, o yüzden zamanlayıcıya atıf yapmıyorum. Mutex’in adaletsizliğini bir nedene yazamıyorum. Tasarım kuralım gereği bu hücreler modeller arası adillik kıyasına girmiyor; tabloda “Mutex daha adaletsiz” demiyorum, “adaletsizliği açıklanamıyor” diyorum.

Aynı tablonun bağlamı: dört çekirdekte Mutex’in Jain değeri G=1.024’te 0,4064, G=10.000’de 0,1892, ikisi de fairness_unexplained. Kanal hücrelerinde G=10.000 ve P=8’deki Jain 0,9999 ve 1; aynı G’de P=4’te tek yönlü kanalın Jain’i 0,7995 (yeniden koşudan), yani kanalların adilliği de P’ye bağlı.

Okuma oranı ve kuyruk tamponu

S2’de Mutex, okuma oranı arttıkça hızlanıyor; sahip-goroutine kanalı neredeyse hiç kıpırdamıyor.

S2, G=64, P=4: okuma oranına göre throughput

Mutex bayraksız. Kanalın yüzde 90 okuma hücresi yeniden koşudan geliyor, yüzde 99 okuma hücresi spread_throughput bayrağı taşıyor. RWMutex hücreleri observer_effect bayraklı, grafikte yok.

  • Mutex
  • Sahip-goroutine kanalı

M Kaynak: go-sync-bench, results/2026-09-29/merged/summary.json, S2 hücreleri

Veri tablosu
go-sync-bench, results/2026-09-29/merged/summary.json, S2 hücreleri
Seri %50 okuma%90 okuma%99 okuma
Mutex 9,86 M12,23 M13,12 M
Sahip-goroutine kanalı 4,42 M4,58 M4,5 M

Grafik tarayıcıda çizilir; aşağıdaki tablo aynı veriyi taşır.

Yüzde 50 okumada Mutex kanalın 2,23 katı, yüzde 90’da 2,67 katı. Yüzde 99 hücresinin oranını bayraklı olduğu için yazmıyorum. Mutex’in yüzde 99’daki hızı yüzde 50’ninkinin 1,33 katı.

S3’te G=64, P=4: tamponsuz kanal 5.414.190, tamponu bir olan kanal 6.912.398, tamponu 1.024 olan sync.Cond’lu kuyruk 4.461.277 işlem/sn. Bu üç hücre bayraksız: tampon bire çıkınca kanal 1,28 kat, sync.Cond’lu kuyruğa göre 1,55 kat hızlı. 64 ve 1.024 tamponlu kanal ile 64 tamponlu sync.Cond hücreleri bayraklı; 18,72 ve 21,98 milyon gibi sayılar yalnız yön gösterir, oran çıkarmadım.

Sonuç

Nasıl ölçtüm

Deney 845 hücre koştu; her hücre 5 round, her round tekrar başına 0,5 sn ısınma, 1,5 sn throughput ve 2,0 sn gecikme penceresi. Toplam 4.225 geçerli tekrar. Tekrarların medyan süresi 4,38 sn; sekiz tekrar bütçeyi aştı (örneğin s1-atomic-g10000-p4-w0 55 sn). Her round × çekirdek grubu container’ından sonra 60 sn soğuma var; container içindeki hücreler arasında bekleme yok.

Konteyner ağsız ve read-only, 4 GiB belleğe sahip. İlk geçişte uyku kapısı yoktu; ana makine bir kez uyudu: 11:45-11:59 UTC aralığında s1-chan_oneway-g64-p1-w0 hücresinin 3. round’u bundan etkilendi ve bu hücre iddiaya girmiyor. Yeniden koşudan önce run.sh’a sleep 0 şartı eklendi.

Sonuçlar iki geçişten geliyor. İlk geçiş sha256:640a046f219a… imajıyla 2026-09-29T08:34:54Z’de başladı. İlk tasarımın yeniden koşu tetikleyicisi 294 aday seçti (290’ı sapma kapısına, 11’i ikinci bir kapıya takılmıştı; bir hücre ikisine de takılabilir). Bu, 84 hücrelik bütçeyi aştı. Tetikleyici kırpılmış aralığa çevrildi: en uçtaki tekrar atılıp kalan aralığa bakılıyor (throughput için %10, p99 için %50) ve bu yalnız hangi hücrenin yeniden koşulacağını seçiyor. Bütçe 84’ten 88’e çıkarıldı; 88 hücre sha256:eabc1573265a… imajıyla 2026-09-30 01:38-02:31 UTC’de yeniden koşuldu. Birleşik tabloda kaynak rerun görünüyor; 36’sı yeniden koşudan sonra da eşik üstünde kaldı. Hücre medyanı yine 5 tekrarın hepsinden alınıyor. Bayrak ise ayrı bir ölçüt: tam aralık üzerinden throughput için %10, p99 için %25, saat kayması için %5.

Kalite bayrakları:

Bayrak Hücre sayısı Anlamı
observer_effect 171 Gecikme penceresindeki işlem hızı, throughput penceresindekinin 0,9 katının altında; ölçüm kendini etkiliyor
fairness_unexplained 235 Adaletsizlik park davranışına bağlanamıyor: G > P, b < 1 ve örneklenen edinimlerin yarısından azı park eşiğinin üstünde; Jain değerine bakılmaz
spread_p99 189 Tekrarlar arası p99 sapması 0,25 üstü
spread_throughput 125 Tekrarlar arası throughput sapması 0,10 üstü
calib_drift 72 Saat kalibrasyonu 0,05 üstünde kaydı
spread_persistent 36 Yeniden koşudan sonra da sapma sürüyor
Bir hücre birden çok bayrak taşıyabilir; toplam 518 bayraklı hücre, yani 845'in %61,3'ü. Oran %15'i aştığı için ölçümün önerdiği güven düzeyi düşük (confidence: low).

Gürültü tabanı: P=2 için atomic G=2’de Jain 0,9999, P=4 için G=4’te 0,9998, P=8 için G=4’te 0,9997. Yani ölçüm düzeneği kendi başına adaleti bozmuyor. Kendi kendini sınayan koşunun 40 denemesinin 40’ı geçti. Yayın komutu reproduce alanında; ham veri ve lisans (MIT) depoda.

Sınırlar ve dürüstlük notları

  1. Bayraklı hücrelerin çoğunluğu. 845 hücrenin 518’i bayraklı. Başlıktaki throughput ve p99 oranlarını bu bayrakları taşımayan hücrelerden aldım; G=10.000, P=8 Mutex hücresi yalnız adillik bayrağı taşıyor. Bayraklı hücreler yön göstermek için anıldıysa öyle etiketlendi.
  2. VM içi vCPU. Ölçüm bir Docker sanal makinesinde koştu; P/E çekirdek ayrımı VM’den görünmez ve küme saati meşgul çekirdek sayısına bağlı. P değerleri fiziksel çekirdek değil, cpuset ile verilmiş vCPU grupları.
  3. P=2 gürültülü. Nedenini doğrulayamadım.
  4. Adillik bayrağının kuralı. Bayrak G, P, b ve park payına bakıyor; zamanlayıcı kaynaklı adaletsizliği ayırt edemiyor. Kuantum modelini (expected_quantum_jain) doğrulamadım, açıklamalarda kullanmadım.
  5. Gecikme çözünürlüğü. Gecikme yüzdelikleri log kovalarının üst sınırıdır ve 1/16 oranında örneklenmiştir. < 164 ns saat çözünürlüğünün 4 katı olan tabandır. starving_fraction alanını örnek sayısı küçük olduğu için kullanmadım.
  6. İstek kanalı tamponsuz. Tamponlu istek kanalı kanal modellerinin sonucunu değiştirebilir.
  7. G=1’de iki goroutine. Sahip-goroutine modelleri G=1’de 2 goroutine koşuyor.
  8. Kapsam dışı. sync.Map hiç ölçülmedi; S2’de atomic de ölçülmedi. Üretim iş yükü değil, sentetik bir sayaç ölçüldü.

Kendi sayınızı üretmek için go-sync-bench deposunu kendi donanımınızda koşturun; P’yi kendi hedefinize göre seçin, çünkü bu kaydın en açık sonucu, cevabın P’ye bağlı olduğu.

İlgili yazılar

Diğer Kayıtlar

Tüm kayıtlar

Bir çekirdek Go'da 14.330, PHP-FPM'de 5.152 OAuth2 isteği taşıyor

Her istekte RS256 bearer token doğrulayıp PostgreSQL'e bir satır yazan ya da okuyan aynı API, bir, iki ve dört çekirdekte Go, PHP-FPM ve FrankenPHP worker ile saniyede kaç karma istek taşıyor?

Bulgu

Dört çekirdekte Go 57.321, FrankenPHP 25.659, PHP-FPM 20.606 karma istek taşıdı; çekirdek başına 14.330, 6.415 ve 5.152. Kapasite planına giren sayı bu değil, istek başına uygulama CPU'su: Go 66,8, FrankenPHP 110,7, PHP-FPM 187,7 mikrosaniye. FrankenPHP doyduğunda dört çekirdeğin ancak 2,84'ünü kullanabiliyor — Go 3,83, PHP-FPM 3,87. Darboğaz veritabanı değil: aynı dört çekirdekte PostgreSQL tek başına saniyede 68.212 satır yazıyor, yani en hızlı adayın karma tavanının üstünde.

17 gün önce ölçüldü

Orta güven

Laravel'in preload eğrisi: 123 dosya, 1.912 dosyadan sekiz kat fazla kazandırıyor

Laravel için elle seçilmiş bir preload nereye kadar iner, ve her dilim ne kadar açılış bedeline mal olur?

Bulgu

Eğri hacimle orantılı değil. İlk 1.592 dosya (Laravel çekirdeği) 30 ms kazandırıyor ve açılışa 1,2 saniye ekliyor. Sonraki 1.094 Symfony dosyası 9,5 ms kazandırıyor, bedava. Ondan sonraki **123 dosya** (psr, carbon) 15,7 ms kazandırıyor — kendinden önceki 1.094 dosyadan fazla. Ve son 1.912 dosya yalnız 1,8 ms kazandırıp açılışa 1,2 saniye daha yazıyor. Yani önceki kaydın tavan olarak ölçtüğü "hepsini derle", eğrinin başlangıç noktası dışındaki en kötü fiyat/performans bölgesi: 2.809 dosyada durmak 12,77 ms ve 1.514 ms açılış verirken, 4.721 dosya 10,96 ms için 2.691 ms istiyor.

43 gün önce ölçüldü

Yüksek güven

opcache preload deploy faturasını on dört kata kadar siliyor — ama yedi framework'ün beşi onu size vermiyor

`opcache.preload` açıkken yedi PHP framework'ünün deploy sonrası ilk isteği ne kadar sürüyor, bu kazanç neye mal oluyor, ve kimler ona erişebiliyor?

Bulgu

Preload, soğuk ilk isteği 3,5 ile 14,2 kat arasında kısaltıyor (sınıfları PHP kaynağı olan altı adayda): Symfony 35,58 ms'den 2,50 ms'ye, yani Phalcon'un çıplak seviyesine iniyor. Ama yedi adayın yalnız ikisi (Symfony, CodeIgniter) resmî bir preload dosyası yayınlıyor; kalan beşinde kazanç masada duruyor ve kullanıcının kendi yazmasını bekliyor. Yazmak da göründüğü kadar kolay değil: classmap'ten körlemesine üretilen preload Symfony'yi hiç ayağa kaldırmıyor, CodeIgniter'da ise elle seçilmiş resmî dosyadan (3,13 ms) daha kötü sonuç veriyor (5,29 ms). Ve bedel kaybolmuyor: Laravel'in classmap preload'ı ziyaretçiden aldığı 62 ms'yi php-fpm'in ayağa kalkışına 2.340 ms olarak yazıyor.

43 gün önce ölçüldü

Yüksek güven

Sitede Ara

Yazı, proje ve sayfalarda arama yapmak için yazmaya başlayın.

Esc ile kapat Pagefind ile güçlendirildi