[英]Why can't GCC assume that std::vector::size won't change in this loop?
我向一位同事聲稱if (i < input.size() - 1) print(0);
將在此循環中得到優化,因此不會在每次迭代中讀取input.size()
,但事實證明並非如此!
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);
}
}
根據帶有 gcc 選項-O3 -fno-exceptions
的編譯器資源管理器,我們實際上是在每次迭代中讀取input.size()
並使用lea
執行減法!
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
有趣的是,在 Rust 中確實發生了這種優化。 看起來i
被替換為每次迭代遞減的變量j
,並且測試i < input.size() - 1
被替換為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);
}
}
}
在Compiler Explorer 中,相關程序集如下所示:
cmpq %r12, %rbx
jae .LBB0_4
我查了一下,我很確定r12
是xs.len() - 1
並且rbx
是計數器。 早些時候有一個rbx
的add
和一個循環外的mov
到r12
。
為什么是這樣? 似乎如果 GCC 能夠像它那樣內聯size()
和operator[]
,它應該能夠知道size()
不會改變。 但也許GCC的優化器判斷不值得把它拉出來變成一個變量? 或者也許還有其他一些可能的副作用會使這不安全——有人知道嗎?
對cout.operator<<(int)
的非內聯函數調用是優化器的黑匣子(因為庫只是用 C++ 編寫的,優化器看到的只是一個原型;請參閱評論中的討論)。 它必須假設全局變量可能指向的任何內存都已被修改。
(或者std::endl
調用。順便說一句,為什么在那個時候強制刷新 cout 而不是只打印'\\n'
?)
例如,就其所知, std::vector<int> &input
是對全局變量的引用,其中一個函數調用修改了該全局變量。 (或者在某處有一個全局vector<int> *ptr
,或者有一個函數返回指向某個其他編譯單元中的static vector<int>
的指針,或者函數可以獲得對該向量的引用而不被我們傳遞了對它的引用。
如果您有一個從未使用過地址的局部變量,則編譯器可以假定非內聯函數調用無法對其進行變異。 因為任何全局變量都無法保存指向該對象的指針。 (這稱為逃逸分析)。 這就是為什么編譯器可以跨函數調用將size_t i
保存在寄存器中。 ( int i
可以被優化掉,因為它被size_t i
遮蔽而沒有被其他使用)。
它可以對局部vector
做同樣的事情(即對於 base、end_size 和 end_capacity 指針。)
ISO C99 對此問題有一個解決方案: int *restrict foo
。 許多C ++編譯支持int *__restrict foo
答應內存指向foo
通過該指針只訪問。 在采用 2 個數組的函數中最常用,並且您希望向編譯器保證它們不會重疊。 因此它可以自動矢量化而無需生成代碼來檢查並運行回退循環。
OP評論:
在 Rust 中,非可變引用是一種全局保證,即沒有其他人正在改變您所引用的值(相當於 C++
restrict
)
這就解釋了為什么 Rust 可以進行這種優化而 C++ 不能。
顯然你應該使用auto size = input.size();
一旦在函數的頂部,編譯器就知道它是一個循環不變式。 C++ 實現不會為您解決這個問題,因此您必須自己解決。
您可能還需要const int *data = input.data();
也從std::vector<int>
“控制塊”提升數據指針的負載。 不幸的是,優化可能需要非常不習慣的源更改。
Rust 是一種更現代的語言,它是在編譯器開發人員了解編譯器在實踐中的可能性之后設計的。 它也確實以其他方式顯示出來,包括可移植地公開 CPU 可以通過i32.count_ones
、旋轉、位掃描等執行的一些很酷的東西。 ISO C++ 仍然沒有可移植地公開這些中的任何一個,這真是愚蠢,除了std::bitset::count()
。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.