İçeriğe geç
academia.sh

Ders 11 / 12

Derleme Süresi Yönetimi

İleri bildirim, gereksiz include azaltma, gizleme deyimi (pimpl), opak tip, açık somutlaştırma ve extern template ile derlemeden kazanılan bedelin nesne başına koşuma nasıl taşındığı.

Yayın Güncelleme

Bu derste

  • İleri bildirimin ve gereksiz include azaltmanın bir başlığı kaç çeviri birimine görünür kıldığını açıklar.
  • Gizleme deyimiyle (pimpl) özel üyeleri bir ileri bildirimin arkasına saklar.
  • Gizleme deyiminin yıkıcıyı ve taşıma işlemlerini neden .cpp dosyasında tanımlamayı gerektirdiğini açıklar.
  • Derlemeden kazanılan bedelin koşuma nasıl taşındığını ayırma sayısıyla gösterir.
İçindekiler

Önceki ders, profilin sıcak noktayı bulduğunu ve ölçümün nerede başladığının sonucu değiştirdiğini gösterdi. Ama hiçbir sayaç yakalayamayan bir bedel daha var: programı koşmadan önce ödenen derleme bedeli. Bir başlık dosyası değiştiğinde, o başlığı doğrudan ya da dolaylı olarak içeren her çeviri birimi yeniden derlenir. Başlık ne kadar çok birime görünüyorsa değişiklik o kadar pahalıdır. Bu ders o görünürlüğü azaltmanın yollarını inceliyor ve bir yolun, gizleme deyiminin (pimpl), kazandığı derleme bedelini koşumda nereye ödettiğini ölçüyor. Cevabın sınıfı koşumdur. M11/K05’ten bir ayrım: orada taşınmış nesnenin durumu ölçü değildi; burada da taşınmış nesnenin durumuna bakmıyoruz, yalnızca taşımanın bir ayırma yapıp yapmadığını sayıyoruz. Derleme süresi basılmaz; bağımlılık “bu başlığı kaç birim görüyor” diye anlatılır, ölçülen sayı ise nesne başına ayırmadır.

Başlığın Görünürlüğünü Azaltmak

İlk araç ileri bildirimdir. Bir başlık başka bir tipi yalnızca işaretçi ya da referans olarak kullanıyorsa, o tipin tam tanımına ihtiyaç duymaz; class Pencere; yazmak yeter. Tam tanımı içeren başlığı #include etmek, o başlığın değiştiği her seferde bu başlığı da yeniden derletir. İleri bildirim bu zinciri keser: tam tanım yalnızca onu gerçekten kullanan .cpp dosyasında içerilir.

İkinci araç, gereksiz #include satırlarını silmektir. Bir başlığa eklenen satır, o başlığı içeren bütün birimlerin ödediği bir bedeldir; kullanılmayan bir satır da aynı bedeli ödetir. Üçüncü araç şablonlara özgüdür: sık kullanılan bir şablon özelleşmesi her birimde yeniden somutlaştırılıyorsa, açık somutlaştırma ve extern template bildirimi bunu tek birime toplar. Başlıkta extern template class Kutu<int>; yazılır, tek bir .cpp dosyasında template class Kutu<int>; ile somutlaştırma yapılır; öteki birimler satır içi olmayan üyeleri yeniden üretmek yerine o birimdeki tanıma bağlanır. Bu araç için aşağıda küçük bir örnek var; etkisi koşum çıktısında görünmez, bu yüzden ölçülmez.

İleri Bildirim Bir Başlığı Neden Hafifletir

İleri bildirimin işini bir örnekle görelim. Bir çizim işlevi Pencere tipini parametre olarak alıyor. İşlevin bildirimi için tipin tam tanımı gerekmez: tamamlanmamış bir tip bir işlev bildiriminde parametre olabilir, ister referans, ister değer olarak. Tam tanım yalnızca işlevin tanımında ve işlevin çağrıldığı yerde gerekir. Çizim başlığı bu yüzden yalnızca class Pencere; yazar ve pencere.hpp’yi hiç içermez; pencere.hpp yalnızca cizim.cpp içinde görünür.

cizim.hpp’yi içeren bir birim Pencere’nin içini görmez; Pencere’nin üyeleri değişse de o birim yeniden derlenmez. Birim yalnızca bir Pencere nesnesi yaratıyor ya da üyesini çağırıyorsa tam tanımı kendisi içerir.

// cizim.hpp — Pencere yalniz ileri bildirimle gorunur
#pragma once

class Pencere;

