为什么输出流首选" "而不是""?

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

本文关键字:quot 为什么 输出流      更新时间:2023-10-16

在这个答案中,我们可以读到:

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

强调我的

这对我来说是有道理的。我认为输出const char*需要一个循环来测试空终止符,它必须引入比简单的putchar更多的操作(并不意味着std::coutchar代表调用它 - 这只是引入示例的简化)。

这说服了我使用

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

而不是

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

值得一提的是,我知道性能差异几乎可以忽略不计。尽管如此,有些人可能会争辩说,前一种方法的意图是实际传递单个字符,而不是恰好是一个char长的字符串文字(如果算上'',则为两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';

实际上编译为:

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

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

另一方面,在编译时:

std::cout << "n";

编译器可以优化str(),简单地"尾部调用"operator<< (const char *),这意味着不需要堆栈帧。

因此,由于您将对operator<<的调用放在单独的函数中,因此您的结果有些偏差。 以内联方式进行这些调用更具启发性,请参阅:https://godbolt.org/z/OO-8dS

现在您可以看到,虽然输出'n'仍然有点昂贵(因为ofstream::operator<< (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 并运行了四次。'' 曾经慢过,快了 3 倍,但从来没有快过太多,但在其他机器上可能会有所不同:

Nanoseconds: 8668263707
Nanoseconds: 7236055911
Nanoseconds: 10704225268
Nanoseconds: 10735594417
Nanoseconds: 10670389416
Nanoseconds: 10658991348
Nanoseconds: 7199981327
Nanoseconds: 6753044774

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

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

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