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.
- Ölçüm tarihi
- Yayın
5 gün önce ölçüldü
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
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 |
İ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 |
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
| Seri | P=4 | P=8 |
|---|---|---|
| Mutex | 7,69 M | 15,96 M |
| Tek yönlü kanal | 7,94 M | 6,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 |
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 |
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
| Seri | %50 okuma | %90 okuma | %99 okuma |
|---|---|---|---|
| Mutex | 9,86 M | 12,23 M | 13,12 M |
| Sahip-goroutine kanalı | 4,42 M | 4,58 M | 4,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 |
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ı
- 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.
- 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ı.
- P=2 gürültülü. Nedenini doğrulayamadım.
- 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. - 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 nssaat çözünürlüğünün 4 katı olan tabandır.starving_fractionalanını örnek sayısı küçük olduğu için kullanmadım. - İstek kanalı tamponsuz. Tamponlu istek kanalı kanal modellerinin sonucunu değiştirebilir.
- G=1’de iki goroutine. Sahip-goroutine modelleri G=1’de 2 goroutine koşuyor.
- Kapsam dışı.
sync.Maphiç ö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
Go'da eşzamanlılık: goroutine ve channel pratiği
Go'nun eşzamanlılık modelini — goroutine ve channel'ı — uygulamalı kavramak. PHP'nin süreç modelinden gelince ne değişiyor?
18.750 istek/sn: kesin, tekrarlanmış ve üç kat yanlış
Aynı koşunun iki fazı aynı servisi üç kat farklı ölçtü. Yanlış olanı yakalayan şey daha iyi bir istatistik değil, aynı şeyi ölçen ikinci bir yöntemdi.