int ciz(const Pencere& p);
int ciz_kopya(Pencere p);
// cizim.cpp — tam tanim yalniz burada icerilir
#include "cizim.hpp"
#include "pencere.hpp"

int ciz(const Pencere& p) { return p.genislik(); }
int ciz_kopya(Pencere p) { return p.genislik(); }

İki işlev aynı genişliği döndürür, ama parametre geçiş biçimi farklıdır. ciz referans alır ve yeni bir Pencere kurmaz. ciz_kopya değer alır: çağrı sırasında bir kopya kurulur ve gizleme deyimli sınıfın kopyası bir öbek ayırması yapar. Başlık aynı ileri bildirimle iki işlevi de gösterir; bedel parametre biçimine bağlıdır, başlığa değil.

Açık Somutlaştırma ve extern template

Şablonlar için ileri bildirim yetmez: şablonun gövdesi başlıkta durmak zorundadır, çünkü derleyici her kullanım yerinde tipi yerine koyar. Bir şablon çok sayıda birimde aynı tiple kullanılırsa, her birim aynı somutlaştırmayı kendi içinde üretir ve bağlayıcı kopyalardan birini seçer. Çalışan program doğrudur, ama bu üretim her birimin derleme süresine yazılır.

extern template bu tekrarı keser. Başlıkta extern template class Kutu<int>; yazılırsa, birimler Kutu<int>’in satır içi olmayan üyelerini kendi içlerinde somutlaştırmaz; tanımı başka bir birimde bekler. Sınıf gövdesinde tanımlanan üyeler örtük olarak satır içidir ve bildirim onları kapsamaz: derleyici bu üyeleri satır içine almak için yine üretebilir. Kazanç, gövdesi dışarıda tanımlanan üyelerde büyür; aşağıdaki Kutu’nun üyeleri sınıf gövdesinde tanımlı olduğu için örnek kazancı değil, yazımı gösterir. Tek bir .cpp dosyasında template class Kutu<int>; ile açık somutlaştırma yapılır ve tanımın tek kopyası orada üretilir. Etki derleme süresinde görünür; çıktıda görünmez. Aşağıdaki program yalnızca çift bildirimin derlendiğini ve bağlandığını doğrular.

// kutu.hpp — sablon govdesi basliktadir, int ozellesmesi disaridan gelir
#pragma once

template <typename T>
class Kutu {
public:
    explicit Kutu(T deger) : deger_(deger) {}
    T al() const { return deger_; }

private:
    T deger_;
};

extern template class Kutu<int>;
// kutu.cpp — Kutu<int> somutlastirmasinin tek tanimi
#include "kutu.hpp"

template class Kutu<int>;

Gizleme Deyimi

Dördüncü araç sınıfın özel üyelerini de başlıktan çıkarır. Bir sınıfın özel alanları başlıkta görünür, çünkü derleyici nesnenin boyutunu bilmek zorundadır. Bir özel alanın tipi değişirse, sınıfı içeren her birim yeniden derlenir. Gizleme deyimi bu alanları Ic adlı iç bir yapıya taşır, başlıkta yalnızca onun ileri bildirimini ve bir std::unique_ptr<Ic> tutar. Ic bu noktada opak tiptir: başlığı gören birim onun var olduğunu bilir, içini bilmez.

Aşağıdaki başlık yalnızca ileri bildirimi ve işaretçiyi taşır. Yıkıcının ve taşıma işlemlerinin burada yalnızca bildirildiğine dikkat edin; tanımları .cpp dosyasındadır.

// pencere.hpp — yalniz ileri bildirim ve unique_ptr
#pragma once
#include <memory>

class Pencere {
public:
    Pencere();
    ~Pencere();
    Pencere(Pencere&&) noexcept;
    Pencere& operator=(Pencere&&) noexcept;
    Pencere(const Pencere&);
    Pencere& operator=(const Pencere&);

    int genislik() const;
    void genislik_ata(int yeni);

    static int nesne_sayisi();
    static int ayirma_sayisi();

private:
    struct Ic;
    std::unique_ptr<Ic> ic_;
};

std::unique_ptr<Ic> yıkılırken Ic’nin yıkıcısını çağırır; bunun için Ic’nin tam tanımını görmek zorundadır. Başlıkta Ic yalnızca ileri bildirimle durduğundan, yıkıcıyı başlıkta örtük bırakmak ya da = default yazmak derleyiciyi Ic tamamlanmamış tipken yıkıcıyı üretmeye çalıştırır ve derleme hata verir. Bu yüzden yıkıcı ve taşıma işlemleri yalnızca bildirilir, Ic’nin tanımlandığı .cpp dosyasında = default ile tanımlanır.

