Saya mengklaim rekan kerja yang if (i < input.size() - 1) print(0);
akan dioptimalkan dalam lingkaran ini sehingga input.size()
tidak dibaca di setiap iterasi, tetapi ternyata ini bukan masalahnya!
void print(int x) {
std::cout << x << std::endl;
}
void print_list(const std::vector<int>& input) {
int i = 0;
for (size_t i = 0; i < input.size(); i++) {
print(input[i]);
if (i < input.size() - 1) print(0);
}
}
Menurut Compiler Explorer dengan opsi gcc, -O3 -fno-exceptions
kami sebenarnya membaca input.size()
setiap iterasi dan menggunakan lea
untuk melakukan pengurangan!
movq 0(%rbp), %rdx
movq 8(%rbp), %rax
subq %rdx, %rax
sarq $2, %rax
leaq -1(%rax), %rcx
cmpq %rbx, %rcx
ja .L35
addq $1, %rbx
Menariknya, di Rust optimasi ini memang terjadi. Sepertinya i
akan diganti dengan variabel j
yang dikurangi setiap iterasi, dan tes i < input.size() - 1
diganti dengan sesuatu seperti j > 0
.
fn print(x: i32) {
println!("{}", x);
}
pub fn print_list(xs: &Vec<i32>) {
for (i, x) in xs.iter().enumerate() {
print(*x);
if i < xs.len() - 1 {
print(0);
}
}
}
Di Penjelajah Kompiler , rakitan yang relevan terlihat seperti ini:
cmpq %r12, %rbx
jae .LBB0_4
Aku memeriksa dan saya cukup yakin r12
adalah xs.len() - 1
dan rbx
adalah meja. Sebelumnya ada add
for rbx
dan di mov
luar loop ke r12
.
Kenapa ini? Sepertinya jika GCC mampu menyejajarkan size()
dan operator[]
seperti yang terjadi, itu harus bisa tahu bahwa size()
tidak berubah. Tapi mungkin pengoptimal GCC menilai bahwa tidak layak menariknya ke dalam variabel? Atau mungkin ada beberapa efek samping lain yang mungkin membuat ini tidak aman - adakah yang tahu?
println
mungkin metode yang kompleks, compiler mungkin mengalami kesulitan pembuktian bahwaprintln
tidak bermutasi vektor.cout.operator<<()
. Kompiler tidak tahu bahwa fungsi kotak hitam ini tidak mendapatkan referensi kestd::vector
dari global.println
atauoperator<<
kuncinya.Jawaban:
Panggilan fungsi non-inline ke
cout.operator<<(int)
adalah kotak hitam untuk pengoptimal (karena perpustakaan hanya ditulis dalam C ++ dan semua pengoptimal melihat adalah prototipe; lihat diskusi dalam komentar). Itu harus mengasumsikan setiap memori yang mungkin dapat ditunjukkan oleh var global telah dimodifikasi.(Atau
std::endl
telepon. BTW, mengapa memaksakan flout of cout pada saat itu alih-alih hanya mencetak'\n'
?)misalnya untuk semua yang diketahuinya,
std::vector<int> &input
adalah referensi ke variabel global, dan salah satu dari panggilan fungsi tersebut memodifikasi global var tersebut . (Atau ada global divector<int> *ptr
suatu tempat, atau ada fungsi yang mengembalikan pointer kestatic vector<int>
dalam beberapa unit kompilasi lain, atau cara lain bahwa suatu fungsi bisa mendapatkan referensi ke vektor ini tanpa melewati referensi oleh kami.Jika Anda memiliki variabel lokal yang alamatnya tidak pernah diambil, kompilator dapat mengasumsikan bahwa panggilan fungsi non-inline tidak dapat memutasinya. Karena tidak akan ada cara bagi variabel global untuk memegang pointer ke objek ini. ( Ini disebut Escape Analysis ). Itu sebabnya kompiler dapat menyimpan
size_t i
register di seluruh panggilan fungsi. (int i
hanya bisa dioptimalkan karena dibayangi olehsize_t i
dan tidak digunakan sebaliknya).Itu bisa melakukan hal yang sama dengan lokal
vector
(yaitu untuk pointer base, end_size dan end_capacity.)ISO C99 memiliki solusi untuk masalah ini:
int *restrict foo
. Banyak C ++ mengkompilasi mendukungint *__restrict foo
berjanji bahwa memori yang ditunjuk olehfoo
adalah hanya diakses melalui pointer itu. Paling umum berguna dalam fungsi yang mengambil 2 array, dan Anda ingin menjanjikan kompilator mereka tidak tumpang tindih. Jadi ia dapat melakukan vektor-otomatis tanpa menghasilkan kode untuk memeriksanya dan menjalankan fallback loop.OP berkomentar:
Itu menjelaskan mengapa Rust bisa melakukan optimasi ini tetapi C ++ tidak bisa.
Mengoptimalkan C ++ Anda
Tentunya Anda harus menggunakan
auto size = input.size();
sekali di bagian atas fungsi Anda sehingga kompiler tahu itu loop invarian. Implementasi C ++ tidak menyelesaikan masalah ini untuk Anda, jadi Anda harus melakukannya sendiri.Anda mungkin juga perlu
const int *data = input.data();
mengangkat banyak pointer data daristd::vector<int>
"blok kontrol" juga. Sangat disayangkan bahwa mengoptimalkan dapat membutuhkan perubahan sumber yang sangat non-idiomatik.Karat adalah bahasa yang jauh lebih modern, dirancang setelah pengembang kompiler belajar apa yang mungkin dalam praktek untuk kompiler. Ini benar-benar menunjukkan dengan cara lain, juga, termasuk mengekspos beberapa hal keren yang dapat dilakukan CPU melalui
i32.count_ones
, memutar, memindai, dll. Sangat bodoh bahwa ISO C ++ masih tidak mengekspos semua portable ini, kecualistd::bitset::count()
.sumber
operator<<
untuk tipe operan tersebut; jadi dalam Standard C ++ itu bukan kotak hitam dan kompiler dapat menganggap itu melakukan apa yang dikatakan dokumentasi. Mungkin mereka ingin mendukung pengembang perpustakaan menambahkan perilaku tidak standar ...cout
memungkinkan objek dari kelas yang ditentukan pengguna yang berasal daristreambuf
dikaitkan dengan aliran menggunakancout.rdbuf
. Demikian pula objek yang berasal dariostream
dapat dikaitkan dengancout.tie
.this
pointer secara implisit disahkan. Ini bisa terjadi dalam praktiknya secepat konstruktor. Pertimbangkan loop sederhana ini - Saya hanya memeriksa loop utama gcc (dariL34:
kejne L34
), tetapi sudah pasti berperilaku seolah-olah anggota vektor telah lolos (memuatnya dari memori setiap iterasi).