· kaynak Hacker News – Front Page (native)
Go 1.27, özelleştirilmiş malloc yollarıyla küçük boyutlu bellek ayırmalarını %30'a kadar hızlandırıyor
Go 1.27, tek tip genel ayırma yolunu 80 bayt ve altındaki talepler için özelleştirilmiş işlevlerle değiştiriyor; ayırma maliyetini %20-30 azaltıyor ve ayırma yoğun programlarda toplam performansı %1'e kadar artırıyor.

Resmi Go blogundaki bir yazıya göre Go 1.27, runtime içindeki ayırma yolunu yeniden düzenleyerek 80 bayt ve daha küçük ayırmaları %20-30'a kadar hızlandırıyor; ayırma yoğun programlar ise genel olarak %1'e kadar kazanç sağlıyor. Bu değişiklik programcılara görünmez durumda: yeni bir API ya da build bayrağı yok, ancak dilin en yoğun kullanılan kod yollarından birine dokunuyor.
Önceden ayırma nasıl çalışıyordu
Go'daki her heap ayırması, mallocgc adlı bir runtime işlevinden geçer; bu işlevin iki bilgiye ihtiyacı vardır: talebin boyutu ve nesnenin işaretçi (pointer) içerip içermediği. Derleyici bu çağrıların çoğunu newobject içinde sarar; newobject, bu bilgileri tip bilgisinden çıkaran ve ileten ince bir ara katmandır.
Ayırıcı (allocator), boyutları 17-24 bayt veya 65-80 bayt gibi size class adı verilen kovalara ayırır ve talepleri kovanın üst sınırına göre boyutlandırılmış bloklardan oluşan boş listelerden karşılar. Span adı verilen bu boş listeler, çöp toplayıcının her biri için farklı kayıt tutması gerektiğinden, işaretçi içeren ve içermeyen nesneler için ayrı tutulur. Size class ve işaretçi durumunun birleşimi bir span class içinde kodlanır ve bu değer, ayırıcının kararlarının çoğunu belirler.
Her span class için bir özelleştirilmiş işlev
Go 1.27, tek bir genel amaçlı mallocgc yerine, 80 bayta kadar her span class için ayrı bir varyant, ayrıca küçük (tiny) ayırmalar için bir varyant üretiyor. Örneğin 17-24 bayt arası işaretçi içermeyen nesneleri işleyen varyantın adı mallocgcSmallNoScanSC3.
Her varyant tam olarak bir boyut kovası ve işaretçi yapılandırmasıyla ilgilendiğinden, genel yolun yapmak zorunda olduğu işleri atlayabiliyor: span class hesaplaması, işaretçi durumuna göre dallanma ve bellek temizleme yardımcılarına yapılan çağrılar. Özelleştirilmiş bir işlev üstesinden gelemediği bir durumla karşılaştığında — aktif bir çöp toplama bunun örneklerinden biridir — genel rutine geri düşüyor.
Ayırma boyutu derleme zamanında bilindiğinde derleyici bu varyantları doğrudan çağırabiliyor. Bilinmediğinde, örneğin dinamik uzunluklu slice'larda, mallocgc yerinde kalıyor ve çalışma zamanında doğru özelleştirilmiş işleve yönlendirme yapıyor. Bu, özelleştirilmiş kodun dinamik yönlendirmenin maliyetini ödedikten sonra bile kazanacak kadar hızlı olması gerektiği anlamına geliyor.
Sınırın 80 bayt olmasının nedeni
Varyant eklemek bedelsiz değil. Her biri ikili dosyayı büyütüyor ve daha da önemlisi instruction cache için rekabet yaratıyor. mallocgc, ayırmanın çok sık gerçekleşmesi nedeniyle neredeyse her zaman icache'te yer kaplıyor; kalabalıklaşan özelleştirilmiş işlevler birbirini dışlama ve kullanıcı kodunu sıkıştırma riski taşıyor, bu da kazançları siliyor. Go ekibi farklı size class'larda sınırları ölçtü ve 80 baytın en uygun nokta olduğunu tespit etti. Boyutlar büyüdükçe kazanç da küçülüyor, çünkü belleği sıfırlama işlemi ayırmaya hâkim oluyor ve çevresindeki işler ihmal edilebilir hale geliyor.
Elle yazılmış kopyalar değil, üretilmiş kod
Birbirine neredeyse aynı onlarca elle yazılmış işlev zamanla uyumsuz hale gelirdi. Bunun yerine paylaşılan mantık, çoğunlukla taslak görevi gören sıradan, tip denetimli Go kodunda yaşıyor ve standart kütüphanenin go/ast paketi ile golang.org/x/tools/go/ast/astutil paketi üzerine kurulu bir inliner, parçaları her varyanta birleştiriyor. Bu yaklaşım, ekibin derleyicinin normalde atlayacağı yardımcıları agresif biçimde inline etmesini (derleyici, çok büyük bulduğu işlevleri inline etmeyi reddettiğinden) ve runtime hata ayıklama bayrakları gibi nadir durumları slow-path işlevlerine taşıyarak sıcak kodun küçük kalmasını sağlamasını mümkün kılıyor.
Yazıya göre en büyük kazanç bellek temizlemeden geliyor. Yeni ayrılan bellek çoğu zaman sıfırlanmalı ve bu temizleme işlemi ayırma süresinin büyük kısmını kaplayabiliyor. Özelleştirilmiş bir işlevde temizleme boyutu derleme zamanı sabiti olduğundan, üretilen kod temizleme talimatlarını memclrNoHeapPointers yardımcısını çağırmak yerine doğrudan üretebiliyor; böylece bir işlev çağrısı ve birkaç dallanma atlanıyor. Bu fark en çok çok küçük ayırmalarda belirgin oluyor ve boyut arttıkça kayboluyor. Sabit boyutlar ayrıca derleyicinin, ayrılan bellekte işaretçilerin nerede olduğunu işaretleme gibi kayıt işlerini hızlandırmasını sağlıyor.
Neden önemli
Ayırma hızı her Go programının ödediği bir vergidir ve küçük nesneler iş yüküne hâkimdir: yazı, 16 ve 24 baytlık ayırmaları en çok yararlanan özellikle yaygın boyutlar olarak vurguluyor. Ayırma maliyetinde %20-30 azalmanın uçtan uca %1'e kadar iyileşmeye dönüşmesi, olgun bir runtime için anlamlı ve çabasız bir yükseltme. Ham rakamların ötesinde, bu çalışma runtime'ların kod üretimi yoluyla ikili dosya boyutu ve instruction cache baskısını çağrı başına hızla takas edebileceğine dair bir örnek olay oluşturuyor ve diğer sıcak runtime yollarının gelecekteki sürümlerde izleyebileceği bir kalıp ortaya koyuyor.
- #go
- #runtime
- #performance
- #memory-management
- #compilers