// pencere.cpp — Pencere gerceklemesi, Ic burada tanimli
#include "pencere.hpp"

namespace {
int nesne = 0;
int ayirma = 0;
}  // namespace

struct Pencere::Ic {
    int genislik = 0;
};

Pencere::Pencere() : ic_(std::make_unique<Ic>()) {
    ++nesne;
    ++ayirma;
}

Pencere::~Pencere() = default;
Pencere::Pencere(Pencere&&) noexcept = default;
Pencere& Pencere::operator=(Pencere&&) noexcept = default;

Pencere::Pencere(const Pencere& diger) : ic_(std::make_unique<Ic>(*diger.ic_)) {
    ++ayirma;
}

Pencere& Pencere::operator=(const Pencere& diger) {
    if (this != &diger) *ic_ = *diger.ic_;
    return *this;
}

int Pencere::genislik() const { return ic_->genislik; }
void Pencere::genislik_ata(int yeni) { ic_->genislik = yeni; }
int Pencere::nesne_sayisi() { return nesne; }
int Pencere::ayirma_sayisi() { return ayirma; }

Ic’ye bir alan eklenirse ya da genislik’in tipi değişirse yalnızca pencere.cpp yeniden derlenir. pencere.hpp olduğu gibi kalır, onu içeren hiçbir birim yeniden derlenmez. Başlık değişmeden gerçeklemenin değişebilmesi, gizleme deyiminin kazandığı derleme bedelidir.

Yıkıcı Neden .cpp Dosyasındadır

Bu düzenin en sık yapılan hatası yıkıcıyı başlıkta bırakmaktır. Kural basittir: std::unique_ptr<T> yıkılırken delete çağrılır ve delete için T’nin tam tanımı gerekir. Başlıkta Pencere’nin yıkıcısı yazılmamış ya da = default olarak yazılmışsa derleyici onu Pencere nesnesini yıkan her birimde üretmeye çalışır. O birimlerde Ic hâlâ tamamlanmamış bir tiptir ve delete geçersiz olur. Çözüm, yıkıcıyı başlıkta yalnızca bildirmek, Ic’nin tam göründüğü .cpp dosyasında = default ile tanımlamaktır. Taşıyan atamada da aynı gerekçe vardır: atama eski unique_ptr’yi yıkmak zorundadır. Taşıyan yapıcı da .cpp dosyasında tanımlanır, çünkü kurulum yarıda kalırsa kurulmuş üyeleri yıkan kod onda da üretilir.

Hata mesajını burada alıntılamıyoruz ve koşturmuyoruz; derleyiciden derleyiciye değişir. Bilinmesi gereken, hatanın kullanıcı birimde çıktığıdır: yıkıcının yazıldığı yerde değil, onu çağıran yerde. Bu, hatayı bulmayı zorlaştırır.

std::shared_ptr ile karşılaştırınca fark görülür. shared_ptr silicisini nesne kurulurken yakalar; tamamlanmamış bir tip için de yıkıcı başlıkta yazılabilir, çünkü silici kurulumun yapıldığı birimde, tip tam görünürken oluşur. unique_ptr’de silici tipin parçasıdır ve yıkıldığı yerde üretilir. shared_ptr, bu sayede başlıktaki yıkıcı kuralından kurtulur, ama karşılığında bir kontrol bloğu taşır: std::make_shared onu nesneyle tek ayırmada kurar, ayrı kurulan bir shared_ptr ise onun için ikinci bir ayırma öder. Varsayılan silicili unique_ptr ise yaygın gerçeklemelerde tek bir işaretçi boyundadır. Bu derste unique_ptr seçildi ve ayırma sayısı onun üzerinden okunur.

Bu Başlığı Kaç Birim Görüyor

Derleme maliyeti, bir başlığın kaç çeviri birimine göründüğüyle ölçülür. Bunu çıktıdan okuyamayız, projenin kurulumundan okuruz; bu yüzden sayıyı bir sayım örneği olarak veriyoruz, ölçüm olarak değil. Beş çeviri biriminin bulunduğu varsayımsal bir proje düşünelim; birimlerin hepsi pencere.hpp’yi içeriyor. Ic içindeki bir alanın tipi değişince hangi birimler yeniden derlenir?

Düzen pencere.hpp’yi içeren birim Ic değişince yeniden derlenen birim
Özel alanlar başlıkta 5 5
Gizleme deyimi, Ic .cpp’de 5 1

