Ubuntu paketlerini yeniden derleyerek %90 daha hızlı hale getirmek
(gist.github.com/jwbee)- Aynı
jqkaynağı yeniden derlenip allocator değiştirildiğinde, 500 MB GeoJSON işleme süresi 4,606 saniyeden 2,428 saniyeye düştü ve Ubuntu ikili dosyasına göre 1,90 kat hızlandı - Benchmark, Ryzen 9 9950X üzerinde Alameda County Assessor parsel haritasında
TotalNetValue < 193000koşuluna uyanSitusCitydeğerlerini çıkarma yöntemiyle yapıldı - Yalnızca basit bir yeniden derleme bile %2–4 iyileşme sağladı;
clang-18,-O3,-flto,-DNDEBUGkombinasyonu Ubuntu paketine kıyasla 1,20 kat performans verdi - Profilde bellek ayırma maliyeti belirgin göründüğü için
TCMalloc,jemalloc,mimallockarşılaştırıldı;LD_PRELOADdeneylerinde en hızlısımimallocoldu - Nihai
mimallocbağlantılı derleme, ayrı bir 2,2 GB JSON işleme örneğinde de 0,755 saniyeye karşı 1,424 saniye sonucu verdi; dağıtımın varsayılan derlemesiyle iş yüküne bağlı olarak büyük farklar oluşabiliyor
Temel iş yükü ve ölçüm yöntemi
- Test hedefi JSON işleme aracı
jq; girdi verisi Alameda County Assessor’ın parsel haritasını içeren 500 MB GeoJSON dosyası - Çalıştırılan sorgu, parsel listesinden
TotalNetValue < 193000koşulunu sağlayan öğelerinSitusCitydeğerini yazdırıyor.features[] | select(.properties.TotalNetValue < 193000) | .properties.SitusCity
- Ubuntu’nun varsayılan
/usr/bin/jqdosya önbelleğe alınmış durumdayken yaklaşık 5 saniye sürdü; ayrıntılı benchmarkhyperfineile tekrarlı ölçüldü - Çalışma sırasındaki değişkenliği azaltmak için
taskset -c 2ile mantıksal CPU 2’ye sabitlendi- Bu ayar, CPU 0’da çalışan sistem kesmeleri ve CPU migration etkilerinden kaçınmak için kullanıldı
Aynı kaynağın basit yeniden derlemesi
- Ubuntu’nun kullandığı
jqkaynak kodu alındı ve ek bayrak olmadan configure ile derleme yapıldı - Bu basit yeniden derleme bile Ubuntu ikili paketinden yaklaşık %2–4 daha hızlı oldu
- Yeniden derlenen ikili: ortalama 4,517 saniye
- Ubuntu
/usr/bin/jq: ortalama 4,641 saniye - Sonuç olarak yaklaşık 1,03 kat performans
clang ve optimizasyon bayraklarının uygulanması
- Sonraki aşamada
clang-18, daha yüksek optimizasyon seviyesi, LTO ve debugging/profiling ile ilgili bayraklar birlikte uygulandı - Performansı etkileyen temel bayraklar
-O3,-flto,-DNDEBUGoldu-O3,-O2den daha yüksek optimizasyon seviyesi kullanır-flto, link-time optimization’ı etkinleştirir-DNDEBUG, profilde belirgin görünen assertion maliyetini azaltır
- Uygulanan configure örneği şöyle
CC=clang-18LDFLAGS="-flto -g -Wl,--emit-relocs -Wl,-z,now -Wl,--gc-sections -fuse-ld=lld"CFLAGS="-flto -DNDEBUG -fno-omit-frame-pointer -gmlt -march=native -O3 -mno-omit-leaf-frame-pointer -ffunction-sections -fdata-sections"
- Bu derleme Ubuntu ikili dosyasından 1,20 kat daha hızlıydı
- Optimize yeniden derleme: ortalama 3,853 saniye
- Ubuntu
/usr/bin/jq: ortalama 4,631 saniye
Allocator değiştirme deneyi
jqkarmaşık bir C programı ve profilde en büyük maliyet bellek ayırma olarak göründü- Önce Ubuntu paketi olarak sunulan
TCMalloclink edilerek yeniden derlendiLDFLAGSiçine-L/usr/lib/x86_64-linux-gnu -ltcmalloc_minimaleklendi- Yeniden derlenen ikili: ortalama 3,253 saniye
- Ubuntu
/usr/bin/jq: ortalama 4,611 saniye - Ubuntu ikili dosyasına göre 1,42 kat daha hızlı sonuç
- Varsayılan Ubuntu ikili dosyasında yalnızca allocator’ı
LD_PRELOADile değiştirmek de kısmi iyileşme sağlayabiliyor- Varsayılan: ortalama 4,601 saniye
- TCMalloc preload: ortalama 4,082 saniye
- Varsayılana göre 1,13 kat hızlanma
Dinamik preload ve THP ayarı
- Ubuntu’nun sağladığı
jemalloc,mimalloc,TCMalloc,LD_PRELOADile karşılaştırıldı - Bu karşılaştırma aşağıdaki ortam değişkenleri ayarlandıktan sonra elde edilen sonuçlara dayanıyor
MIMALLOC_LARGE_OS_PAGES=1MALLOC_CONF="thp:always,metadata_thp:always"GLIBC_TUNABLES=glibc.malloc.hugetlb=1
- Sonuçta mimalloc en hızlısı oldu
- Varsayılan glibc: ortalama 4,123 saniye
- TCMalloc preload: ortalama 4,130 saniye
- jemalloc preload: ortalama 3,510 saniye
- mimalloc preload: ortalama 3,154 saniye
- THP’nin etkinleştirilmesi glibc allocator, jemalloc ve mimalloc’un hepsine fayda sağladı
THP + mimalloc,THP + glibcten %31 daha hızlı, glibc varsayılanından ise %48 daha hızlı
mimalloc bağlantılı derlemenin nihai sonucu
- Dinamik preload’un performans açısından ideal olmadığı düşünülerek, son aşamada
mimalloclink edilipjqyeniden derlendi - Nihai derleme Ubuntu ikili paketinden 1,90 kat daha hızlıydı
mimallocyeniden derlemesi: ortalama 2,428 saniye- Ubuntu
/usr/bin/jq: ortalama 4,606 saniye - Her benchmark 10 çalıştırma üzerinden yapıldı
- Aynı derleme başka bir uygulamada da kullanıldı
- 13.000 dosyadaki 2,2 GB JSON işlendi
- Paralelleştirme için
rushkullanıldı mimallocile yeniden derlenenjq: 0,755 saniye- Ubuntu paketi
jq: 1,424 saniye
- Bu ayrı örnekte de hız artışı neredeyse 2 kat oldu
1 yorum
Hacker News yorumları
“Tek bir Ubuntu paketini yeniden derleyip bellek ayırıcısını değiştirerek %90 daha hızlı yapmak” gibi clickbait başlıklar, insanın TCP/IP üzerinden bir yumruk atası geliyor. Sonuçta sadece tek bir paketti ve iyileşmenin bir kısmı yeniden derlemeden bile kaynaklanmıyordu.
Yine de jemalloc’u
LD_PRELOADile bir programa enjekte edipmallocuygulamasını değiştirmeyi denemişliğim var; sonuçlar epey iyiydi. Performansı ölçmedim ama o uygulamanın bellek kullanımı dengelendi ve bellek sızıntısı gibi görünen sorun da çözüldü. Aslında bunun uygulamanın kendi sorunundan çok, standartmallocun bellek parçalanması olması kuvvetle muhtemel.free()çağrılsa bile, istisnai durumlar dışında dışarıdan bakıldığında bellek gerçekten serbest bırakılmıyor.İş parçacığı ve CPU çekirdeği sayısı arttıkça sorun daha da kötüleşiyor. Kolay çözümlerden biri, “sihirli” ortam değişkeni
MALLOC_ARENA_MAX=2ayarlayıp önbellek sayısını sınırlamak. Diğer yol, uygulamanın önbellekleri boşaltmak için düzenli olarakmalloc_trim()çağırması; ama bu kaynak kod değişikliği gerektiriyor.https://www.joyfulbikeshedding.com/blog/2019-03-14-what-caus...
Buna karşılık, potansiyel olarak hatalı olabilecek
-O3ile derlememesi iyi bir şey. Performansın kritik olduğu bazı kısımlar için iyi olabilir ama tüm sistemi-O3ile derlenmiş halde istemem.Uzun zaman önce Mozilla’yı ve kendi Linux çekirdeğimi zevkime göre derlemeye başladım; genelde makul bir performans artışı elde ediyordum. Gentoo Linux dağıtımının bütün amacı da örneğin her şeyi kaynaktan optimize ederek derleyip performans artışı elde etmek.
jq,grep,ffmpeg,ocrmypdfgibi bazı yaygın yardımcı araçlarda CPU darboğaz olduğunda; bu tür genel Unix araçları çoğu zaman belirli bir uygulama için değil, genel amaçlı derleme hedefleri olarak hazırlanıyor.Mühendislik uzlaşmadır. Yazıdaki kazanımların çoğu bellek ayırıcısını özelleştirmekten geliyor. Bazı projelerin çok iş parçacıklı olduğunu; bir iş parçacığında ayırıp veriyi başka bir iş parçacığında yazdığını, üçüncü bir iş parçacığında da serbest bırakabildiğini unutmamak gerekir.
Ayırıcı bununla başa çıkmak zorunda olduğundan, bir projede hız artışı sağlayan şey başka bir projede çakışmaya dönüşebilir. Yeniden ayırma stratejisi de sorun. Bazı programlar baştan ayırır ve bir daha
malloca dokunmaz; bazıları ise sürekli serbest bırakıp yeniden alır. Parçalanmayı ne kadar iyi yönettiği, çalışma süresinin 10 saniye mi 10 yıl mı olduğu da önemlidir. Bazen ayırıcı seçimi, uzun vadeli kararlılık ile kısa vadeli hız arasındaki fark anlamına gelir.4K video test edip kareleri önbelleğe alan bir video düzenleyici geliştirirken çeşitli ayırıcıları denemiştim. Kare başına 32 MB ve 60 fps olduğunda, tek bir iz için saniyede neredeyse 2 GB eder. Hemen ayırıcı sınırlarına çarpıyorsunuz ve en azından varsayılan glibc ayırıcısının uzun vadeli kararlılık açısından en iyisi olduğunu fark ediyorsunuz. Ama kısa benchmark'larda en yavaşıdır.
Elbette hız artışının boyutu değişebilir, ama benchmark'lar genelde genel bir iyileşme gösterir. Bunun belirli bir uygulama için anlamlı olup olmadığı ise bambaşka bir mesele. Çakışmalar konusunda da, bunların hepsi genel amaçlı çok iş parçacıklı ayırıcılardır; glibc'den farklı davranmazlar ve hatalar glibc'de de aynı şekilde bulunabilir.
DEFAULT_MMAP_THRESHOLD_MAXdeğerini aşıyor ve 64 bit platformlarda bu değer 32 MiB olduğu için,malloptkılavuzunda belgelendiği üzere glibc'yi bunları önbelleğe almaya ikna edemiyorsunuz.Her seferinde
mmapile doğrudan çekirdekten bellek istiyor vemunmapile geri veriyor. Bu sistem çağrıları biraz yavaştır; benim durumumda ise ilk erişimde her bellek sayfası için page fault oluşturmanın maliyeti, performans hedefini tutturamayacak kadar yavaştı. Çözüm gerçekten basit: Video kareleri için genel amaçlı bir ayırıcının ya dammapin üzerinde kendi serbest listenizi kullanmak yeterli. Tam olarak aynı boyuttaki ayırmalar çok kararlı biçimde tekrarlandığı için iyi çalışır.[1] UYVY formatında 64 MiB'den biraz küçük, I420 formatında ise 48 MiB'den biraz küçük.
Ancak bu üst yorumu okuyunca yazıyı tamamen yanlış mı anladım diye endişelendim. Üslubundan, gist'te söylenen şeyin kesinlikle yapılmaması gerektiğini ve bunun tüm bu karmaşıklıkları gözden kaçıran korkunç bir öneri olduğunu ima ediyormuş gibi geliyor. Asıl gist iyi bir yazı mı, geçerli noktaları var mı, yoksa hiçbir değeri yok mu anlamama yardımcı olabilir misiniz? Bu yorumu görmeden önce değerli olduğunu düşünmüştüm; ama bunu ayırt edecek kadar zeki olmadığımı fark ettim.
Veriye uygun sıkıştırma algoritmasını kullandığınızda, insanlar neden aptal olduğunuzu ve başka bir algoritma kullanmanız gerektiğini anlatır. Yakın zamanda Dynamo'ya koymak için belirli uzun JSON dizelerini sıkıştırmam gerekti; popüler algoritmaların hepsini kapsamlı biçimde test ettim ve Brotli açık ara öndeydi. Yine de gelip geçen herkesin zlib'in daha iyi olduğunu söylemesini engelleyemedim. Bazen epey yorucu oluyor.
Gentoo Linux, aslında tam da böyle insanlar için, kendi Linux makinenizi kendi kullanımınıza göre optimize edebilmeniz amacıyla yapılmış bir dağıtım
İlk kurulumdan sonra oldukça basit ve kullanımı kolay. Matrix’teki Gentoo Linux kanalında çok arkadaş edindiğimi hatırlıyorum; eğlenceli zamanlardı
https://www.gentoo.org/
İlginç bir bilgi: İlk ChromeOS temelde özel bir Gentoo Linux kurulumuydu. Hâlâ içeride Gentoo Linux kullanıp kullanmadığını bilmiyorum
Gentoo’yu 20 yıldır kullanıyorum ama hiç performans için kullanmadım. Gentoo, nasıl davranmasını istediğinizi bildiğinizde harika ve sizi oraya ulaştırıyor
Gentoo’nun amacı, önceden derlenmiş ikili paketler yerine tüm programların kaynaktan derlendiği bir işletim sistemine sahip olmak. Bu, ileri düzey hız artışları ve özelleştirme sağlar; ancak kernel gibi en temel bileşenleri bile kaynaktan derlemeniz gerektiği anlamına gelir. Linux topluluğunda, zahmetli kurulum süreci nedeniyle çok karmaşık bir işletim sistemi olarak bilinir. Varsayılan Gentoo kurulumu doğrudan komut istemine önyüklenir; kullanıcı diski elle bölümlendirmeli, “Stage 3 tarball” denen paketi indirip açmalı, paketleri elle kurarak sistemi oluşturmalıdır. Yeni ya da deneyimsiz kullanıcılar, kurulum programına girip grafik ekran olmadığını gördüklerinde çoğu zaman ne yapacaklarını bilemez. /g/ üyeleri de sık sık Gentoo’nun değerini abartarak yeni kullanıcıları kurulum denemeye kandırır
Bir iki kez tökezleyebilir, ama genel Linux deneyiminiz yeterliyse build recipe’lere girip düzeltmeniz, ihtiyacınıza göre çalışır hâle getirmeniz ve değişikliklerinizi upstream’e katkı olarak göndermeniz muhtemel. Bu, minimalizme takıntılı biçimde odaklanmanın ve her tür aşırı mühendislikten kaçınmanın sonucu. Gentoo kullandığım süre boyunca özlediğim şey de buydu. Gentoo’da hep diğer kullanıcılara pek faydası olmayacak biçimde USE flag’leri ve package mask’leriyle oynamak zorunda kalıyordum. Build sistemi o kadar karmaşıktı ki, yıllar içinde gerçekten öğrenip sorunları kök neden düzeyinde düzeltmek ve upstream’e katkı vermek çok zordu. Tüm sistemi kaynaktan derlemek istemiyor, ama dağıtımın sağladığı ikililerle doğrudan kaynaktan derlediğiniz paketleri birlikte kullanmak istiyorsanız Void ideal bir temel de olabilir
Sonra ArchLinux’a geçtim ve benim için genel olarak iyi oldu. Oldukça standart bir işlemci kullanıyorsanız Gentoo’nun o kadar büyük bir avantaj sağlayacağını sanmıyorum
Bunu yaparsanız yalnızca
jqiçin değil, regex ayrıştırma bağımlılığı onigurama için gelen güvenlik güncellemelerinin de dışında kalırsınız. Geçmişte onigurama için güvenlik güncellemesi olmuştu; böyle bir şey tekrar olursa savunmasız kalabilirsiniz.jq, güvenilmeyen JSON’u ayrıştırmak için sık kullanılırİçerik “Güvenlik güncellemesi: çeşitli hatalı pointer dereference’ları, sınır dışı yazma bellek bozulması ve stack buffer overflow düzeltmeleri” şeklindeydi; ilgili olay CVE-2017-9224, CVE-2017-9226, CVE-2017-9227, CVE-2017-9228, CVE-2017-9229 hakkındaydı
libonig5adlı ayrı bir pakette yer alıyor ve normal şekilde güncellenecektirCVE sisteminin değerini küçümsemeye çalışmıyorum, ama bulgular arasında gerçek etki açısından büyük farklar olduğunu da inkâr etmek zor
Bu işlerle uğraşmayalı biraz oldu, ama hatırladığım kadarıyla upstream geliştiricilerin kullandığı flag’lerin ötesine geçtiğiniz anda tuhaf bug’lar ve bug çıktığında muazzam bir ilgisizlik satın almış oluyorsunuz. Burada kastettiğim dağıtım paketleyicisi değil, upstream geliştirici
libc dışı
mallockullanmadım ama aynı ilkenin geçerli olacağını düşünüyorumAma herkes böyle yaparsa bu bir monokültür olur; monokültür kırılgan ve kötüdür. Kod ancak farklı bağlamlarda, yani farklı platformlarda, derleyicilerde, seçeneklerde, kütüphanelerde vb. derlendiği için biraz olsun sağlamlaşır. Platformun ya da build flag’lerinin tesadüfen tuzağın hemen yanından geçip çoğunluğun dokunmadığı bir bug yine de bug’dır; onu bulup düzeltmek kod için daha iyidir. Bireyler olarak kodun genel olarak kırılgan olmak yerine sağlamlaşmasından hepimiz fayda görürüz
Elbette
-march=nativein gördüğüm başlıca iyileştirme olduğunu da sanıyordum; bu yazı bunun her zaman öyle olmadığını gösteriyor. Kayan nokta kullanan uygulamalarda pürüzlü alanların daha fazla olması da muhtemel görünüyorBelirli bir iş akışında benchmark’ları iyi çıkan başka bir allocator ile yeniden derlemeye daha yakın
malloctan daha iyi olan neredeyse her şey mümkün. Dağıtımların mimalloc veya jemalloc yerine hâlâ glibcmallockullanması fiilen görev ihmalimallocın hangi iş yüklerinde iyi olduğu biliniyor mu ki?Bu Rust tabanlı
jqklonuyla performansının nasıl karşılaştırıldığını merak ediyorumcargo install --locked jaqBelirli bir CPU ailesine yönelik optimizasyonları açmak için
RUSTFLAGS="-C target-cpu=native"de eklenebilir.cargo install, yazıda anlatılan türden kullanım için Rust’ın hakkı yeterince verilmeyen bir özelliği. Aracı kaynak koddan derlediği için, eski CPU’larla uyumluluk adına ikili dosyalara normalde dahil edilmeyen platforma özgü özellikleri veya komutları seçebilirsiniz. Depoyu klonlamanız ya da nasıl derleneceğini çözmeniz gerekmez; bunlar kendiliğinden gelirjaq[1] veyq[2],jqkullanırken hızlı ve kolay bir performans artışına ihtiyaç duyduğumda tercih ettiğim seçenekler[1] https://github.com/01mf02/jaq
[2] https://github.com/mikefarah/yq
jqvegojqile karşılaştırıyorum; AoC 2022 day 13 için kendijqçözümümle test ediyorumhttps://gist.github.com/oguz-ismail/8d0957dfeecc4f816ffee79d...
Hâlâ ikisinin gerisinde kalıyor
cargo installda depo URL’si belirtebileceğiniz bir--gitbayrağı da var. Herkese açık paket olmadığında ya da henüz yayımlanmamış en son commit’i istediğinizde kullanılabilirDaha önce birçok kez kullandım; özellikle de kişisel olarak aceleyle yaptığım araçları depoya push ettikten sonra, release süreci oluşturmadan veya binary’leri kendi makinelerime elle kopyalayıp derlemede kullanılan tam commit’i takip etmek zorunda kalmadan hızlıca kurmanın kolay bir yolu olarak işe yaradı
Bunu gerçekten yapacaksanız Ubuntu’nun istediği tam kaynak paketini indirtmeniz yeterli. Bu durumda
apt-get source jqkullanılırArdından paketin içine girip istediğiniz gibi yeniden derleyebilirsiniz. Dağıtım veya arşivleme için yeniden paketleyebilirsiniz de. Böylece bir yığın garip hata ve tutarsızlık elde etmek yerine upstream Ubuntu’ya çok daha yakın bir sonuç alırsınız
Başlık yanıltıcı. Daha hızlı hâle gelen sürenin %90’ı anlamına geliyor; gerçekte yaklaşık %45 daha hızlı
Dili nasıl kullandığımızla ilgileniyorsanız biraz ilginç. Aynı sürede %90 daha fazla iş yaptığını söylemek de mümkün; bu, sık kullandığımız başka hız birimleriyle, örneğin mil/saat, kelime/dakika, bit/saniye ile uyumlu. Ama bilgisayar performansında sabit bir iş miktarının ne kadar sürdüğünü ölçme geleneği var. Muhtemelen genelde iş miktarı sabit olduğu, değişenin bekleme süresi olduğu için. Bu blog yazısında da durum tam olarak böyle, bu yüzden zaman paya yerleşiyor. Yazının kendisi çok ilginç ve iyi yazılmış, ama %90 hızlı değil
Ayrıca zaman birimi kullanacaksanız “faster” kelimesini kullanmazsınız. “45% less time” ile “45% faster” çok farklı iddialar ve ikisi de programlamanın içinde de dışında da anlamlı
“Bir şeyi %N azalttık” dediğimizde genellikle o %N’nin başka bir değerin değil, azaltılan şeyin kendisinin %N’si olduğu varsayılır
Böyle basit bir değişiklikle büyük bir hız artışı elde edilebildiğini okuyunca aklıma ilk gelen, bunu jq yazarlarına bildirmek gerektiğiydi. Dikkat edilmesi gereken tuzaklar olabilir; test ettikten sonra herkes için daha hızlı hâle getirebilirler
Sonuç ne olursa olsun, kısaca haber vermek faydalı görünüyor. Ama yazıda bu seçenek hiç değerlendirilmemiş gibi, buradaki yorumlarda da göremiyorum. Bir şeyi mi kaçırıyorum?
Orada da glibc allocator’ın standart olup olmadığını bilmiyorum
https://en.m.wikipedia.org/wiki/Clear_Linux_OS