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?
var2
tidak.sizeof(Foo)
tidak dapat dikurangi menurut definisi - jika Anda mencetaknyasizeof(Foo)
harus menghasilkan8
(pada platform umum). Compiler dapat mengoptimalkan ruang yang digunakanvar2
(tidak peduli apakah melaluinew
atau 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.Jawaban:
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 .
Tidak (jika "benar-benar" tidak digunakan).
Sekarang muncul dua pertanyaan dalam benak:
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()
f2
adalah sama denganf1
, dan tidak ada memori yang pernah digunakan untuk menyimpan yang sebenarnyaFoo2::var2
. ( Dentang melakukan hal serupa ).Diskusi
Beberapa orang mungkin mengatakan ini berbeda karena dua alasan:
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:
sizeof(Foo)
),memcpy
,memcmp
),1)
2) Seperti menegaskan lulus atau gagal.
sumber
assert(sizeof(…)…)
tidak benar-benar membatasi kompilator — ia harus menyediakansizeof
yang memungkinkan kode menggunakan hal-hal sepertimemcpy
bekerja, tetapi itu tidak berarti kompilator entah bagaimana diharuskan menggunakan banyak byte kecuali mereka mungkin diekspos sedemikianmemcpy
rupa sehingga dapat tetap menulis ulang untuk menghasilkan nilai yang benar.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
Foo
tidak menempati memori apa pun,Foo
bahkan tidak muncul! Jika ada penggunaan lain yang tidak dapat dioptimalkan, missizeof(Foo)
mungkin penting - tetapi hanya untuk segmen kode itu! Jika semua penggunaan bisa dioptimalkan seperti ini maka keberadaan egvar3
tidak mempengaruhi kode yang dihasilkan. Tetapi bahkan jika digunakan di tempat lain,test()
akan tetap dioptimalkan!Singkatnya: Setiap penggunaan
Foo
dioptimalkan secara mandiri. Beberapa mungkin menggunakan lebih banyak memori karena anggota yang tidak dibutuhkan, beberapa mungkin tidak. Konsultasikan manual kompiler Anda untuk lebih jelasnya.sumber
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
Foo
yang 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.
sumber
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 mendapatkan2*sizeof(int)
. Jika Anda membuat larikFoo
s, jarak antara permulaan dua objek berurutanFoo
selalusizeof(Foo)
byte.Tipe Anda adalah tipe tata letak standar , yang berarti bahwa Anda juga dapat mengakses anggota berdasarkan offset yang dihitung waktu kompilasi (lih.
offsetof
Makro). Selain itu, Anda dapat memeriksa representasi byte-by-byte dari objek dengan menyalin ke arraychar
penggunaanstd::memcpy
. Dalam semua kasus ini, anggota kedua dapat diamati berada di sana.sumber
gcc -fwhole-program -O3 *.c
bisa secara teori melakukannya, tetapi dalam praktiknya mungkin tidak. (misalnya jika program membuat beberapa asumsi tentang nilai pasti yangsizeof()
ada pada target ini, dan karena ini adalah pengoptimalan yang sangat rumit yang harus dilakukan oleh pemrogram secara manual jika mereka menginginkannya.)Contoh-contoh yang diberikan oleh jawaban lain untuk pertanyaan ini yang
var2
didasarkan 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 makavar2
tidak dapat dihilangkan. Sejauh yang saya tahu, tidak ada kompiler C / C ++ saat ini yang dapat mengkhususkan fungsi sesuai dengan penghapusanvar2
, 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
var2
dari struct kecuali seluruh variabel struct dipisahkan. Untuk kasus-kasus elisiasi yang menarik darivar2
struct, jawabannya adalah: Tidak.Beberapa compiler C / C ++ di masa mendatang akan dapat keluar
var2
dari struct, dan ekosistem yang dibangun di sekitar compiler perlu beradaptasi untuk memproses informasi elisi yang dihasilkan oleh compiler.sumber
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 ...
-fdce
singkatan dari Dead Code Elimination .Anda dapat menggunakan
__attribute__((used))
untuk mencegah gcc menghilangkan variabel yang tidak digunakan dengan penyimpanan statis:sumber