为什么 '\n' 对于输出流比 "\n" 更受欢迎?

Why is '\n' preferred over "\n" for output streams?

this答案中我们可以读到:

I suppose there's little difference between using '\n' or using "\n", but the latter is an array of (two) characters, which has to be printed character by character, for which a loop has to be set up, which is more complex than outputting a single character.

强调我的

这对我来说很有意义。我认为输出 const char* 需要一个循环来测试空终止符, 必须 引入比简单的 putchar (并不意味着 std::coutchar 委托调用它 - 这只是介绍示例的简化)。

这说服了我使用

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

而不是

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

这里值得一提的是,我知道性能差异几乎可以忽略不计。尽管如此,有些人可能会争辩说,前一种方法的意图实际上是传递单个字符,而不是恰好是 char 长的字符串文字(two chars long if you count the '[=22=]').

最近我为使用后一种方法的人做了一些小的代码审查。我对这个案子发表了一点评论,然后继续前进。开发人员随后向我表示感谢,并表示他根本没有想到这种差异(主要看意图)。它根本没有影响(不出所料),但更改被采纳了。

然后我开始想 这个变化到底有多重要,所以我 运行 神马。令我惊讶的是,在带有 -std=c++17 -O3 标志的 GCC(主干)上进行测试时,它显示了 following results。为以下代码生成的程序集:

#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 文字方法不仅 没有更好 ,而且实际上 比字符串文字方法更差

是的,对于这个特定的实现,对于您的示例,char 版本比字符串版本慢一点。

两个版本都调用了 write(buffer, bufferSize) 风格的函数。对于字符串版本,bufferSize 在编译时已知(1 字节),因此不需要在 运行 时寻找零终止符。对于 char 版本,编译器在堆栈上创建一个 1 字节的小缓冲区,将字符放入其中,然后将此缓冲区传递给写出。所以,char 版本有点慢。

请记住,您在程序集中看到的只是调用堆栈的创建,而不是实际函数的执行。

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

我想总的来说我不会太在意。

None 的其他答案确实解释了为什么编译器会生成它在您的 Godbolt 中生成的代码 link,所以我想我会参与其中。

如果您查看生成的代码,您会发现:

std::cout << '\n';

编译为,实际上:

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

为了使这项工作正常进行,编译器必须为函数 chr() 生成堆栈帧,这是许多额外指令的来源。

另一方面,编译时:

std::cout << "\n";

编译器可以将str()优化为简单的'tail call'operator<< (const char *),这意味着不需要堆栈帧。

因此,由于您将对 operator<< 的调用放在不同的函数中,因此您的结果有些偏差。使这些调用内联更能说明问题,请参阅:https://godbolt.org/z/OO-8dS

现在您可以看到,虽然输出 '\n' 仍然有点昂贵(因为 ofstream::operator<< (char) 没有特定的重载),但差异不如您的示例明显。