簡體   English   中英

為什么 GCC 不能假設 std::vector::size 在這個循環中不會改變?

[英]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

我查了一下,我很確定r12xs.len() - 1並且rbx是計數器。 早些時候有一個rbxadd和一個循環外的movr12

為什么是這樣? 似乎如果 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++ 不能。


優化你的 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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM