为什么'\n'在输出流中被认为优于"\n"?

12 浏览
0 Comments

为什么'\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

#include 
void 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字面量的方法不仅不好,而且比字符串字面量更糟糕?

0
0 Comments

为什么在输出流中'\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<<可以期望传入缓冲区的地址在递归调用之间发生变化。

0
0 Comments

'\n'在输出流中优于"\n"的原因是,'\n'是一个字符,而"\n"是一个字符串。在执行函数时,'\n'只需要更少的指令,而"\n"需要更多的指令,这最终会使它更慢。此外,'\n'的大小为1,而"\n"的大小为2,所以执行函数的速度可能更快。

解决方法是使用更大的迭代次数,并将输出重定向到文件或"/dev/null",这样差异应该更小。另外,关闭"std::sync_with_stdio"可以提高性能约30%。

尽管在理论上'\n'可能更快一些,但在实际情况下差异并不明显,因此选择使用'\n'或"\n"主要取决于个人偏好和代码的可维护性。

0
0 Comments

为什么在输出流中'\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。

0