为什么'\n'在输出流中被认为优于"\n"?
为什么'\n'在输出流中被认为优于"\n"?
在这个答案中,我们可以看到:\n
\n我想使用
\'\\n\'
和\"\\n\"
几乎没有什么区别,但后者是一个由两个字符组成的数组,需要逐个字符打印,这需要设置一个循环,比输出一个单个字符复杂。\n
\n这对我来说是有道理的。我认为输出一个const char*
需要一个循环来检查空终止符,这必须比简单的putchar
要多进行一些操作(并不是暗指std::cout
使用char
时调用该函数 - 这只是一个简化引入的示例)。\n这使我坚信使用:\n
std::cout << '\n'; std::cout << ' ';
\n而不是:\n
std::cout << "\n"; std::cout << " ";
\n这里值得一提的是,我知道性能差异几乎可以忽略不计。尽管如此,有些人可能会认为前一种方法传递的是实际上是单个字符的意图,而不仅仅是一个恰好只有一个char
长(如果计算上算上\'\\0\'
,则为两个char
长)的字符串字面量。\n最近,我对某人使用后一种方法进行了一些小的代码审查。我在这个情况下发表了一些小的评论,然后继续了其他事情。然后,开发者感谢我,并说他甚至没有考虑到这样的差异(主要关注意图)。这并没有产生任何影响(不足为奇),但是这种变化被采用了。\n然后我开始想知道这种变化到底有多大意义,所以我跑去看godbolt。令我惊讶的是,当我在GCC(trunk)上使用-std=c++17 -O3
参数测试以下代码时,它显示了以下结果。以下代码生成的汇编代码:\n
#includevoid str() { std::cout << "\n"; } void chr() { std::cout << '\n'; } int main() { str(); chr(); }
\n让我感到惊讶,因为它显示chr()
实际上生成的指令数量是str()
的两倍:\n
.LC0: .string "\n" str(): mov edx, 1 mov esi, OFFSET FLAT:.LC0 mov edi, OFFSET FLAT:_ZSt4cout jmp std::basic_ostream>& std::__ostream_insert >(std::basic_ostream >&, 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 >& std::__ostream_insert >(std::basic_ostream >&, char const*, long) add rsp, 24 ret
\n为什么会这样?为什么它们都最终调用相同的std::basic_ostream
函数,并传递const char*
参数?这是否意味着char
字面量的方法不仅不好,而且比字符串字面量更糟糕?
为什么在输出流中'\n'优于"\n"的原因
在这个特定的实现中,对于你的例子,char版本比string版本稍微慢一些。两种版本都调用一个write(buffer, bufferSize)样式的函数。对于string版本,bufferSize在编译时就已知(1个字节),所以不需要在运行时找到零终止符。对于char版本,编译器在栈上创建一个1字节的小缓冲区,将字符放入其中,并将此缓冲区传递给write函数进行输出。所以,char版本稍微慢一些。
对于Clang也是一样。MSVS将两者编译为相同的汇编代码。这看起来真的很奇怪,特别是因为char版本似乎是被普遍建议的。
这应该没有太大关系。差异非常微不足道。整个写入操作应该比创建1字节的小缓冲区花费更多时间(即使有缓冲区)。
我知道这应该没有太大关系,这可能就是为什么没有针对char方法引入优化的原因。如果没有人提出意外的解释,我将很高兴在不久的将来接受你的答案。
难道char不能存储在常量内存中,就像字符串一样,并且只传递它的地址吗?实际上,它甚至可以检测到'\n'是"\n"的第一个字符,并为两者都使用.LC0吗?
如果编译器可以证明这不会引起任何问题,那么是的,可以进行这种转换。但是证明这一点并不容易。例如,operator<<可以(理论上)调用chr()。它应该创建另一个缓冲区,并且不能使用之前的缓冲区(因为理论上,operator<<可以存储缓冲区的地址,并期望在chr()再次调用时发生更改)。
它是一个常量,它不会改变。并且它使用的是operator<<的const char*重载,所以它不会改变它。
当然。我指的是缓冲区的地址。理论上,operator<<可以期望传入缓冲区的地址在递归调用之间发生变化。
为什么在输出流中'\n'比"\n"更受青睐?
编译器生成的代码没有很好地解释为什么在您的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)的特定重载),但与你的示例相比,差异不那么明显。
很好的答案。让我真的很惊讶的是,默认情况下,输出char实际上委托给输出const char*。C++似乎是以性能为重点的,而这样的事情,虽然通常可以忽略不计,但仍然会出现...
是的,我也感到很惊讶。我在Godbolt上简要检查了一下,Clang和gcc都做了和gcc一样的事情。然而,MSVC则有一个特定的重载operator<< (char),见:godbolt.org/z/AQiyMw
同样;我以为普通的C++库会有一个以值传递char的ostream等效的fputc,但显然只有MSVC做到了,在x86的三个主要库中(MSVCRT,libstdc++和libc++)只有MSVC做到了。我在Godbolt上检查了libc++(clang -stdlib=libc++ godbolt.org/z/sDDgsC),它对字符串和字符都使用char* + length函数。对于未知字符串长度,它首先运行strlen。所以内部我猜它的iostream库必须使用显式长度缓冲区,这样它可以使用memcpy而不是strcpy。
编译器存在一个未优化的问题,它们不仅仅是push 0xa / mov rsi,rsp来存储和保留字符;而是分别进行字节存储,然后需要一个LEA来复制地址。愚蠢的编译器。在较大的函数内部这是有意义的,因为通常希望RSP按16对齐,所以push会将其对齐。这成为不使用push在函数入口处存储变量的初始值的一般未优化的特例,这些变量在内存中被溢出/初始化。
人们经常忘记<<是格式化输出-根据流的宽度/填充/标志要填充字符。这是可以在char和const char*之间重复使用的相当大的代码块,所以我对它们共享一个共同的实现并不感到惊讶。如果你只想输出一个字符,有一个未格式化的put。