Padding
Derleyicinin bazen kullanıcı tanımlı tiplerimize fazladan bayt, yani dolgu eklemesi gerekir. Bir sınıf veya struct içinde veri üyeleri tanımladığımızda, derleyici üyeleri tanımladığımız sıraya göre yerleştirmek zorunda kalır.
Ancak, derleyici aynı zamanda sınıf içindeki veri üyelerinin doğru hizalamaya sahip olduğundan emin olmak zorundadır; bu nedenle, gerekirse veri üyeleri arasına dolgu eklemesi gerekir. Örneğin, aşağıdaki gibi tanımlanmış bir class’ımız olduğunu varsayalım:
class Document {
bool is_cached_{};
double rank_{};
int id_{};
};
std::cout << sizeof(Document) << '\n'; // Possible output is 24
Olası çıktının 24 olmasının nedeni, derleyicinin tek tek veri üyelerinin ve tüm class’ın hizalama gereksinimlerini karşılamak için bool ve int’ten sonra dolgu eklemesidir. Derleyici, Document class’ı aşağıdaki gibi bir şeye dönüştürür:
class Document {
bool is_cached_{};
std::byte padding1[7]; // Invisible padding inserted by compiler
double rank_{};
int id_{};
std::byte padding2[4]; // Invisible padding inserted by compiler
};
Bool ve double arasındaki ilk dolgu 7 bayttır, çünkü double türünün rank_ data üyesi 8 baytlık bir hizalamaya sahiptir. int’ten sonra eklenen ikinci dolgu 4 bayttır. Bu, Document class’ın kendi hizalama gereksinimlerini karşılamak için gereklidir. En büyük hizalama gereksinimine sahip üye, tüm veri yapısı için hizalama gereksinimini de belirler. Örneğimizde bu, 8 baytlık hizalanmış bir çift değer içerdiğinden, Document class’ın toplam boyutunun 8’in katı olması gerektiği anlamına gelir.
Artık Document class’taki veri üyelerinin sırasını, en büyük hizalama gereksinimlerine sahip türlerden başlayarak derleyici tarafından eklenen dolguyu en aza indirecek şekilde yeniden düzenleyebileceğimizi fark ettik. Document class’ın yeni bir versiyonunu oluşturalım:
// Version 2 of Document class
class Document {
double rank_{}; // Rearranged data members
int id_{};
bool is_cached_{};
};
Üyelerin yeniden düzenlenmesiyle, derleyicinin artık Document’ın hizalamasını ayarlamak için yalnızca is_cached_ data üyesinden sonra dolgu yapması gerekiyor. Dolgu işleminden sonra class bu şekilde görünecektir:
// Version 2 of Document class after padding
class Document {
double rank_{};
int id_{};
bool is_cached_{};
std::byte padding[3]; // Invisible padding inserted by compiler
};
Yeni Document class’ının boyutu, 24 byte olan ilk versiyona kıyasla artık sadece 16 byte’tır. Buradan çıkarılacak sonuç, bir nesnenin boyutunun sadece üyelerinin bildirilme sırasını değiştirerek değişebileceğidir. Bunu, güncellenmiş Document versiyonumuz üzerinde sizeof operatörünü tekrar kullanarak da doğrulayabiliriz:
std::cout << sizeof(Document) << '\n'; // Possible output is 16
Aşağıdaki resim Document class’ın 1. ve 2. sürümlerinin bellek düzenini göstermektedir:
Genel bir kural olarak, en büyük veri üyelerini en başa ve en küçük üyeleri en sona yerleştirebilirsiniz. Bu şekilde, dolgunun neden olduğu bellek yükünü en aza indirebilirsiniz. Daha sonra, oluşturduğumuz nesnelerin hizalamasını bilmeden önce, nesneleri ayırdığımız bellek bölgelerine yerleştirirken hizalama hakkında düşünmemiz gerektiğini göreceğiz.
Performans açısından bakıldığında, bir nesnenin yayıldığı önbellek satırlarının sayısını en aza indirmek için nesneleri önbellek satırlarına hizalamak istediğiniz durumlar da olabilir. Önbellek dostu olma konusuna değinmişken, sık kullanılan birden fazla veri üyesini yan yana yerleştirmenin faydalı olabileceğinden de bahsetmek gerekir.
Veri yapılarınızı kompakt tutmak performans açısından önemlidir. Birçok uygulama bellek erişim süresine bağlıdır. Bellek yönetiminin bir diğer önemli yönü de artık ihtiyaç duyulmayan nesneler için asla bellek sızdırmamak veya israf etmemektir. Kaynakların sahipliği konusunda açık ve net olarak her türlü kaynak sızıntısını etkili bir şekilde önleyebiliriz. Bu, bir sonraki bölümün konusudur.
Memory Ownership
Kaynakların sahipliği, programlama yaparken göz önünde bulundurulması gereken temel bir husustur. Bir kaynağın sahibi, artık ihtiyaç duyulmadığında kaynağı serbest bırakmaktan sorumludur. Bir kaynak tipik olarak bir bellek bloğudur, ancak bir veritabanı bağlantısı, bir dosya tanıtıcısı vb. de olabilir. Sahiplik, hangi programlama dilini kullandığınızdan bağımsız olarak önemlidir. Ancak, dinamik bellek varsayılan olarak çöp toplamadığı için C ve C++ gibi dillerde daha belirgindir. C++’da ne zaman dinamik bellek ayırsak, bu belleğin sahipliğini düşünmemiz gerekir. Neyse ki, bu bölümde daha sonra ele alacağımız akıllı işaretçileri kullanarak çeşitli sahiplik türlerini ifade etmek için dilde artık çok iyi bir destek var.
Standart kütüphanedeki akıllı işaretçiler dinamik değişkenlerin sahipliğini belirtmemize yardımcı olur. Diğer değişken türlerinin zaten tanımlanmış bir sahipliği vardır. Örneğin, yerel değişkenler geçerli kapsama aittir. Kapsam sona erdiğinde, kapsam içinde yaratılmış olan nesneler otomatik olarak yok edilir:
{
auto user = User{};
} // user automatically destroys when it goes out of scope
Statik ve global değişkenler program tarafından sahiplenilir ve program sonlandırıldığında yok edilir:
static auto user = User{};
Veri üyeleri, ait oldukları class’ın instance’ları tarafından sahiplenilir:
class Game
{
User user; // A Game object owns the User object
// ...
};
Varsayılan bir sahibi olmayanlar yalnızca dinamik değişkenlerdir ve değişkenlerin yaşam süresini kontrol etmek için dinamik olarak tahsis edilen tüm değişkenlerin bir sahibi olduğundan emin olmak programcıya bağlıdır:
auto* user = new User{}; // Who owns user now?
Modern C++ ile kodlarımızın çoğunu new ve delete’e açık çağrılar yapmadan yazabiliriz ki bu harika bir şeydir. new ve delete çağrılarını manuel olarak takip etmek çok kolay bir şekilde bellek sızıntıları, çift silme ve diğer kötü hatalarla sonuçlanan bir sorun haline gelebilir. Ham pointer’lar herhangi bir sahiplik ifade etmez, bu da dinamik belleğe başvurmak için sadece ham pointer kullanıyorsak sahipliğin izlenmesini zorlaştırır.
Sahipliği açık ve net hale getirmenizi, ancak manuel bellek yönetimini en aza indirmeye çalışmanızı öneririm. Bellek sahipliğini ele almak için oldukça basit birkaç kuralı takip ederek, kaynak sızdırmadan kodunuzu temiz ve doğru hale getirme olasılığını artıracaksınız. İlerleyen bölümler, bu amaca yönelik bazı en iyi uygulamalar konusunda size rehberlik edecektir.
Handling Resources Implicitly
İlk olarak, nesnelerinizin dinamik bellek allocation/deallocation işlemlerini dolaylı olarak gerçekleştirmesini sağlayın:
auto func()
{
auto v = std::vector{1, 2, 3, 4, 5};
}
Yukarıdaki örnekte, hem stack hem de dinamik bellek kullanıyoruz, ancak açıkça new ve delete çağırmamız gerekmiyor. Yarattığımız std::vector nesnesi stack üzerinde yaşayacak otomatik bir nesnedir. Kapsam tarafından sahiplenildiği için, fonksiyon geri döndüğünde otomatik olarak yok edilecektir. std::vector nesnesinin kendisi tamsayı elemanlarını saklamak için dinamik bellek kullanır. v kapsam dışına çıktığında, destructor dinamik belleği güvenli bir şekilde serbest bırakabilir. Yıkıcıların dinamik belleği serbest bırakmasına izin veren bu model, bellek sızıntılarını önlemeyi oldukça kolaylaştırır.
Hazır kaynakları serbest bırakma konusuna girmişken, RAII’den bahsetmek mantıklı olacaktır. RAII, Resource Acquisition Is Initialization’ın kısaltması olan ve bir kaynağın yaşam süresinin bir nesnenin yaşam süresi tarafından kontrol edildiği iyi bilinen bir C++ tekniğidir. Bu model basittir ancak kaynakların (bellek dahil) kullanımı için son derece kullanışlıdır. Ancak, bir değişiklik için, ihtiyacımız olan kaynağın istek göndermek için bir tür bağlantı olduğunu varsayalım. Bağlantıyı kullanmayı bitirdiğimizde, biz (sahipler) onu kapatmayı unutmamalıyız. İşte bir istek göndermek için bağlantıyı manuel olarak açıp kapattığımızda nasıl göründüğüne dair bir örnek:
auto send_request(const std::string& request)
{
auto connection = open_connection("http://www.example.com/");
send_request(connection, request);
close(connection);
}
Gördüğünüz gibi, bağlantıyı kullandıktan sonra kapatmayı unutmamalıyız, aksi takdirde bağlantı açık kalacaktır (sızıntı). Bu örnekte, unutmak zor görünmektedir, ancak uygun hata işleme ve birden fazla çıkış yolu ekledikten sonra kod daha karmaşık hale geldiğinde, bağlantının her zaman kapatılacağını garanti etmek zor olacaktır. RAII bunu, otomatik değişkenlerin yaşam süresinin bizim için öngörülebilir bir şekilde ele alındığı gerçeğine güvenerek çözer. İhtiyacımız olan şey, open_connection() çağrısından elde ettiğimiz bağlantıyla aynı ömre sahip olacak bir nesnedir. Bunun için RAIIConnection adında bir sınıf oluşturabiliriz:
class RAIIConnection
{
public:
explicit RAIIConnection(const std::string& url)
: connection_{open_connection(url)} {}
~RAIIConnection()
{
try {
close(connection_);
}
catch (const std::exception&) {
// Handle error, but never throw from a destructor
}
}
auto& get() { return connection_; }
private:
Connection connection_;
};
Connection nesnesi artık bağlantının (kaynağın) ömrünü kontrol eden bir sınıfa sarılmıştır. Bağlantıyı manuel olarak kapatmak yerine, artık RAIIConnection’ın bunu bizim için halletmesine izin verebiliriz:
auto send_request(const std::string& request)
{
auto connection = RAIIConnection("http://www.example.com/");
send_request(connection.get(), request);
// No need to close the connection, it is automatically handled
// by the RAIIConnection destructor
}
RAII kodumuzu daha güvenli hale getirir. Burada send_request() bir istisna fırlatsa bile, bağlantı nesnesi yine de yok edilecek ve bağlantı kapatılacaktır. RAII’yi yalnızca bellek, dosya tanıtıcıları ve bağlantılar için değil, birçok kaynak türü için kullanabiliriz. Bir başka örnek de C++ standart kütüphanesinden std::scoped_lock’tur. Oluşturma sırasında bir kilit (mutex) edinmeye çalışır ve ardından yok etme sırasında kilidi serbest bırakır. std::scoped_lock hakkında daha fazla bilgiyi Concurrency araştırarak öğrenebilirsiniz.
Şimdi, C++’da bellek sahipliğini açık hale getirmenin daha fazla yolunu keşfedeceğiz.