1 Milyar Satır Meydan Okuması
(morling.dev)- 2024 Ocak ayı boyunca düzenlenen One Billion Row Challenge(1BRC), 1 milyar satırlık bir metin dosyasını işleyerek Java'nın ne kadar hızlanabileceğini ölçen bir performans meydan okumasıydı
- Girdi
station;temperaturebiçiminde basit bir metin olsa da, her istasyon için minimum·ortalama·maksimum sıcaklığı hesaplayıp ad sırasına göre doğru biçimde yazdırmak gerekiyor - Uygulamada yalnızca Java'ya izin veriliyor; SDKMan dağıtımları ve openjdk.net Early Access derlemeleri kullanılabiliyor, ancak harici bağımlılıklar yasak
- Katılımcılar GitHub'daki 1brc deposuna pull request ile gönderim yapıyor ve sağlanan temel uygulama ile doğru çıktı biçimini ve performansı karşılaştırabiliyor
- Değerlendirme, aynı Hetzner Cloud CCX33 ortamında 5 çalıştırmanın ardından en düşük ve en yüksek süreler hariç tutularak kalan 3 çalıştırmanın ortalamasıyla liderlik tablosu sıralamasını belirliyor
1 milyar satırı en hızlı toplayan Java görevi
- One Billion Row Challenge, 1 Ocak 2024 ile 31 Ocak 2024 arasında düzenlenen bir Java performans meydan okumasıydı
- Katılımcılar, bir metin dosyasındaki sıcaklık ölçümlerini okuyup her meteoroloji istasyonu için minimum·ortalama·maksimum sıcaklığı hesaplayan bir Java programı yazdı
- Zorluğun temel noktası, girdi dosyasının 1.000.000.000 satır olmasıydı
- Girdi, her satırda bir ölçüm değeri bulunan basit bir yapıya sahipti
- Örn:
Hamburg;12.0 - Örn:
Bulawayo;8.9 - Örn:
Palembang;38.8
- Örn:
- Çıktıda istasyon adları alfabetik sıraya göre sıralanmalı ve her istasyonun
min/mean/maxdeğerleri gösterilmeliydi- Örn:
{Abha=5.0/18.0/27.4, Abidjan=15.7/26.0/34.1, ...}
- Örn:
Gönderim kuralları ve çalışma ortamı
- Amaç, aynı işi yapan en hızlı Java uygulamasını oluşturmaktı
- Optimizasyonda sanal iş parçacıkları, Vector API ve SIMD, GC optimizasyonu, AOT derleme gibi tekniklerden yararlanılabiliyordu
- Temel kurallar şöyleydi
- Gönderimler Java ile yazılmalıydı
- SDKMan tarafından sunulan Java dağıtımları ve openjdk.net'in Early Access derlemeleri kullanılabiliyordu
- Valhalla gibi OpenJDK projelerinin EA derlemelerine de izin veriliyordu
- Harici bağımlılıklar kullanılamıyordu
- Katılımcılar 1brc deposunu klonlayıp README yönergelerine göre uygulamalarını gönderiyordu
- Temel uygulama, karşılaştırma ölçütü ve doğru çıktı biçimini kontrol etmek için sağlanmıştı
- Gönderimler, upstream depoda pull request açılarak yapılıyordu
Liderlik tablosu hesaplama yöntemi ve topluluk paylaşımı
- Değerlendirme Hetzner Cloud CCX33 instance'ı üzerinde yapılıyordu
- Özellikler 8 dedicated vCPU, 32 GB RAM idi
- Uçtan uca çalışma süresi
timeprogramıyla ölçülüyordu - Her gönderim art arda 5 kez çalıştırılıyordu
- En yavaş ve en hızlı çalıştırmalar hariç tutuluyordu
- Kalan 3 çalıştırma süresinin ortalaması ilgili gönderimin sonucu oluyordu
- Sonuçlar leaderboard'a ekleniyordu
- Optimizasyon teknikleriyle ilgili tartışmalar GitHub deposundaki discussion bölümünde sürüyordu
- Java dışındaki dillerle yapılan uygulamaları paylaşmak için Show & Tell bölümü de hazırlanmıştı; Rust, Go, C++ gibi dillerdeki 1BRC uygulamaları paylaşılıyordu
2 yorum
Hacker News yorumları
Şu anda en iyi performansı veriyor gibi görünen çözüm [0], hash çakışmalarını hesaba katmadığı için veri kümesinde yeterince çok farklı şehir varsa yanlış sonuç üretecek gibi görünüyor
Bir şeyi kaçırıp kaçırmadığımı merak ediyorum
[0] https://github.com/gunnarmorling/1brc/blob/main/src/main/jav...
Şimdilik bu girdileri liderlik tablosundan kaldırdık; iki yazar da gönderilerini düzeltiyor, sonrasında yeniden eklenecekler
[0] https://twitter.com/mtopolnik/status/1742652716919251052
Aşağıdaki yaklaşımla tamamının 0,3 saniye içinde işlenebileceğini düşünüyorum
Sıcaklıklar tek ondalık basamaklı olduğundan genel durumda yaklaşık 400 değer yeterli; yer adları da yaklaşık 400 ile sınırlı olduğundan sıcaklık×yer adı kombinasyonları için yaklaşık 160 bin girdilik bir lookup table oluşturulabilir
Bu 160 bin girdiyi, 4 baytlık bir register içinde hangi döndürme konumunda olurlarsa olsunlar hash tablosunda benzersiz bucket'lara eşleyen bir durum makinesi otomatik üretilebilir; 32 bitlik durum register'ında her döngüde durum geçiş tablosu sorgusu ve sonraki 4 baytla XOR yapılır
Tüm veriyi bellek hızında tarayıp duruma göre sayaçları artırmak yeterli; yalnızca 65K durum olduğu için sayaçlar cache'e sığar
AVX512 ile çekirdek başına bu 32 bitlik durum makinelerinden 512 tanesini paralel çalıştırabilirsiniz, yani hesaplamanın darboğaz olmayacağını düşünüyorum
Geçerli bir bucket'a eşlenmeyen yüksek/düşük sıcaklıklar veya bilinmeyen yer adları yavaş koda devredilir; minimum/maksimum işleme de böyle bir escape ile yapılırsa yalnızca birkaç bin kez gerçekleşir
Bu yöntemin tek bir AVX512 çekirdeğiyle bile bellek hızında çalışabileceğini, bu yüzden birden fazla çekirdeğe bölmenin avantajı olmadığını düşünüyorum
Gerekenler 400 öğeli bir hash tablosu, çalışma anındaki minimum·ortalama·maksimum için 3 kayan nokta değeri ve ortalamayı güncellemek için bir sayaç tamsayısı
Adlar için 16 bayt kullansanız bile tamamı 16KB içinde kalır
Çalışma süresini I/O belirler; ardından muhtemelen JSON ayrıştırma gelir
Modern x86 sunucu yongalarının çoğu saat döngüsü başına 2 SIMD load'u retire edebilir; bu yüzden AVX2 ile 1GHz'de yaklaşık 32GB/s mümkündür, çekirdek başına bant genişliğini maksimize etmek için AVX-512 şart değildir
Ama DRAM'den okuyorsanız çok daha erken, tipik sunucularda muhtemelen 10–16GB/s civarında takılırsınız
Verinin çoğu RAM'e taştığı sürece tek çekirdek throughput'u ciddi biçimde düşer; büyük streaming işlerinde çok çekirdekli paralellik neredeyse her zaman kazanç sağlar
L3 cache'ten çok daha büyük bir bellek bloğu ayırıp, sayfa hatalarını önceden tetikledikten sonra, sıkı bir döngüde açılmış vektör load'ları (AVX2/AVX-512) yaparsanız bunu kolayca görebilirsiniz
Ayrıca durum register'ını nasıl yorumlayacağınız da soru işareti. Girişteki 4 baytla XOR yapılırsa beklenmeyen yer adlarında fiilen 4,7 milyar olası değerden herhangi biri olabilir
Beklenen bir yer adı olsa bile 4 bayttan uzunsa, ortak öneke sahip başka adlardan ayırt etmek için her biri için birden çok durum gerekmez mi diye düşünüyorum
Kurallar, veri üreteci sabit bir gözlem istasyonu adı kümesi kullansa bile her çözümün keyfi UTF-8 gözlem istasyonu adlarında çalışması gerektiğini söylüyor
En yavaş çalıştırmayı ve en hızlı çalıştırmayı atıp kalan üç çalıştırmanın ortalamasını almak yerine, en yavaş iki çalıştırmayı atmak ya da sadece en hızlı değeri kabul etmek bence daha iyi
İyi bir çalıştırma sonucunu atmak için geçerli bir neden olduğunu düşünmüyorum
Ama programın içinde en ufak bir belirsizlik kaynağı varsa —ki bu sanıldığından yaygındır— en iyi süre temsil gücü düşük olabilir
Bununla ilgili https://tratt.net/laurie/blog/2019/minimum_times_tend_to_mis... iyi bir yazı
Kuralları didikleyen biri olarak, ilk çalıştırmada arka plan daemon’ını başlatıp dosyanın tamamını belleğe yükleyip sabitlemek, ardından sonraki çalıştırmaların fiilen yalnızca doğrusal tarama yapması için önbelleği de önceden ısıtmak isteyebilirsiniz.
İlk çalıştırmada sonucu önceden hesaplamak da, kuralları ne kadar esneterek yorumladığınıza bağlı olarak mümkün görünüyor; sayıları önceden daha yoğun bir biçime ayrıştırıp sonraki çalıştırmalarda doğrudan kümülatif toplam olarak okumak da mümkün olabilir.
Yarışmanın ruhuna hiç uymuyor, ama görünen kurallara göre yasaklanmış gibi de durmuyor.
Ön hesaplamadan hoşlanmıyorsanız girdiyi önceden sıralamak ya da önceden ayrıştırmak; sıkıştırma, sıralama, sıralanmış bellek yerleşimi gibi hilelere başvurmak da mümkün.
Uç bir örnek olarak
calculate_timebetiğini 0 saniye döndürecek, rakipler içinse 9999 döndürecek şekilde yamalamak bile mümkün olabilir.Girdiyi hiç okumadan yanıtı tek satır olarak sabit kodlamaktan, dosya içeriğini bilmiyormuş varsayarak işlemeye kadar ön hesaplamanın gri alanı yaklaşık bir milyar aşamadan oluşur.
Neyin adil ön hesaplama olup neyin olmadığına karar verme yarışmasına dönüşebilir.
Bu yüzden makine öğrenmesi yarışmaları katılımcılara nihai veriyi göstermez.
Hesaplamanın uygulama çalıştırıldığı anda gerçekleşmesi gerektiği, ölçüm dosyasını derleme aşamasında işleyip sonucu ikili dosyanın içine gömmenin yasak olduğu belirtilmiş.
Bu sadece disk hızına bağlı bir problem değil mi diye düşünüyorum. SIMD ya da çok iş parçacığı gibi optimizasyonların anlamlı olup olmayacağından emin değilim.
Farklı gözlem istasyonu sayısına ve hash arama yöntemlerine göre değişir elbette, ama giriş/çıkışa kıyasla ölçülebilir düzeyde olup olmayacağı konusunda şüpheliyim.
Modern donanımı varsayarak tasarlanan sistemler bundan yararlanır; çalıştığım redpanda.com da buna bir örnek.
Ayrıştırma, hesaplama süresinin büyük bir bölümünü oluşturur ve ayırıcıları bulmak için SWAR gibi SIMD teknikleri yardımcı olabilir.
Bu tür algoritmaların temiz bir uygulamasını görmek istiyorsanız Stringzilla iyi bir örnek: https://github.com/ashvardanian/StringZilla
İlk çalıştırmadan sonra dosyanın tamamen bellekte önbelleğe alınması konusuna burada yanıt verdim: https://news.ycombinator.com/item?id=38864034
Tipik sunucularda bu SSD’lerden 15 tane takmaya yetecek kadar PCIe hattı olduğundan, sunucu giriş/çıkış bant genişliği bellek bant genişliğine yakın seviyededir.
Daha pahalı sunucularda PCIe 5.0 gibi daha hızlı ve daha fazla hat bulunur.
Bu dosya 1 milyar satır olduğu için sıkıştırıldığında yaklaşık 1GB’dır ve atılan ilk çalıştırmadan sonra belleğe gireceğinden bu senaryoda giriş/çıkış bant genişliği önemli değildir.
GitHub deposunda sıkıştırılmamış hâlinin 12GB olduğu yazıyor; bu da yine giriş/çıkış bant genişliğinin önemli olmadığını doğruluyor.
Ana fikir, diskin nadiren darboğaz olduğudur.
Örneğin Linux’ta ext2 kullanılırsa ilk çalıştırmadan sonra tüm dosyanın önbelleğe alınması olasıdır, ama ZFS’te böyle olmayabilir.
Böylece sayılar düşük basamaktan yüksek basamağa doğru gelir; ardından ayırıcı ve string gelir, EOF ya da satır sonu görülene kadar ilerlenir.
Kurallara göre gönderilerin tüm girdiler için doğru çalışması gerekiyor, ama
create_measurements.shile oluşturulan belirli girdiye göre ayarlanabileceği ve muhtemelen ayarlanması gerektiği anlamına geliyor gibi görünüyor.Örneğin verilen gözlem istasyonu kümesine özel perfect hash function kullanan bir gönderi hayal edilebilir.
Böylece aşırı uyumlu optimizasyonların önüne geçilebilir.
127’den büyük byte, çok baytlı bir UTF-8 karakteri anlamına gelir.
Eğlence olsun diye awk ile Java arasında hız karşılaştırması yaptım.
awk -F';'ile istasyon bazında toplam, adet, minimum ve maksimumu biriktirip END kısmında ortalamayı hesaplayarak yazdıran bir betik.file_fdwile CSV dosyasını dış tablo yapıpGROUP BY station_nameileMIN,AVG,MAXhesaplama yöntemi.clickhouse localiçindefile('measurements.txt', 'CSV', 'station String, t Float32')okunup istasyon bazındamin,max,avggruplanıyor vemax_threads = 8ile çalıştırılıyor.Zamanın büyük kısmı dosya ayrıştırmaya gidiyor.
sumdeğişkeni epey büyüyebileceğinden akışlı ortalama kullanmak daha iyi olur.Örneğin
new_mean = ((n*old_mean)+temp)/(n+1)gibi bir yöntem.İlginç bir meydan okuma ama yalnızca Java’ya özel olması üzücü. İnsanların doğrudan JVM bayt kodunu elle üretmeye başlayacağı zamanı merakla bekliyorum
[0] https://github.com/gunnarmorling/1brc/discussions
Eğlenceli. Advent of Code sonrası etkinliği gibi hissettiriyor
Diller arası adil bir karşılaştırma olacaksa make ve derleme süresi de dahil edilmeli. Java/Maven’ı birkaç yıldır kullanmıyordum;
./mvnw clean verifyindirmesinin ikinci dakikada hâlâ sürdüğünü görünce nedenini yeniden hatırladımAyrıca artımlı derleme araçları arasında Gradle daha hızlıdır
Programlamayı öğrenmek için harcanan sürenin uygun bir oranı da eklenmeli
Böyle bir meydan okumada çok saf bir sürümün kazanma ihtimali yüksek olur; bunun hem gerçekçi olmadığını hem de meydan okumanın amacına aykırı olduğunu düşünüyorum
cleanyaptığınızı anlamıyorumÖnbelleği çöpe atıp sonra yavaş demek gibi
Harici bağımlılık kullanılamayacağı yazıyor
Çek Teknik Üniversitesi’ndeki C dersinde buna çok benzer bir ödev vardı
Tüm öğrenci gönderimleri sürekli sıralama tablosunda değerlendiriliyordu; daha iyi not için ek puanlar, fiilen de statü puanı kazanmak isteyen birçok öğrenci onlarca saatini optimizasyona harcadı
Birinci olan 6 saniye sürmüş.. gerçekten şaşırtıcı