← RIZGAR OZAN

MSVC'de çalışıyordu: üç derleyiciden yalnız ikisinde görünen senkron hatası

Rızgar Ozan · 2026-10-12 · English version

lockstep-tactics küçük bir C++20 taktik simülasyonu ve tek bir sözü var: aynı girdiler her derleyicide, her tick'te aynı durum hash'ini verir. Lockstep çok oyunculu oyunlar da tekrar kayıtları da bu sözün üstünde durur. Söylemesi kolay, kontrol etmesi zor. O yüzden repo bunu her push'ta kontrol ediyor: bin tohumlu maç Windows'ta MSVC ile, Linux'ta WebAssembly ile ayrı ayrı oynanıyor ve iki hash listesi karşılaştırılıyor.

Kontrolün birkaç yüz kez geçmesi bana hiç başarısız olup olamayacağını söylemiyordu. Hiçbir yerde kırılmayan kural, batıl inanç gibi görünür. Ben de simülasyonu bilerek iki kez yanlış derleyip saydım.

Birinci kuralı kırmak: birimler unordered_map sırasıyla hareket etsin

Simülasyon birimleri kimliğe göre sıralı bir std::vector'de tutuyor ve o sırayla hareket ettiriyor. Bozuk derleme (-DLOCKSTEP_RISKY=unordered) onları bir std::unordered_map'e koyup onun üstünde dolaşıyor. Sıra, isabet zarı için hangi birimin hangi rastgele sayıyı alacağına karar verdiği için farklı sıra farklı maç demek.

600 tick'lik 1.000 maçta, normal derlemeden farklı biten maç sayısı:

Derleyicifarklı biten maç
MSVC 19.50 x640
gcc 16 (libstdc++)881
Emscripten 6 / WebAssembly (libc++)860

gcc ile WebAssembly birbirinden de 249 maçta ayrışıyor.

İlginç sayı sıfır. MSVC'nin unordered_map'i elemanları ekleme sırasında bir listede tutuyor; altı küçük anahtar ve yeniden hash'leme olmayınca dolaşma tesadüfen kimlik sırasında çıkıyor, yani tam olarak güvenli sıra. Hata, Windows geliştiricilerinin çoğunun test ettiği derleyicide görünmez. Bütün birim testleri geçer. Bütün yerel tekrarlar tutar. Yayınla; ilk Linux ya da tarayıcı istemcisi maçların %86'sında ayrışır.

Derleyiciler arası kontrolün, koşular arası kontrolden farkı bu: "bende çalışıyor" burada kelimesi kelimesine doğruydu.

İkinci kuralı kırmak: hareket float ile

Simülasyon her yerde Q16.16 sabit nokta kullanıyor; reponun 01 numaralı notu derleyicilerin float işlemlerini birleştirebileceğini ya da sırasını değiştirebileceğini söylüyor. İkinci bozuk derleme (-DLOCKSTEP_RISKY=float) birimleri hedefe giden doğru boyunca float, karekök ve bölmeyle yürütüp sonra sabit noktaya geri çeviriyor.

farklı biten maç
MSVC, gcc, WebAssembly, herhangi iki çift0 / 1000

Bunun kırılmasını bekliyordum, kırılmadı. Üç hedef de aynı yuvarlamayla IEEE skaler float kullanıyor, sqrt doğru yuvarlanıyor ve ISO C++ kipinde hiçbiri a*b + c'yi tek bir fused multiply-add'e birleştirmiyor. Bu üç derleyicide, bugün, float deterministikti.

Sonra tek bir bayrak:

farklı biten maç
gcc -mfma -ffp-contract=fast vs düz gcc, float derlemesi12 / 1000
aynı bayraklar, sabit nokta derlemesi0 / 1000

Birleştirmeye izin verilince nesne dosyasına üç FMA komutu giriyor (bayraksız sıfır) ve on iki maç farklı bitiyor. Aynı bayraklarla derlenen sabit nokta sürümü hiç kıpırdamıyor: bayrağın dokunacağı bir float işlemi yok. Yani "float yok" kuralı bu derleyiciler için değil; henüz eklenmemiş derleme bayrağı, derleyici güncellemesi ya da ARM hedefi için var, ve sabit nokta o günleri olaysız geçiren şey.

Çıkardıklarım

Yukarıdakilerin hepsi repodan tekrar üretilebilir: cmake -S . -B build-risky -DLOCKSTEP_RISKY=unordered (ya da float), replay 1000 600, normal derlemeyle karşılaştır. Her kuralın gerekçesi docs/notes/ altında; 06 numaralı not bu yazının uzun hâli. Maçı tarayıcıda oynayabilirsin.