c++ - 為什麼'\ n'優先於輸出流的“\ n”?




performance cout (3)

在 this 答案中我們可以讀到:

我想使用 '\n' 或使用 "\n" 之間沒有什麼區別,但後者是一個(兩個)字符數組,必須逐個字符地打印,為此必須設置一個循環, 比輸出單個字符更複雜

強調我的

這對我來說很有意義。 我認為輸出一個 const char* 需要一個循環來測試null-terminator,它 必須 引入更多的操作,比如說,一個簡單的 putchar (並不意味著帶有 char 委託的 std::cout 來調用它 - 它只是簡化介紹一個例子)。

這說服我使用

std::cout << '\n';
std::cout << ' ';

而不是

std::cout << "\n";
std::cout << " ";

值得一提的是,我知道性能差異幾乎可以忽略不計。 儘管如此,有些人可能會爭辯說前一種方法實際上傳遞了一個單一字符的意圖,而不是恰好是一個 char 長的字符串文字(如果算上 '\0' 兩個 char 長)。

最近我為使用後一種方法的人做了一些小代碼修改。 我對案件做了一個小評論並繼續前進。 開發商然後感謝我,並說他甚至沒有想到這種差異(主要關注意圖)。 它根本沒有影響(不出所料),但改變被採納了。

然後我開始想知道這種變化 究竟 多麼 重要,所以我跑到了Godbolt。 令我驚訝的是,當使用 -std=c++17 -O3 標誌在GCC(主幹)上進行測試時,它顯示了 以下結果 。 為以下代碼生成的程序集:

#include <iostream>

void str() {
    std::cout << "\n";
}

void chr() {
    std::cout << '\n';
}

int main() {
    str();
    chr();
}

讓我感到驚訝,因為看起來 chr() 實際上生成的指令恰好是 str() 兩倍:

.LC0:
        .string "\n"
str():
        mov     edx, 1
        mov     esi, OFFSET FLAT:.LC0
        mov     edi, OFFSET FLAT:_ZSt4cout
        jmp     std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
chr():
        sub     rsp, 24
        mov     edx, 1
        mov     edi, OFFSET FLAT:_ZSt4cout
        lea     rsi, [rsp+15]
        mov     BYTE PTR [rsp+15], 10
        call    std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
        add     rsp, 24
        ret

這是為什麼? 為什麼他們兩個最終用 const char* 參數調用相同的 std::basic_ostream 函數? 這是否意味著 char 字面方法不僅 不是更好 ,而且實際上比字符串文字


其他答案都沒有真正解釋為什麼編譯器生成它在Godbolt鏈接中的代碼,所以我認為我會插入。

如果查看生成的代碼,可以看到:

std::cout << '\n';

編譯為,實際上:

char c = '\n';
std::cout.operator<< (&c, 1);

為了使這項工作,編譯器必須為函數 chr() 生成一個堆棧幀,這是許多額外指令的來源。

另一方面,在編譯時:

std::cout << "\n";

編譯器可以優化 str() 以簡單地“尾調用” operator<< (const char *) ,這意味著不需要堆棧幀。

因此,您在單獨的函數中調用 operator<< 這一事實會使結果有些偏差。 內聯調用更具啟發性,請參閱: https://godbolt.org/z/OO-8dShttps://godbolt.org/z/OO-8dS

現在您可以看到,雖然輸出 '\n' 仍然有點貴(因為沒有特定的重載來自 ofstream::operator<< (char) ),但差異不如示例中那麼明顯。


是的,對於這個特定的實現,對於您的示例, char 版本比字符串版本稍慢。

兩個版本都調用 write(buffer, bufferSize) 樣式函數。 對於字符串版本, bufferSize 在編譯時(1字節)是已知的,因此不需要找到零終止符運行時。 對於 char 版本,編譯器在堆棧上創建一個小的1字節緩衝區,將字符放入其中,並將此緩衝區傳遞給寫出。 所以, char 版本有點慢。


請記住,雖然您在程序集中看到的只是callstack的創建,而不是實際函數的執行。

std::cout << '\n'; 仍然比 std::cout << "\n"; 稍微快一點 std::cout << "\n";

我已經創建了這個小程序來測量性能,並且使用g ++ -O3在我的機器上快了 大約20倍 。 親自嘗試一下!

編輯:對不起,注意到我的程序錯字了,速度不是那麼快! 幾乎無法衡量任何差異。 有時一個更快。 另一次是另一次。

#include <chrono>
#include <iostream>

class timer {
    private:
        decltype(std::chrono::high_resolution_clock::now()) begin, end;

    public:
        void
        start() {
            begin = std::chrono::high_resolution_clock::now();
        }

        void
        stop() {
            end = std::chrono::high_resolution_clock::now();
        }

        template<typename T>
        auto
        duration() const {
            return std::chrono::duration_cast<T>(end - begin).count();
        }

        auto
        nanoseconds() const {
            return duration<std::chrono::nanoseconds>();
        }

        void
        printNS() const {
            std::cout << "Nanoseconds: " << nanoseconds() << std::endl;
        }
};

int
main(int argc, char** argv) {
    timer t1;
    t1.start();
    for (int i{0}; 10000 > i; ++i) {
        std::cout << '\n';
    }
    t1.stop();

    timer t2;
    t2.start();
    for (int i{0}; 10000 > i; ++i) {
        std::cout << "\n";
    }
    t2.stop();
    t1.printNS();
    t2.printNS();
}

編輯:正如geza建議我為兩者嘗試了100000000次迭代並將其發送到/ dev / null並運行了四次。 '\ n'曾經慢一點,速度提高了3倍但從未太多,但在其他機器上可能會有所不同:

Nanoseconds: 8668263707
Nanoseconds: 7236055911

Nanoseconds: 10704225268
Nanoseconds: 10735594417

Nanoseconds: 10670389416
Nanoseconds: 10658991348

Nanoseconds: 7199981327
Nanoseconds: 6753044774

我想總的來說我不會太在乎。





string-literals