Apakah variabel anggota yang tidak digunakan menggunakan memori?

92

Apakah menginisialisasi variabel anggota dan tidak mereferensikan / menggunakannya lebih jauh memakan RAM selama runtime, atau apakah kompilator mengabaikan variabel itu?

struct Foo {
    int var1;
    int var2;

    Foo() { var1 = 5; std::cout << var1; }
};

Pada contoh di atas, anggota 'var1' mendapatkan nilai yang kemudian ditampilkan di konsol. 'Var2', bagaimanapun, tidak digunakan sama sekali. Oleh karena itu, menuliskannya ke memori selama runtime akan membuang-buang resource. Apakah kompilator memperhitungkan situasi semacam ini ke dalam akun dan mengabaikan variabel yang tidak digunakan, atau apakah objek Foo selalu berukuran sama, terlepas dari apakah anggotanya digunakan?

Chriss555888
sumber
25
Ini tergantung pada kompilator, arsitektur, sistem operasi, dan pengoptimalan yang digunakan.
Burung Hantu
16
Ada metrik ton kode driver tingkat rendah di luar sana yang secara khusus menambahkan anggota struct tidak melakukan apa-apa untuk padding agar sesuai dengan ukuran bingkai data perangkat keras dan sebagai peretasan untuk mendapatkan penyelarasan memori yang diinginkan. Jika kompiler mulai mengoptimalkannya, akan ada banyak kerusakan.
Andy Brown
2
@Andy mereka tidak melakukan apa-apa karena alamat anggota data berikut dievaluasi. Artinya, keberadaan anggota padding tersebut memang memiliki perilaku yang dapat diamati pada program tersebut. Di sini, var2tidak.
YSC
4
Saya akan terkejut jika kompiler dapat mengoptimalkannya mengingat bahwa setiap unit kompilasi yang menangani struct seperti itu mungkin akan ditautkan ke unit kompilasi lain menggunakan struct yang sama dan kompilator tidak dapat mengetahui apakah unit kompilasi terpisah mengalamatkan anggota atau tidak.
Galik
2
@geza sizeof(Foo)tidak dapat dikurangi menurut definisi - jika Anda mencetaknya sizeof(Foo)harus menghasilkan 8(pada platform umum). Compiler dapat mengoptimalkan ruang yang digunakan var2(tidak peduli apakah melalui newatau di stack atau dalam panggilan fungsi ...) dalam konteks apa pun yang menurut mereka wajar, bahkan tanpa LTO atau pengoptimalan program secara keseluruhan. Jika tidak memungkinkan, mereka tidak akan melakukannya, seperti hampir semua pengoptimalan lainnya. Saya percaya pengeditan pada jawaban yang diterima membuatnya sangat kecil kemungkinannya untuk disesatkan olehnya.
Max Langhof

Jawaban:

107

Aturan emas C ++ "as-if" 1 menyatakan bahwa, jika perilaku program yang dapat diamati tidak bergantung pada keberadaan anggota data yang tidak digunakan, kompilator diizinkan untuk mengoptimalkannya .

Apakah variabel anggota yang tidak digunakan menggunakan memori?

Tidak (jika "benar-benar" tidak digunakan).


Sekarang muncul dua pertanyaan dalam benak:

  1. Kapan perilaku yang dapat diamati tidak bergantung pada keberadaan anggota?
  2. Apakah situasi seperti itu terjadi dalam program kehidupan nyata?

Mari kita mulai dengan sebuah contoh.

Contoh

#include <iostream>

struct Foo1
{ int var1 = 5;           Foo1() { std::cout << var1; } };

struct Foo2
{ int var1 = 5; int var2; Foo2() { std::cout << var1; } };

void f1() { (void) Foo1{}; }
void f2() { (void) Foo2{}; }

Jika kita meminta gcc untuk mengkompilasi unit terjemahan ini , ia akan menampilkan:

f1():
        mov     esi, 5
        mov     edi, OFFSET FLAT:_ZSt4cout
        jmp     std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
f2():
        jmp     f1()