Tablodaki sayılar bu varsayımsal projenin kurulumundan çıkar. İkinci satırda yeniden derlenen tek birim Ic’yi tanımlayan pencere.cpp’dir. Aynı mantıkla cizim.hpp’nin pencere.hpp’yi içermesi, cizim.hpp’yi içeren her birimi pencere.hpp’ye de bağlardı; ileri bildirim bu dolaylı bağlantıyı keser. Dolaylı içermeler bu sayıyı sessizce büyütür: bir başlık, kendisini içeren başlıkların içerdiği her şeyi de getirir.

Bu yüzden bir başlığa #include eklemeden önce iki şey kontrol edilir. Eklenen tip başlıkta gerçekten tam tanımla mı gerekiyor, yoksa ileri bildirim yeterli mi? Ve bu başlığı kaç başlık, onlar da kaç birim içeriyor? Zincirin uzunluğu bu soruların yanıtından çıkar; derleyici bu sayımı bizim yerimize yapıp uyarmaz, çünkü program doğru derlenir ve yalnızca daha çok iş yapılır.

Ölçüm: Nesne Başına Ayırma

// pencere_olcumu.cpp — gizleme deyiminin nesne basina ayirmasi
#include <iostream>
#include <utility>

#include "cizim.hpp"
#include "kutu.hpp"
#include "pencere.hpp"

int main() {
    Pencere a;
    Pencere b;
    Pencere c;
    a.genislik_ata(640);
    b.genislik_ata(480);
    std::cout << "gizleme deyimi kosum nesne=" << Pencere::nesne_sayisi()
              << " ayirma=" << Pencere::ayirma_sayisi() << "\n";

    int once = Pencere::ayirma_sayisi();
    Pencere tasinan(std::move(a));
    int tasima = Pencere::ayirma_sayisi() - once;

    once = Pencere::ayirma_sayisi();
    Pencere kopya(b);
    int kopyalama = Pencere::ayirma_sayisi() - once;

    std::cout << "gizleme deyimli nesne kosum tasima=" << tasima
              << " kopya=" << kopyalama << "\n";
    std::cout << "genislik tasinan=" << tasinan.genislik()
              << " kopya=" << kopya.genislik() << "\n";

    int baslangic = Pencere::ayirma_sayisi();
    int g1 = ciz(b);
    int referans = Pencere::ayirma_sayisi() - baslangic;
    baslangic = Pencere::ayirma_sayisi();
    int g2 = ciz_kopya(b);
    int deger = Pencere::ayirma_sayisi() - baslangic;
    std::cout << "ileri bildirim kosum referans=" << referans << " deger=" << deger
              << " genislik=" << g1 + g2 << "\n";

    Kutu<int> kutu(7);
    std::cout << "acik somutlastirma kosum kutu=" << kutu.al() << "\n";
    return 0;
}
gizleme deyimi kosum nesne=3 ayirma=3
gizleme deyimli nesne kosum tasima=0 kopya=1
genislik tasinan=640 kopya=480
ileri bildirim kosum referans=0 deger=1 genislik=960
acik somutlastirma kosum kutu=7

EB24. İlk satır, üç Pencere nesnesinin her birinin kuruluşta bir Ic ayırdığını gösterir: nesne=3, ayirma=3. Gizleme deyimi, başlığın görünürlüğünü azaltmanın bedelini her nesnede bir öbek ayırması olarak ödettirir; iç yapı artık nesnenin içinde değil, ayrı bir bellek bloğundadır.

EB25. Üçüncü satır iki nesnenin doğru değerleri taşıdığını doğrular: taşınan nesne 640, kopya 480 genişliğini aldı. Taşınmış a nesnesinin durumuna bakılmadı; bu ders onun durumunu değil, taşımanın ayırma yapıp yapmadığını ölçüyor.

EB26. Dördüncü satır, ileri bildirimle gösterilen Pencere’nin referansla geçişte hiç ayırma yapmadığını (referans=0), değerle geçişte bir ayırma yaptığını (deger=1) gösterir. Genişlik toplamı 960, iki çağrının da 480 değerini okuduğunu doğrular. Başlık iki işlevi de aynı ileri bildirimle taşıdı; bedel başlıktan değil, çağrı biçiminden geldi.

EB27. Beşinci satır kutu=7 basar. extern template bildirimi ve template class tanımı birlikte derlendi ve bağlandı; çıktı tek bir somutlaştırma olduğunu göstermez, yalnızca çift bildirimin geçerli olduğunu gösterir. Derleme süresindeki etki bu çıktıdan okunmaz.

