为什么输出流优先选择“ \ n”而不是“ \ n”?

时间:2019-07-28 12:27:24

标签: c++ performance cout string-literals

this答案中,我们可以看到:

  

我想使用'\n'或使用"\n"几乎没有什么区别,但是后者是一个(两个)字符的数组,必须逐个字符地打印,为此必须循环设置,比输出单个字符要复杂

强调我的

这对我来说很有意义。我认为输出const char*需要一个循环,该循环将测试空终止符,与简单的putchar相比,必须引入更多的操作(这并不意味着std::coutchar的代表进行了调用-只是简单地介绍了一个示例。

说服我使用

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

而不是

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

这里值得一提的是,我知道性能差异几乎可以忽略不计。但是,有些人可能会争辩说,前一种方法的意图是实际上传递单个字符,而不是恰好是一个char长(两个 char如果您算上'\0',就可以。)

最近,我为使用后一种方法的人做了一些代码修改。我对此案发表了一点评论,然后继续。然后,开发人员感谢我,并说他甚至都没有想到这种差异(主要集中在意图上)。完全没有影响(毫不奇怪),但是更改被采用。

然后我开始怀疑该变化到底有多精确,所以我狂奔。令我惊讶的是,它在带有-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

那是为什么?为什么他们两个最终都使用std::basic_ostream参数调用相同的const char*函数?这是否意味着char文字方法不仅比字符串文字方法更好,而且实际上更糟

4 个答案:

答案 0 :(得分:13)

没有其他答案能真正解释为什么编译器会生成您在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-8dS

现在您可以看到,尽管输出'\n'仍然要贵一些,但差异没有您的示例那么明显。

答案 1 :(得分:3)

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

两个版本都调用write(buffer, bufferSize)样式函数。对于字符串版本,bufferSize在编译时(1字节)是已知的,因此无需查找零终止符运行时。对于char版本,编译器会在堆栈上创建一个1字节的缓冲区,将字符放入其中,然后将该缓冲区传递出去以进行写出。因此,char版本要慢一些。

答案 2 :(得分:2)

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

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

我想总体上我不会太在意。

答案 3 :(得分:-9)

建议不要使用std :: endl代替“ \ n”,以提高可读性。