f2adalah sama dengan f1, dan tidak ada memori yang pernah digunakan untuk menyimpan yang sebenarnya Foo2::var2. ( Dentang melakukan hal serupa ).

Diskusi

Beberapa orang mungkin mengatakan ini berbeda karena dua alasan:

  1. ini contoh yang terlalu sepele,
  2. struct sepenuhnya dioptimalkan, itu tidak dihitung.

Nah, program yang bagus adalah kumpulan hal-hal sederhana yang cerdas dan kompleks, bukan penjajaran sederhana dari hal-hal kompleks. Dalam kehidupan nyata, Anda menulis banyak sekali fungsi sederhana menggunakan struktur sederhana daripada yang dioptimalkan oleh compiler. Contohnya:

bool insert(std::set<int>& set, int value)
{
    return set.insert(value).second;
}

Ini adalah contoh asli dari anggota data (di sini, std::pair<std::set<int>::iterator, bool>::first) yang tidak digunakan. Tebak apa? Itu dioptimalkan jauh ( contoh sederhana dengan set boneka jika perakitan itu membuat Anda menangis).

Sekarang akan menjadi waktu yang tepat untuk membaca jawaban yang sangat baik dari Max Langhof (tolong beri saya suara positif ). Ini menjelaskan mengapa, pada akhirnya, konsep struktur tidak masuk akal pada tingkat perakitan keluaran kompilator.

"Tapi, jika saya melakukan X, fakta bahwa anggota yang tidak terpakai dioptimalkan adalah masalah!"

Ada sejumlah komentar yang menyatakan jawaban ini pasti salah karena beberapa operasi (seperti assert(sizeof(Foo2) == 2*sizeof(int))) akan merusak sesuatu.

Jika X adalah bagian dari perilaku program 2 yang dapat diamati , kompilator tidak diizinkan untuk mengoptimalkan semuanya. Ada banyak operasi pada objek yang berisi anggota data "tidak terpakai" yang akan memiliki efek yang dapat diamati pada program. Jika operasi seperti itu dilakukan atau jika kompilator tidak dapat membuktikan tidak ada yang dijalankan, anggota data yang "tidak terpakai" adalah bagian dari perilaku program yang dapat diamati dan tidak dapat dioptimalkan .

Operasi yang mempengaruhi perilaku yang dapat diamati termasuk, tetapi tidak terbatas pada:

  • mengambil ukuran dari sebuah tipe object ( sizeof(Foo)),
  • mengambil alamat anggota data yang dideklarasikan setelah yang "tidak digunakan",
  • menyalin objek dengan fungsi seperti memcpy,
  • memanipulasi representasi objek (seperti dengan memcmp),
  • memenuhi syarat suatu objek sebagai volatile ,
  • dll .

1)

[intro.abstract]/1

Deskripsi semantik dalam dokumen ini mendefinisikan mesin abstrak nondeterministik berparameter. Dokumen ini tidak menempatkan persyaratan pada struktur implementasi yang sesuai. Secara khusus, mereka tidak perlu menyalin atau meniru struktur mesin abstrak. Sebaliknya, implementasi yang sesuai diperlukan untuk meniru (hanya) perilaku yang dapat diamati dari mesin abstrak seperti yang dijelaskan di bawah ini.

2) Seperti menegaskan lulus atau gagal.

YSC
sumber
Komentar yang menyarankan perbaikan pada jawaban telah diarsipkan dalam obrolan .
Cody Grey
1
Bahkan assert(sizeof(…)…)tidak benar-benar membatasi kompilator — ia harus menyediakan sizeofyang memungkinkan kode menggunakan hal-hal seperti memcpybekerja, tetapi itu tidak berarti kompilator entah bagaimana diharuskan menggunakan banyak byte kecuali mereka mungkin diekspos sedemikian memcpyrupa sehingga dapat tetap menulis ulang untuk menghasilkan nilai yang benar.
Davis Herring
@Davis Tentu.
YSC
64

Penting untuk disadari bahwa kode yang dihasilkan kompilator tidak memiliki pengetahuan aktual tentang struktur data Anda (karena hal seperti itu tidak ada di tingkat perakitan), dan begitu juga dengan pengoptimal. Kompilator hanya menghasilkan kode untuk setiap fungsi , bukan struktur data .