EB28. Tablodaki beşe karşı bire hiçbir çıktı doğrulamıyor; bu, projenin kurulumundan çıkan bir sayım örneğidir. Doğruluğu varsayıma bağlıdır: beş birimin hepsinin pencere.hpp’yi içerdiği ve yalnızca birinin Ic’yi tanımladığı. Gerçek bir projede dolaylı içermeler bu sayıyı değiştirir.

EB29. İkinci satırdaki tasima=0, taşıyan yapıcının .cpp dosyasında = default ile tanımlanmasına bağlıdır: Ic tam tanımlıyken taşıma yalnızca unique_ptr’yi devralır. Aynı yapıcı başlıkta = default ile tanımlansaydı Ic tamamlanmamış olduğu için sorun çıkabilirdi. Bu yüzden ölçüm yalnızca kuralı uygulayan bir düzende anlamlıdır.

Sınırlayıcı Ölçüm

Bu dersin ölçtüğü düzenek, derlemeden kazanılan bedeli nesne başına koşuma taşımaktır. İkinci satır bu bedelin ne zaman yeniden ödendiğini gösterir. Gizleme deyimli bir nesneyi taşımak yeni ayırma yapmaz: tasima=0; çünkü taşıma yalnızca unique_ptr’nin işaretçisini devralır, Ic yerinde kalır. Aynı nesneyi kopyalamak ise Ic’yi çoğaltır ve bir ayırma yapar: kopya=1.

Yani kazanılan derleme bedeli, nesne üretildiği her yerde koşum bedeline dönüşür: üretme ve kopyalama bir ayırma öder, taşıma ödemez. İleri bildirim aynı sınırı gösterir: referans=0 ve deger=1. Referansla geçen çağrı gizleme deyiminin bedelini ödemez; değerle geçen ödeyen çağrıdır. Sınır şudur: sınıf sıcak bir döngüde yüz bin kez kopyalanıyorsa, derlemede kazanılan süre koşumda yüz bin ayırmaya karşılık gelir. Bu yüzden gizleme deyimi, nesnelerin seyrek üretildiği, başlığın çok birime görünür olduğu ve gerçeklemenin sık değiştiği sınıflar için gerekçelidir.

Karar verirken üç soru sorulur. Sınıf kaç birime görünür? Gerçekleme ne sıklıkla değişir? Nesne kaç kez üretilir ya da kopyalanır? İlk iki sorunun yanıtı büyükse ve üçüncüsü küçükse gizleme deyimi kazandırır. Üçüncü soru büyükse bedel koşumda geri ödenir ve ayırma sayısı, bu derste olduğu gibi, doğrudan sayılarak kontrol edilmelidir. Sayım yapmadan verilen karar bir tahmindir; dersin amacı bu tahmini bir ölçüme çevirmektir. Ölçüm de yalnızca sayıyı verir: kazanılan derleme süresi bu çıktıdan okunamaz, çünkü derleme süresi bu programın çıktısında yer almaz.

Özet

  • Bu dersin ölçtüğü düzenek bedeli koşum sınıfına taşır: gizleme deyimi başlığın görünürlüğünü azaltır ve bunun karşılığında her nesne bir öbek ayırması ödemek zorunda kalır.
  • Başlık değiştiğinde onu içeren her birim yeniden derlenir; ileri bildirim, gereksiz #include silme ve extern template bu zinciri kısaltır.
  • Gizleme deyimi özel üyeleri Ic arkasına saklar; başlık değişmeden gerçekleme değişebilir. Yıkıcı ve taşıma işlemleri Ic tam tanımlıyken, .cpp dosyasında tanımlanır.
  • gizleme deyimi üç nesnede nesne=3 ayirma=3 ölçtü: her nesne bir ayırma öder.
  • İleri bildirim başlığı hafifletir; referans=0 ve deger=1 ile bedel çağrı biçimine bağlıdır. extern template şablon somutlaştırmasını tek birime toplar.
  • Taşıma tasima=0 ayırma, kopyalama kopya=1 ayırma yaptı: kazanılan derleme bedeli kopyalamada koşumda yeniden ödenir.

Sonraki Adım

Bu ders bir başlığın görünürlüğünü azaltmayı gösterdi; ama azaltmanın etkisini hâlâ tahminle anlattık. Sıradaki ders kursun kapanışıdır: değişen bir başlık kaç birimi yeniden derletiyor sorusuyla bu kursta ölçtüğümüz bütün düzenekleri birlikte değerlendireceğiz.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat