- {fmt}, type erasure ile şablon şişmesini azaltan bir C++ biçimlendirme kütüphanesi ve bu deneyde basit bir
fmt::printyürütülebilir dosyası 75kB'den 14kB'ye indirildi - Temel yapı,
formatın şablon olmayanvformata devretmesi ve çıktı türünün de tampon API'siyle gizlenmesi; böylece ikili boyut ve derleme süresi birlikte azaltılabiliyor - aarch64 Ubuntu 22.04 ve GCC 11.4.0 üzerinde {fmt} 11.0.2'nin stripped yürütülebilir dosyası 75kB idi; locale devre dışı bırakma, yerleşik türleri azaltma ve boyut optimizasyon makrolarıyla bu değer 71kB → 31kB → 27kB → 23kB seviyesine indi
- C++ çalışma zamanını kaldırmak, istisnaları
FMT_THROWileabortolarak ele alıp-fno-exceptions,-nodefaultlibs,-lcile derledikten sonrabasic_memory_bufferın varsayılan ayırıcısını malloc/free tabanlı hale getirerek mümkün oldu - Nihai yürütülebilir dosya 14kB; aynı sistemde boş bir C
maininin 6kB olduğu düşünülürse {fmt}'nin eklediği boyut 10kB'den az velddçıktısında da C++ çalışma zamanı bağımlılığı görünmüyor
{fmt} küçük ikilileri nasıl üretiyor
- {fmt} formatting library, IOStreams, Boost Format ve tinyformat gibi alternatiflere kıyasla işlev çağrısı başına çoğu zaman kat kat daha az üretilmiş kod oluşturuyor
- Bunun anahtarı, birden çok katmanda type erasure uygulanarak şablon şişmesinin azaltılması
- Biçimlendirme argümanları
format_argsile type erasure'dan geçiriliyorformatşablon işlevi, gerçek işi şablon olmayanvformata devrediyor- Çıktı yineleyicileri ve diğer çıktı türleri de ayrı bir tampon API'si üzerinden type erasure ile gizleniyor
- Şablon kullanımı en üstteki ince katmanla sınırlı tutuluyor; bu yapı hem daha küçük ikililere hem de daha hızlı C++ derleme sürelerine katkı sağlıyor
printfe yakın kod boyutu ve daha güçlü güvenlik
- Örnek program yalnızca
fmt::print("The answer is {}.", 42);çağrısı yapıyor - Derleme sonucu, IOStreams'ten çok daha küçük ve
printförneğiyle benzer seviyede printften farklı olarak {fmt}, çalışma zamanı tür güvenliği sunuyor- Biçim dizgesi hataları derleme zamanında yakalanabiliyor
- Biçim dizgesi çalışma zamanında belirlense bile hatalar istisna ile ele alındığından tanımsız davranış, bellek bozulması ve olası çökme önlenebiliyor
- C değişkenli argümanlarıyla pek uyumlu olmayan konumsal argümanlar (positional arguments) kullanıldığında, {fmt} çağrısı genellikle daha verimli oluyor
Başlangıç boyutu ve locale'ın kaldırılması
- 2020 tarihli kütüphane boyutu optimizasyonu çalışmasında {fmt}, 100kB'nin altına,
-Os -fltoile yaklaşık 57kB seviyesine kadar indirilmişti - Sonrasında {fmt}, Junekey Jeon'un katkıda bulunduğu Dragonbox algoritmasını kayan noktalı sayı biçimlendirmede kullanmaya başladı
- Bu ölçüm, son kullanıcının hissettiği yürütülebilir dosya boyutunu temel alıyor ve aarch64 Ubuntu 22.04 ile GCC 11.4.0 üzerinde yapıldı
- {fmt} 11.0.2'nin temel derlemesi
-Os -flto -DNDEBUGvestripsonrası 75kB- Son 4 yılda çeşitli değişiklikler olsa da boyut belirgin biçimde gerilemedi
- Locale desteği
FMT_STATIC_THOUSANDS_SEPARATORile kapatıldığında ikili boyut 71kB'ye düşüyor- {fmt} biçimlendirmesi varsayılan olarak locale'dan bağımsız
- Locale, isteğe bağlı olarak
Lbiçim belirteciyle kullanılabiliyor
Yerleşik türleri azaltma ve “kullanmadığın şeyin maliyetini ödeme” modeli
- Bloaty analizi, sayı biçimlendirme kodunun, özellikle kayan noktalı sayı biçimlendirme kısmının ikili boyutun büyük bölümünü oluşturduğunu gösteriyor
- Kayan noktalı sayı biçimlendirme tablolar da kullanıyor ve bu tablolar Bloaty çıktısında görünmüyor
- Temel yük, biçimlendirme işlevinin biçimlendirilebilen tüm türleri bilmek zorunda olmasından kaynaklanıyor
- Bu yaklaşım C standardındaki
printfiçin uygun, ancak {fmt} için zorunlu değil - {fmt}, tüm tür kümesini önceden bilmeden de rastgele türleri biçimlendirebilen bir genişletme API'si sunuyor
- Bu yaklaşım C standardındaki
- Deneysel uygulamada
FMT_BUILTIN_TYPES=0ayarlanarak yalnızcaintözel işlem görüyor, diğer türler ise genel genişletme API'sine gönderiliyorint, dinamik genişlik ve hassasiyet işlemleri için gerekli- Örnek:
fmt::print("{:{}}\n", "hello", 10);ifadesi"hello "çıktısını üretir
- Bu yaklaşım, kullanılmayan türlerin maliyetini ödememe modeli sağlıyor; ancak çağrı başına ikili boyut biraz artıyor
- Kayan noktalı sayı veya başka türler gerçekten biçimlendirilirse ilgili kod yine derlemeye dahil ediliyor
FMT_BUILTIN_TYPES=0sonrasında örnek ikili 31kB'ye düştü- Ardından kalan locale izleri e582d37 ve b3ccc2d ile temizlendi;
FMT_USE_LOCALEmakrosuyla bunun daha açık biçimde kapatılabilmesi sağlandı ve boyut 27kB oldu
Hız ve boyut arasında seçim, ardından C++ çalışma zamanını kaldırma
- Kütüphane içinde hız için boyuttan feragat edilen birden fazla bölüm var
- Ondalık basamak sayısını hesaplayan
do_count_digits, 256 baytlık bir tablo kullanıyor- Bu uygulamayı koşulsuz değiştirmek başka kullanım senaryolarını olumsuz etkileyebilir
__builtin_clzkullanılamayanconstexprgibi durumlar için bir fallback uygulama zaten mevcut
FMT_OPTIMIZE_SIZEmakrosu eklenerek kullanıcının fallback uygulamayı kullanıp kullanmayacağını kontrol etmesi sağlandı- Bu ayar ve benzer birkaç değişiklikle ikili boyut 23kB'ye indi
- C++ standart kütüphanesi bağımlılığını kaldırmak için istisnalar
FMT_THROWile devre dışı bırakılabiliyor- Örnek,
FMT_THROW(s)=abort()ve-fno-exceptionskullanıyor - Genel olarak önerilmese de hataların çoğunun derleme zamanında yakalandığı bazı kullanım senaryolarında kabul edilebilir olabilir
- Örnek,
-nodefaultlibs -lcile derlendiğinde geriye kalan C++ çalışma zamanı bağımlılığıfmt::basic_memory_bufferkaynaklı- Bu tampon, küçük bir yığın içi tahsisli tampon ve gerekirse dinamik belleğe genişleyebiliyor
fmt::printgenellikle doğrudanFILEtamponuna yazabildiğinden dinamik tahsis gerekmiyor
- Daha genel bir çözüm olarak varsayılan ayırıcı,
new/deleteyerine malloc/free tabanlı olacak şekilde değiştirildi- Bu değişiklikten sonra nihai ikili boyut 14kB oldu
- Aynı sistemde boş bir C
mainprogramı 6kB olduğundan, {fmt}'nin eklediği boyut 10kB'den az
ldd a.outçıktısı yalnızcalibc.so.6ve yükleyiciyi gösteriyor; C++ çalışma zamanı bağımlılığı görünmüyor- Nihai sonuç, {fmt}'nin gömülü ve bellek kısıtlı ortamlarda daha küçük boyutla kullanılabileceğini gösteriyor
1 yorum
Hacker News yorumları
Bu aslında daha çok komite eğilimiyle ilgili bir sorun; bu yüzden üçüncü taraf bir kütüphane olan fmt’nin illa hatalı varsayılanlara sahip olmasını beklemem
Şaşırtıcı biçimde, bu özellik C++20’de std::format olarak standartlaştırılırken komite, standardın başka birçok yerindeki bu hatayı tekrar eklemedi
Bu yüzden C++’ı “tutarlı” yapmak adına gereksiz yere daha kötü hale getirmemeleri için çağrıda bulunan öneri sahipleri için de biraz umut var
Kayan nokta biçimlendirme için gereken kod miktarına bakınca epey şaşırtıcı
Bağlantısı verilen Dragonbox [1] projesi de okunmaya değer; neredeyse hiç kullanılmayan dallanmalar bile oldukça optimize edilmiş
[1] https://github.com/jk-jeon/dragonbox
Normalde Zig derleyicisi Windows’ta C runtime’a bağımlı olmadığı için MSVC’den daha küçük ikililer üretebiliyor; ama bu sefer araç yaptığı işe kıyasla garip biçimde büyüktü
Binary Ninja ile açınca kodun büyük kısmının kayan nokta biçimlendirme desteği için olduğunu gördüm; çıktıya vermeden önce kayan nokta sayıları tamsayıya cast edince beklediğim boyuta kadar küçüldü
Boyut optimizasyonu denemeleri yapılıyor ve şu anda 8-bit AVR üzerinde yaklaşık 3k’ye kadar küçültülebiliyor
Yalnızca tek duyarlıklı binary32 için implementasyon ve tablolar dahil; çift duyarlık çok daha fazlasını gerektiriyor, ama aynı zamanda şişmenin önemli bir kısmı AVR’nin kısıtlarından kaynaklanıyor
x64 gibi platformlarda çok daha küçük olabilir; yine de 3k’nin hâlâ büyük olduğu söylenebilir
Referans implementasyon da sonuçta keyfi duyarlıklı aritmetik implementasyonu ama o kadar da kötü değil
[1] https://research.swtch.com/ftoa
[2] https://go.dev/src/strconv/ftoa.go
Ondalık basamak sayısı kadar çarpıp sonra tamsayıya dönüştürmek, itoa()’dan geçirmek ve ardından ondalık noktayı uygun yere eklemek daha verimli olur mu merak ediyorum
C++’a yeni başlayan biri olarak merak ediyorum: libc++’ın varsayılan ayırıcısı, yani varsayılan new/delete implementasyonu, içeride libc’nin malloc/free’sini çağırmaktan gerçekten farklı bir şey yapıyor mu? Yapıyorsa neden?
delete[] ise belleği serbest bırakmadan önce her elemanın destructor’ını çalıştırmaya çalışır
delete[]’ın çalışması için C++’ın bir yerde ayırma boyutunu takip etmesi gerekir; bu bilgi ayrılan alanın yakınına da konabilir, ayrı bir yapıda da tutulabilir
Ayrı bir yapı kullanılırsa, nesnenin arkasındaki belleğin yanlışlıkla kullanılması durumunda bilginin üzerine yazılma olasılığı azalır; ama arama maliyeti ve ek kod gerekir
Düzgün bir C++ kütüphanesi daha fazla iş yapacaktır, ama new/delete’ın malloc/free ile aynı olmadığına dair fikir verir
Birçok implementasyonun bunu yapmasının sebebi, zaten var olması ve kullanımının kolay olmasıdır
Ancak uygulama, ELF sembol interposition’a karşılık gelen bir özellik bulunmayan platformlarda bile varsayılan standart kütüphane operator new’ini kendi implementasyonuyla değiştirebilir
Küçük olup string ve tamsayı yazdırabilecek şekilde tasarlanmış bir biçimlendirme kütüphanesinin kabaca 50 bayt civarında olmasını beklerdim
String için null sonlandırıcı kontrolü, karakter yazdırma ve iki adım geriye dallanma gibi yaklaşık 4 komut yeter
Tamsayı için negatif kontrolü ardından '-' yazdırma ve işareti ters çevirme, 1000000000’ı R1’e koyma, bölme ve kalanı saklama, ASCII '0' ekleme, karakter yazdırma, R1’i 10’a bölme, kalanı girdi olarak verme, R1=0 olana kadar yineleme gibi yaklaşık 20 komut yeter
Kayan nokta birçok programda kullanılmadığı için yalnızca gerektiğinde derlenmeli; onaltılık, pointer ve başa 0 doldurma da aynı şekilde
Kod alanı 2KB olan mikrodenetleyiciler için kod yazarken 14KB’lik string biçimlendirme kütüphanesi koymazsınız
Aynı anda hem çok özellikli, hem hızlı, hem de küçük bir kütüphane yapamazsınız
Bunun fmt’ye özgü bir şey olmaktan ziyade, kamusal ortamda yapılan genel bir yakınmadan ne farkı var pek emin değilim
Dragonbox veya Dragon4 gibi algoritma kodları tek başına bile boyut bütçesini aştığı için “isteğe bağlı” özellikler çok da önemli değil
Üstelik bu, insanların istediği yaklaşık 20 özellikten yalnızca biri
Böylece başkaları daha fazla özelliği daha akıllıca sığdırmanın yollarını bulabilir
Aksi halde ne anlatılmak istendiğini pek anlamıyorum
Bu gereksinimler geçerli, ama bunları dil spesifikasyonu değil, en düşük seviye mikrodenetleyici derleyicisi çözmeli
Temel özellikleri bile desteklememek pahasına aşırı küçük olması gerekiyorsa, kesinlikle daha iyi seçenekler var
Kod alanı yalnızca 2KB ise bunu kullanmamalısınız
Neyse ki modern mikrodenetleyicilerin çoğu bundan çok daha büyük; örneğin esp32 1MB’tan başladığı için 14KB’lik bir biçimlendirme kütüphanesi kullanmak gayet makul
Biraz tanıtım yapacak olursam, çıktı buffering’i olan libc dahil olsa bile 1008 baytlık yürütülebilir dosya ile
printf(Hello, World!\n");yapılabiliyor: https://github.com/pts/minilibc686Elbette doğrudan kıyaslarsak elmayla armudu karşılaştırmış oluruz
“Boş main fonksiyonu olan bir C programı bu sistemde 6kB ise, {fmt} artık ikili dosyaya 10kB’den az ekliyor” kısmı ilginç
Böyle bir test hiç yapmadım
Hangi C kütüphanesini kullandığınız da önemlidir; ELF mi yoksa başka bir container mı kullandığınız da biraz etkiler
Sorun her zaman fmt
Yeterince çok sayıyla, özellikle kayan nokta ve decimal biçimlendirme/ayrıştırma ile uğraşınca linker’ın kayan nokta ve BigInt ile ilgili çok kodu çekip ikili boyutunu büyütmesinin artık .NET’te de aynı şekilde yaşanması çok komik
Çok ilginç
Bu tür bakış açısını değiştiren optimizasyonları seviyorum
Ben mi yavaşım bilmiyorum ama başlıktaki “14k”nin 14kB anlamına geldiğini fark etmem biraz zaman aldı
En azından tarihsel olarak k, kB’nin yaygın bir kısaltmasıdır