Ok, itu juga menulis bagian data konstan dan semacamnya.

Berdasarkan itu, kita sudah dapat mengatakan bahwa pengoptimal tidak akan "menghapus" atau "menghilangkan" anggota, karena tidak menampilkan struktur data. Ini mengeluarkan kode , yang mungkin atau mungkin tidak menggunakan anggota, dan di antara tujuannya adalah menghemat memori atau siklus dengan menghilangkan penggunaan yang tidak berguna (yaitu menulis / membaca) dari anggota.


Intinya adalah bahwa "jika kompilator dapat membuktikan dalam lingkup suatu fungsi (termasuk fungsi yang dimasukkan ke dalamnya) bahwa anggota yang tidak digunakan tidak membuat perbedaan tentang bagaimana fungsi tersebut beroperasi (dan apa yang dikembalikannya) maka kemungkinan besar kehadiran anggota tidak menyebabkan biaya tambahan ".

Saat Anda membuat interaksi suatu fungsi dengan dunia luar menjadi lebih rumit / tidak jelas bagi kompiler (ambil / kembalikan struktur data yang lebih kompleks, misalnya a std::vector<Foo>, sembunyikan definisi fungsi dalam unit kompilasi yang berbeda, larang / disinsentif sebaris, dll.) , menjadi semakin besar kemungkinan bahwa kompilator tidak dapat membuktikan bahwa anggota yang tidak digunakan tidak berpengaruh.

Tidak ada aturan ketat di sini karena semuanya bergantung pada pengoptimalan yang dibuat kompilator, tetapi selama Anda melakukan hal-hal sepele (seperti yang ditunjukkan dalam jawaban YSC), kemungkinan besar tidak ada overhead yang akan muncul, saat melakukan hal-hal rumit (mis. Mengembalikan a std::vector<Foo>dari fungsi yang terlalu besar untuk disejajarkan) mungkin akan menimbulkan overhead.


Untuk mengilustrasikan maksudnya, pertimbangkan contoh ini :

struct Foo {
    int var1 = 3;
    int var2 = 4;
    int var3 = 5;
};

int test()
{
    Foo foo;
    std::array<char, sizeof(Foo)> arr;
    std::memcpy(&arr, &foo, sizeof(Foo));
    return arr[0] + arr[4];
}

Kami melakukan hal-hal non-sepele di sini (mengambil alamat, memeriksa dan menambahkan byte dari representasi byte ) namun pengoptimal dapat mengetahui bahwa hasilnya selalu sama di platform ini:

test(): # @test()
  mov eax, 7
  ret

Tidak hanya anggota yang Footidak menempati memori apa pun, Foobahkan tidak muncul! Jika ada penggunaan lain yang tidak dapat dioptimalkan, mis sizeof(Foo)mungkin penting - tetapi hanya untuk segmen kode itu! Jika semua penggunaan bisa dioptimalkan seperti ini maka keberadaan eg var3tidak mempengaruhi kode yang dihasilkan. Tetapi bahkan jika digunakan di tempat lain, test()akan tetap dioptimalkan!

Singkatnya: Setiap penggunaan Foodioptimalkan secara mandiri. Beberapa mungkin menggunakan lebih banyak memori karena anggota yang tidak dibutuhkan, beberapa mungkin tidak. Konsultasikan manual kompiler Anda untuk lebih jelasnya.

Max Langhof
sumber
6
Mic drop "Konsultasikan manual kompiler Anda untuk lebih jelasnya.": D
YSC
22

Kompilator hanya akan mengoptimalkan variabel anggota yang tidak digunakan (terutama variabel publik) jika dapat membuktikan bahwa menghapus variabel tidak memiliki efek samping dan tidak ada bagian dari program yang bergantung pada ukurannya Fooyang sama.

Saya tidak berpikir ada kompiler saat ini yang melakukan pengoptimalan seperti itu kecuali jika strukturnya tidak benar-benar digunakan sama sekali. Beberapa kompiler setidaknya memperingatkan tentang variabel privat yang tidak digunakan tetapi biasanya tidak untuk variabel publik.

