- Ruby’nin varsayılan
jsongem’i, profillemeyle darboğazları ortadan kaldırarak hız nedeniyleoj’ye geçme yönündeki pratik baskıyı azaltacak şekilde iyileştirildi - Amaç,
oj’yi her durumda geçmek değil;Oj.mimic_JSONveOj.optimize_railsgibi monkey patching olmadan da yeterince hızlı ve öngörülebilir JSON işleme sunmak ojbazı benchmark’larda hızlıydı; ancakscript_safeseçeneğini yok sayması, Rails serileştirme farkları ve Ruby çökmeleri gibi nedenlerle operasyonel kararlılık ve API uyumluluğu yükü oluşturdu- Başlıca optimizasyonlar; yinelenen UTF-8 kontrollerinin kaldırılması, yaygın koşulların önce kontrol edilmesi, generator ayar maliyetinin azaltılması, encoding pointer takibinden kaçınılması ve lookup table tabanlı escape kontrolüyle yapıldı
twitter.json467KiB üretim benchmark’ında değişikliklere göre %3, %8, %15, %30 iyileşme görüldü; küçük Hash üretimi ise yalnızca ayar maliyetinin azaltılmasıyla 1,51 kat hızlandı
json gem’ini hızlandırmanın arka planı
- Kısa süre önce
jsongem’inin maintainer’ı olduktan sonra eski hataları düzeltirken performans iyileştirmelerine de odaklanıldı; sonuçta çoğu benchmark’ta Ruby için en hızlı JSON parser ve generator haline geldi - Performans patch’lerinin çoğu özel bir sihirden çok, profillemeyle darboğazları bulup basit israfı azaltmaya yakındı
- Temel motivasyon,
ruby/jsonyeterince hızlanarak kullanıcıların sırf hız yüzünden alternatif gem seçmek zorunda kalmadığı bir duruma ulaşmaktı
oj yerine kullanımın getirdiği yük
json 2.7.2ileojarasındaki fark, gerçek boyuta yakın bazı benchmark’larda büyük değildi- 100 tweet’ten oluşan 467KiB JSON belgesini parse etmede
json 2.7.21.9ms,ojise1.6mssürdü - Aynı belgeyi üretmede
json 2.7.20.8ms,ojise0.4mssürdü
- 100 tweet’ten oluşan 467KiB JSON belgesini parse etmede
- Birçok kullanım senaryosunda yavaş olan kısım JSON serileştirmenin kendisi değil, Active Record modellerini Ruby Hash ve Array’lere dönüştüren üst katmandır
oj, Shopify kod tabanı dahil birçok projede kullanılıyordu ve popülerliğinin nedeni büyük olasılıkla hızdı-
Monkey patching’in API uyumsuzluğu
Oj.mimic_JSONsıklıklajsongem’ini,Oj.optimize_railsiseActiveSupport::JSON’ı monkey patch etmek için kullanılırJSON.dump(data, script_safe: true), JSON’u bir<script>etiketi içine güvenli biçimde koymak için</script>ifadesini<\/script>olarak escape edebiliroj,script_safeseçeneğini bilmediği için yok sayar; bu nedenle tek başına güvenli olan bir gem bileOj.mimic_JSONçağırmış bir uygulama içinde XSS saldırısı olasılığı yaratabilirOj.optimize_railsde nesne serileştirmede ince farklar oluşturabilirActiveSupport::JSON::Encoding.time_precision = 0durumundaActiveSupport::JSON.encode(t)saniye düzeyinde bir string üretebilirOj.optimize_railsveOj.mimic_JSONsonrasında milisaniye içeren bir string çıktısı görülen örnekler vardır- Bu örnek, yükleme sırasından kaynaklanan bir corner case’tir; ancak geçmişte bundan daha fazla davranış değişiyordu
-
Üretim ortamı kararlılığı sorunları
- Büyük ölçekli ortamlarda
oj, Ruby çökmelerinin öne çıkan nedenlerinden biriydi vegrpc’den sonra en sorunlu olanlardandı - Native gem yazmak, Ruby VM’i ve özellikle GC’yi anlamayı gerektirir; aksi halde çökme veya bellek bozulması oluşabilir
ojkod tabanında güvenmeyi zorlaştıran hack’ler vardı ve bir dönem hataları atlatmak için bazı durumlarda GC’yi devre dışı bırakıyordu- GC yeniden etkinleştirildiğinde major GC cycle tetiklenebilir
- Bu tür kodlar mikrobenchmark’lar için avantajlı olsa da gerçek production performansını düşürebilir
- Bu deneyim nedeniyle Shopify’ın monolith’inden
Ojkaldırıldı; bu süreçteOj.mimic_JSONile gerçekjsonarasındaki ince farklar doğrulandı
- Büyük ölçekli ortamlarda
Benchmark ve profillemeyle darboğazları bulmak
- Amaç,
ruby/json’ın hem gerçek kullanımda hem mikrobenchmark’lardaoj’ye benzer davranması ve hız nedeniyleOj.mimic_JSONkullanma cazibesini azaltmasıydı - İlk adım bir benchmark suite oluşturmaktı
- Mikrobenchmark’lar ile daha gerçekçi benchmark’ları birlikte içeriyordu
- John Hawthorn’un rapidjson-ruby gem’inde bulunan benchmark suite temel alındı ve bazı eklemeler yapıldı
- C profiler olarak samply kullanıldı
- Firefox Profiler uyumlu raporlar üretmesi, paylaşımı kolaylaştıran bir avantajdır
Yinelenen UTF-8 kontrolünü kaldırmak
twitter.jsonpayload’u ileJSON.dumpprofillendiğinde zamanın9%’u JSON’un kendiisLegalUTF8fonksiyonunda,1.9%’urb_enc_str_asciionly_piçinde harcanıyordu- Ruby String,
coderangeadlı dahili bir özelliğe sahiptir; string’in encoding durumunu veya yalnızca ASCII olup olmadığını bir kez taradıktan sonra cache’lerENC_CODERANGE_UNKNOWN: Henüz taranmamışENC_CODERANGE_VALID: Encoding geçerliENC_CODERANGE_7BIT: Encoding geçerli ve yalnızca ASCII karakterler varENC_CODERANGE_INVALID: Encoding geçerli değil
- Eski
convert_UTF8_to_JSON_ASCII, baştarb_enc_str_asciionly_pçağırıyor, ardından UTF-8 geçerliliğini doğrulamak için string’i yeniden manuel tarayarak yinelenen iş yapıyordu - Değişiklikten sonra UTF-8 geçerliliği, zaten hesaplanmış
coderangekarşılaştırılarak belirleniyor- Ruby ifadesiyle yapı şu şekilde:
string.ascii_only?değilsestring.encoding != Encoding::UTF_8veya!string.valid_encoding?olduğundaJSON::GeneratorErrorraise edilir - Hem
#ascii_only?hem de#valid_encoding?cache’lenmişcoderangekullandığı için string taraması en fazla bir kez yapılır
- Ruby ifadesiyle yapı şu şekilde:
- Beklenen %9’un aksine gerçek iyileşme yaklaşık %3 düzeyinde kaldı
isLegalUTF8içinde harcanan zamanın önemli bir bölümüconvert_UTF8_to_JSON’a taşındı- Nedeni kesin değil; ancak %9’un büyük kısmı string byte’larını RAM’den CPU cache’e getirme maliyeti olabilir
twitter.jsonüretim benchmark’ı1077.3 i/sdeğerinden1113.3 i/sdeğerine çıkarak1.03xhızlandı
Daha ucuz ve daha olası koşulları önce kontrol etmek
fbuffer_inc_capa, toplam çalışma süresinin5.7%’si olarak görünüyordu ve zamanın çoğu buffer’ın zaten allocate edilip edilmediğini kontrol etmeye gidiyordu- Bu fonksiyon buffer’a bir şey yazılan her sefer çağrılır; ancak ilk çağrıdan sonra buffer her zaman zaten allocate edilmiş olur
- Eski yapı, neredeyse hiç tutmayan koşulu önce kontrol ettiği için israf yaratıyordu; buffer henüz allocate edilmemişse
fb->capa0olduğundanrequired > fb->capakontrolüyle de kısmen yineleniyordu - Düzeltmeden sonra en yaygın durum olan “buffer kapasitesi yeterli” önce kontrol ediliyor;
RB_LIKELYveRB_UNLIKELYile CPU branch prediction’a ipucu veriliyor - Fonksiyon
inlineolarak işaretlendiği için çağrı maliyeti azaldı ve çoğu durumda gereken iş çıkarma ve karşılaştırma seviyesine indi - Bu değişiklik,
twitter.jsonüretim benchmark’ını1068.6 i/sdeğerinden1224.7 i/sdeğerine çıkararak 1,15 kat hızlandırdı - Aynı ilke Ruby koduna da uygulanabilir: en ucuz ve gerçekleşme olasılığı en yüksek koşulu önce kontrol etmek
JSON generator’ın ayar maliyetini azaltmak
- Ruby committer’ı Yusuke Endoh, namıdiğer Mame,
ruby/jsonoptimizasyonuna da katıldı ve çeşitli optimizasyonlar içeren eski bir PR vardı - Değişikliklerin çoğu, JSON üretimi öncesinde gereken ayar maliyetini azaltmaya odaklandı
- Argüman parse etme
- generator ve ilgili struct’ları allocate etme
- Üretim işine girmeden önceki hazırlıklar
ruby/json’ın bu ayar maliyeti alternatif implementasyonlardan yüksek olduğundan mikrobenchmark’larda kötü görünüyorduJSON.generate, pretty JSON üretimi içinarray_nl,object_nl,indent,spacegibi seçenekler alabilir- Eskiden verilen string’lerle ayraç buffer’ları önceden hesaplanıyordu
- Örn:
",#{opts[:array_nl]}",",#{opts[:object_nl]}",":#{opts[:space]}" - Amaç uzun bir parçayı tek seferde append etmekti; ancak pratikte tasarruf edilen iş küçüktü
- Çoğu durumda bu seçenekler kullanılmadığı için ön hesaplama maliyeti daha baskın hale geliyordu
- Örn:
- Mame bu optimizasyonu fiilen geri alarak ayar maliyetini büyük ölçüde azalttı
- Büyük benchmark’larda fark çok büyük değil
- Küçük Hash 65 bytes üretim benchmark’ı
2,112,189.3 i/sdeğerinden3,199,311.0 i/sdeğerine çıkarak 1,51 kat hızlandı
Pointer takibinden kaçınıp encoding index’i karşılaştırmak
- Mame’in bir diğer optimizasyonu, bir
rb_enc_getçağrısını kaldırmaktı - JSON, string’in UTF-8 uyumlu olup olmadığını sık sık kontrol etmek zorundaydı; eski yöntemde
rb_enc_get(obj)ilerb_encoding *alınıp US-ASCII veya UTF-8 olup olmadığı karşılaştırılıyordu rb_enc_get, savunmacı, yüksek seviye bir API olduğu için birçok tip kontrolü yapar- String, Symbol, Regexp, File, Data gibi çeşitli nesnelerle çalışabilecek şekilde tasarlanmıştır
- Çok sayıda koşul içerir; CPU branch prediction yanılırsa maliyet artabilir
- Ruby String kavramsal olarak bir encoding referansı taşır; ancak gerçekte 64-bit pointer yerine her String’in iç bitmap’inde daha küçük bir 7-bit encoding index saklanır
- Tüm encoding nesnesi pointer’ını almak için VM’in dahili global dizisi üzerinden gerçek encoding’i bulmak gerekir; bu da düşük seviye kodda pointer takibi anlamına gelir
- Zaten CPU cache’teyse hızlıdır; ancak RAM’den getirilmesi gerekiyorsa CPU beklemek zorunda kalır
json, hedefin zaten String olduğunu biliyor ve gereken bilgi yalnızca ASCII veya UTF-8 olup olmadığı olduğu içinRB_ENCODING_GETile encoding index’i doğrudan karşılaştırabilir- Bu değişiklik,
twitter.jsonüretim benchmark’ını1159.6 i/sdeğerinden1253.3 i/sdeğerine yükselterek 1,08 kat hızlandırdı
Lookup table ile string escape işlemini hızlandırmak
- JSON string dump işlemi, her karakterin olduğu gibi kopyalanıp kopyalanamayacağını veya escape gerektirip gerektirmediğini kontrol etmek zorunda olduğu için maliyetlidir
- Naif yöntem her karakter için birkaç koşulu kontrol eder
- ASCII control character olup olmadığını kontrol etme
\n,\r,\t,\f,\bolup olmadığını kontrol etme"veya\olup olmadığını kontrol etme
- Lookup table yöntemi, bu kararı statik bir dizide önceden hesaplar ve her karakter için birden fazla karşılaştırma yapmak yerine dinamik offset’ten boolean okur
- Biraz daha fazla statik bellek kullanma karşılığında loop çok daha hızlı hale gelir
- Çoğu string’de escape gerektiren karakter olmadığı varsayımıyla Mame önce fast path olup olmadığını ucuzca kontrol eden, uygunsa tüm string’i tek seferde buffer’a kopyalayan bir ön koşul ekledi
- Mame’in patch’i C kodu olduğu için daha karmaşıktır; ancak aynı pattern’i kullanır
- Yalnızca bu değişiklikle
twitter.jsonüretim benchmark’ı1258.1 i/sdeğerinden1630.2 i/sdeğerine çıkarak 1,30 kat hızlandı
Devam eden optimizasyonlar
- Ele alınacak başka optimizasyonlar da kaldığından devam yazısının geleceği duyuruldu
- Daha sonra part two yayımlandı
1 yorum
Hacker News yorumları
byroot’un çalışmalarını gerçekten seviyorum. Yalnızca katkılarının türü değil, üretkenliğinin ölçeği de her zaman şaşırtıcı derecede etkileyici
Ruby core tarafına birkaç kez girmeye çalıştım ama kendi seviyeme uygun ve olumlu katkı yapabileceğim bir iş bulamadım; birkaç hafta sonuç alamayınca da motivasyonum kayboldu. Yazıda paylaşılan türden bir bağlam edinmek gerçekten çok zor
Ruby C tarafında çalışan insanlar daha sık yazsa, Ruby’yi daha da geliştirmek için gereken yetkinliklere sahip kişi sayısı artar gibi geliyor. C profiler tavsiyeleri de iyiydi; Ruby gem’lerinden C kodu içeren birini alıp optimizasyonla yeniden uğraşmaya başlamayı bile düşünebilirim
C extension üzerine olsa da bazı kavramları anlamaya yardımcı oluyor
Burada anılması gereken bir şey de Rails’in varsayılan kullanım biçimi olan jbuilder. jbuilder doğrudan JSON serialization kısmı olmasa da, Ruby/Rails’te JSON render etmeyi yavaşlatan şeyler listesi yapsam en üste koyarım
jbuilder ile çok sayıda partial render ederseniz gerçekten çok yavaşlıyor
Bu konudaki yazıların takibi kolay ve kendi Ruby kodumu da benchmark edip optimize etme isteği uyandırıyor. Hem yazı hem de yapılan iş çok iyiydi
Gözden kaçırmış olabilirim ama tüm optimizasyonlar uygulandıktan sonra yeni sürümün Twitter JSON dump’ını parse/encode etmesinin ne kadar sürdüğünü gösteren bir yer var mı?
https://github.com/ruby/json/releases/tag/v2.7.3
https://github.com/ruby/json/releases/tag/v2.8.0
Harika bir yazı ve yapılan iş de çok iyi. Bundan sonra Oj kullanmak için hâlâ bir sebep var mı?
Oj’nin, temel
jsongem’inin taklit etmeye niyetli olmadığı çok büyük bir API’si var. Örneğin “SAJ” (SAX tarzı parsing), birden fazla escape yöntemi gibi şeylerBenim hedefim yalnızca kullanım senaryolarının kabaca %95’inde Oj’ye ihtiyaç bırakmamak; o yüzden pek çok kullanım için Oj hâlâ faydalı olacaktır
Bu yazı yayımlandığından beri, şu anda bakımsız olan bu implementasyona kıyasla ne kadar daha hızlı hale geldiğini merak ediyorum
https://netflixtechblog.com/fast-json-api-serialization-with...
Bu saf Ruby implementasyonunun epey temiz olduğunu düşünmüştüm ama gerçek prodüksiyonda hiç kullanmadım. Uzun zamandır terk edilmiş durumda
Genel olarak saf Ruby implementasyonlarının mevcut durumunu da merak ediyorum.
json_purekaldırılmış gibi görünüyor; öyleyse üzücü. Bunun arka planını bilen var mı? Yazının en ilginç kısmı benim için C optimizasyonundan çok Ruby optimizasyonu tarafıKeyifle okudum. Yalnız Ruby’ye özgü olmayan optimizasyonlarda, örneğin escape karakterleri için lookup table gibi şeylerde, bunu zaten yapan simdjson gibi mevcut kütüphanelerden neden yararlanılmadığını merak ediyorum
Kısacası
ruby/json, Ruby ile birlikte dağıtıldığı için Ruby’nin kısıtlarıyla uyumlu olmak zorunda; şu an bu da saf C99 ve C++ yok anlamına geliyor. simdjson’un Apache 2 lisansı da sorun olabilir ama emin değilimGenel olarak dragonbox gibi harika C++ kütüphanelerini kullanmayı isterdim ama yapamıyorum
Son olarak kontrol ettiğimde simdjson yalnızca parser sağlıyordu. ruby/json gem’i ise hem parsing hem encoding yapıyor; dolayısıyla problem alanının sadece yarısına yardımcı oluyor
Modern projelerde bu tür işler yeterince yapılmıyor. Bu tür işler daha düzenli yapılsaydı, baştan simdjson ya da oj gibi kütüphanelere ihtiyaç olur muydu emin değilim. Bu problem alanı o kadar da zor değil
Ruby JSON intrinsics kullanıyor mu? Kullanabilir mi?
Bir de çeşitli JIT’lerle nasıl etkileşiyor?
jsongem’i C ile yazıldığı için YJIT, yani referans implementasyonun JIT’i açısından bir kara kutuTruffleRuby JIT, eskiden Sulong ile C extension’ları yorumlayıp dil sınırlarını aşacak şekilde JIT yapabiliyordu ama bildiğim kadarıyla çeşitli uyumluluk sorunları yüzünden yakın zamanda bundan vazgeçti
Ayrıca TruffleRuby’de JSON parser C ile implement edilmiş olsa da encoder saf Ruby: https://github.com/ruby/json/blob/e1f6456499d497f33f69ae4c1a...
Yanlış hatırlamıyorsam branch prediction hint modern CPU’larda işe yaramıyor
“Redwood Cove mikro mimarisinden itibaren, predictor’ın belirli bir branch hakkında kayıtlı bilgisi yoksa ve o branch’te Intel SSE2 branch taken hint, yani 3EH instruction prefix’i varsa, codec branch’i decode ederken branch prediction’ı not-taken’dan taken’a çevirir. Ardından ön taraftaki pipeline’ı flush eder ve pipeline’ı taken yolunu izlemeye yönlendirir
...
Bu hint yalnızca predictor’da o branch için kayıtlı bilgi olmadığında kullanılır. Kod şişmesini ve instruction fetch bant genişliği kaybını önlemek için, çok sayıda iterasyona sahip loop’ların içindeki branch’ler gibi hot code branch’lerine hint eklenmemelidir. Çünkü predictor büyük olasılıkla o branch hakkında zaten bilgi tutuyordur. İdeal olarak hint’ler seyrek çalışan ama çoğunlukla taken olan branch’lere eklenmelidir, ancak bu tür branch’leri tespit etmek zor olabilir. Compiler’ın bir execution path’i fall-through olarak yerleştiremediği durumlarda, profile-guided optimization’ın parçası olarak hint eklemesi önerilir. Redwood Cove mikro mimarisi, hint yerleşimini yönlendirmek için yeni performance monitoring event’leri sunuyor”