Alan Birtles
sumber
1
Namun demikian: godbolt.org/z/UJKguS + tidak ada kompilator yang akan memperingatkan untuk anggota data yang tidak digunakan.
YSC
@YSC clang ++ memperingatkan tentang variabel dan anggota data yang tidak digunakan.
Maxim Egorushkin
3
@YSC Saya pikir itu adalah situasi yang sedikit berbeda, ini mengoptimalkan struktur sepenuhnya dan hanya mencetak 5 secara langsung
Alan Birtles
4
@AlanBirtles Saya tidak melihat perbedaannya. Kompilator mengoptimalkan semuanya dari objek yang tidak berpengaruh pada perilaku program yang dapat diamati. Jadi kalimat pertama Anda "kompilator sangat tidak mungkin untuk mengoptimalkan awau variabel anggota yang tidak terpakai" salah.
YSC
2
@YSC dalam kode nyata di mana struktur sebenarnya digunakan daripada hanya dibangun untuk efek sampingnya mungkin lebih tidak mungkin itu akan dioptimalkan jauh
Alan Birtles
7

Secara umum, Anda harus berasumsi bahwa Anda mendapatkan apa yang Anda minta, misalnya, variabel anggota "tidak terpakai" ada di sana.

Karena dalam contoh Anda kedua anggota adalah public, kompilator tidak dapat mengetahui apakah beberapa kode (terutama dari unit terjemahan lain = file * .cpp lainnya, yang dikompilasi secara terpisah dan kemudian ditautkan) akan mengakses anggota "tidak terpakai".

Jawaban dari YSC memberikan contoh yang sangat sederhana, dimana tipe kelas hanya digunakan sebagai variabel durasi penyimpanan otomatis dan tidak ada penunjuk ke variabel tersebut yang diambil. Di sana, kompilator dapat menyebariskan semua kode dan kemudian dapat menghilangkan semua kode yang mati.

Jika Anda memiliki antarmuka antar fungsi yang ditentukan dalam unit terjemahan yang berbeda, biasanya kompilator tidak tahu apa-apa. Antarmuka biasanya mengikuti beberapa ABI yang telah ditentukan sebelumnya (seperti itu ) sehingga file objek yang berbeda dapat ditautkan bersama tanpa masalah. Biasanya ABI tidak membuat perbedaan jika anggota digunakan atau tidak. Jadi, dalam kasus seperti itu, anggota kedua harus secara fisik ada di memori (kecuali nanti dihilangkan oleh linker).

Dan selama Anda berada dalam batasan bahasa, Anda tidak dapat mengamati bahwa ada penghapusan yang terjadi. Jika Anda menelepon sizeof(Foo), Anda akan mendapatkan 2*sizeof(int). Jika Anda membuat larik Foos, jarak antara permulaan dua objek berurutan Fooselalu sizeof(Foo)byte.

Tipe Anda adalah tipe tata letak standar , yang berarti bahwa Anda juga dapat mengakses anggota berdasarkan offset yang dihitung waktu kompilasi (lih. offsetofMakro). Selain itu, Anda dapat memeriksa representasi byte-by-byte dari objek dengan menyalin ke array charpenggunaan std::memcpy. Dalam semua kasus ini, anggota kedua dapat diamati berada di sana.

Handy999
sumber
Komentar tidak untuk diskusi panjang; percakapan ini telah dipindahkan ke obrolan .
Cody Gray
2
+1: hanya pengoptimalan seluruh program yang agresif yang mungkin dapat menyesuaikan tata letak data (termasuk ukuran waktu kompilasi dan offset) untuk kasus di mana objek struct lokal tidak dioptimalkan sepenuhnya. gcc -fwhole-program -O3 *.cbisa secara teori melakukannya, tetapi dalam praktiknya mungkin tidak. (misalnya jika program membuat beberapa asumsi tentang nilai pasti yang sizeof()ada pada target ini, dan karena ini adalah pengoptimalan yang sangat rumit yang harus dilakukan oleh pemrogram secara manual jika mereka menginginkannya.)
Peter Cordes
6

Contoh-contoh yang diberikan oleh jawaban lain untuk pertanyaan ini yang var2didasarkan pada teknik pengoptimalan tunggal: propagasi konstan, dan elisi selanjutnya dari seluruh struktur (bukan elisi hanyavar2 ). Ini kasus sederhana, dan pengoptimalan kompiler mengimplementasikannya.

Untuk kode C / C ++ yang tidak terkelola, jawabannya adalah bahwa kompilator pada umumnya tidak akan mengelak var2. Sejauh yang saya tahu tidak ada dukungan untuk transformasi struct C / C ++ dalam informasi debugging, dan jika struct dapat diakses sebagai variabel dalam debugger maka var2tidak dapat dihilangkan. Sejauh yang saya tahu, tidak ada kompiler C / C ++ saat ini yang dapat mengkhususkan fungsi sesuai dengan penghapusan var2, jadi jika struct diteruskan ke atau dikembalikan dari fungsi non-inline makavar2 tidak dapat dihilangkan.

Untuk bahasa terkelola seperti C # / Java dengan kompilator JIT, kompilator mungkin dapat mengelida dengan aman var2 karena ia dapat dengan tepat melacak apakah ia sedang digunakan dan apakah ia lolos ke kode tak terkelola. Ukuran fisik struct dalam bahasa terkelola dapat berbeda dari ukurannya yang dilaporkan ke pemrogram.

Kompiler C / C ++ tahun 2019 tidak dapat keluar var2dari struct kecuali seluruh variabel struct dipisahkan. Untuk kasus-kasus elisiasi yang menarik dari var2struct, jawabannya adalah: Tidak.

Beberapa compiler C / C ++ di masa mendatang akan dapat keluar var2dari struct, dan ekosistem yang dibangun di sekitar compiler perlu beradaptasi untuk memproses informasi elisi yang dihasilkan oleh compiler.

simbol atom
sumber
1
Paragraf Anda tentang informasi debug bermuara pada "kami tidak dapat mengoptimalkannya jika itu akan mempersulit proses debug", yang sebenarnya salah. Atau saya salah membaca. Bisakah Anda menjelaskan?
Max Langhof
Jika kompilator mengeluarkan informasi debug tentang struct maka ia tidak dapat menggunakan var2. Pilihannya adalah: (1) Jangan memancarkan informasi debug jika tidak sesuai dengan representasi fisik dari struct, (2) Dukung elisi anggota struct dalam informasi debug dan
keluarkan
Mungkin yang lebih umum adalah merujuk pada Penggantian Skalar dari Agregat (dan kemudian penghapusan penyimpanan mati, dll .).
Davis Herring
4

Ini tergantung pada kompiler Anda dan tingkat pengoptimalannya.

Di gcc, jika Anda menentukan -O, ini akan mengaktifkan tanda pengoptimalan berikut :

-fauto-inc-dec 
-fbranch-count-reg 
-fcombine-stack-adjustments 
-fcompare-elim 
-fcprop-registers 
-fdce
-fdefer-pop
...

-fdcesingkatan dari Dead Code Elimination .

Anda dapat menggunakan __attribute__((used))untuk mencegah gcc menghilangkan variabel yang tidak digunakan dengan penyimpanan statis:

Atribut ini, yang dilampirkan ke variabel dengan penyimpanan statis, berarti variabel tersebut harus dipancarkan meskipun tampaknya variabel tersebut tidak direferensikan.

Ketika diterapkan ke anggota data statis dari template kelas C ++, atribut juga berarti bahwa anggota tersebut dibuat instance-nya jika kelas itu sendiri dibuat.

wonter
sumber
Itu untuk anggota data statis , bukan anggota per-instance yang tidak digunakan (yang tidak dioptimalkan kecuali seluruh objek melakukannya). Tapi ya, saya rasa itu dihitung. BTW, menghilangkan variabel statis yang tidak digunakan bukanlah penghapusan kode mati , kecuali GCC menekuk istilah tersebut.
Peter Cordes