C++中的std::ostringstream与std::string::append比较

57

在所有使用某种缓冲的示例中,我看到他们使用流而不是字符串。 std::ostringstream 和 << 操作符与使用 string.append 有何不同。哪个更快,哪个使用更少的资源(内存)。

我知道的一个区别是,您可以将不同类型(例如整数)输出到输出流中,而不仅仅是 string::append 接受的有限类型。

以下是一个示例:

std::ostringstream os;
os << "Content-Type: " << contentType << ";charset=" << charset << "\r\n";
std::string header = os.str();

对比

std::string header("Content-Type: ");
header.append(contentType);
header.append(";charset=");
header.append(charset);
header.append("\r\n");

显然使用流更短,但我认为append返回字符串的引用,所以可以这样写:

std::string header("Content-Type: ");
header.append(contentType)
  .append(";charset=")
  .append(charset)
  .append("\r\n");

使用输出流,您可以执行以下操作:

std::string content;
...
os << "Content-Length: " << content.length() << "\r\n";

但是对于内存使用和速度呢?特别是在大循环中使用时。

更新:

更明确的问题是:我应该使用哪一种,为什么?是否有某些情况下更偏向于其中一种而不是另一种?就性能和内存来说......我认为基准测试是唯一的方法,因为每种实现可能都不同。

更新2:

哦,从答案中我没有得到清晰的想法,究竟应该使用哪一个,这意味着它们中的任何一个都可以完成工作,加上vector。 Cubbi 进行了良好的基准测试,并添加了 Dietmar Kühl,最大的区别在于这些对象的构造。如果您正在寻找答案,也应该查看该测试。我会再等待一段时间以获取其他答案(请参见上一个更新),如果我没有得到答案,我认为我会接受 Tolga 的答案,因为他建议使用的向量已经被采用过了,这意味着向量应该是占用资源较少的。


离题:你还应该寻找一个快速的函数来将整数转换为字符串/字符。sprintf/itoa在执行Content-Length的简单整数到十进制字符串转换时表现不佳。 - Etherealone
sprintf 可能因为格式选项而变慢,但你认为 itoa 为什么会变慢呢? - NickSoft
我不应该在那里写itoa。我的意思是itoa不应该是一个选项,因为它是非标准的。但我记得将其与这些进行比较:https://gist.github.com/anonymous/7179097 - Etherealone
4个回答

40

构建流对象比构建字符串对象要复杂得多,因为它必须持有(并且因此构造)它的std::locale成员,以及其他需要维护状态的内容(但是地域设置明显是最重的)。

添加操作类似:两者都维护着一段连续的字符数组,在容量不足时都会进行分配。我能想到的唯一区别是在向流添加内容时,每次溢出都会有一个虚函数调用(除了内存分配/复制之外,这也是溢出处理主导的因素),而operator<<则必须对流的状态进行一些额外的检查。

另外,请注意你正在调用str()函数,它会再次复制整个字符串,因此根据你的代码编写方式,流示例执行更多操作,应该会更慢。

让我们来测试一下:

#include <sstream>
#include <string>
#include <numeric>

volatile unsigned int sink;
std::string contentType(50, ' ');
std::string charset(50, ' ');
int main()
{
 for(long n = 0; n < 10000000; ++n)
 {
#ifdef TEST_STREAM    
    std::ostringstream os;
    os << "Content-Type: " << contentType << ";charset=" << charset << "\r\n";
    std::string header = os.str();
#endif
#ifdef TEST_STRING
    std::string header("Content-Type: ");
    header.append(contentType);
    header.append(";charset=");
    header.append(charset);
    header.append("\r\n");
#endif
    sink += std::accumulate(header.begin(), header.end(), 0);
 }
}

那是1000万次重复。

在我的Linux系统上,我得到了

                   stream         string
g++ 4.8          7.9 seconds      4.4 seconds
clang++/libc++  11.3 seconds      3.3 seconds

所以,在这种情况下,在这两个实现中,字符串似乎运行速度更快,但显然两种方法都有很大的改进空间(保留字符串的reserve()函数,将流构造移出循环,使用不需要复制就能访问其缓冲区的流等等)


你忘记处理类似 std::ios_base::width 的东西了。 - Slava
@Slava在流构造的额外负载中添加了一个荣誉提及:当宽度为零时,字符串的operator<<没有任何特殊操作。 - Cubbi
11
将设置稍微改动一下,构建流程放在循环外并仅重置它(os.str("")),会以有趣的方式改变数字:在gcc上流程现在更快了,但在clang上更慢了。我得到的结果是gcc/string=4.5秒,gcc/stream=2.5秒,clang/string=2.25秒,clang/stream=4.1秒:非常好玩 ;) - Dietmar Kühl
2
所以,除非您每次都在构建流,否则它实际上与使用字符串相当。 - NickSoft

18

在内存中,std::ostringstream 不一定是作为连续的字符数组进行存储的。当发送 HTTP 头信息时,你实际上需要一个连续的字符数组,并且这可能会复制/修改内部缓冲区以使其成为连续的。

使用适当的 std::string::reservestd::string 在这种情况下没有理由比 std::ostringstream 慢。

然而,如果你不确定要保留多大的空间,std::ostringstream 在追加方面可能更快。如果你使用 std::string 并且字符串增长了,它最终需要重新分配和复制整个缓冲区。相比于否则会发生的多次重新分配,最好使用一次 std::ostringstream::str() 将数据变成连续的。

P.S. 在 C++11 之前,std::string 也不一定是连续的,虽然几乎所有库都将其实现为连续的。你可以冒险使用它,或者改用 std::vector<char>。你需要使用以下方式进行追加:

char str[] = ";charset=";
vector.insert(vector.end(), str, str + sizeof(str) - 1);

std::vector<char>出于性能考虑可能是最便宜的构造方式,但与std::string和它们实际构造所需的时间相比,这可能并不重要。我曾尝试过类似于你正在尝试的事情,并选择了std::vector<char>。纯粹是因为逻辑原因;向量似乎更适合这项工作。你实际上并不需要字符串操作之类的东西。后来我做的基准测试表明它的性能更好,或者只是因为我没有用std::string实现好操作。

在选择时,具有您需要的要求和最少额外功能的容器通常做得最好。


3
动态增长缓冲区的成本出人意料地低廉。 - jthill
1
使用适当的缓存,我同意,否则会导致持续的内存重新分配,从而降低性能。尽管 ostringstream 由于性能原因不会按顺序存储它,但这并不意味着您不能使用 str().c_str() 在连续的缓冲区中获取它。 - Havenard
1
@NickSoft 由于向量是连续的,因此您可以通过访问其第一个元素来访问其缓冲区:char const* data = &vector[0]; - Etherealone
1
@NickSoft 我认为任何头文件都不会超过2KB,这似乎是一个很好的保留值。即使您有100万个连接的客户端,它也只会使用2GB的RAM,与您的数据库服务器相比,这将是微不足道的,假设只有5〜10%的实际用户同时在线(当然,这是一个原始的假设,如果没有非常便宜的话,数据库操作可能根本不存在)。您甚至可以每天记录统计数据并进行计算,以动态找到正确的保留大小。 - Etherealone
3
这段内容的意思是:vector中的元素是存储在一起的,这意味着不仅可以通过迭代器访问元素,还可以使用指向元素的普通指针进行偏移。这意味着可以将指向vector元素的指针传递给期望指向数组元素的任何函数。(顺便提一下,建议使用cppreference.com查找信息)。 - Etherealone
显示剩余14条评论

1
使用流(stream),您可以使您的类Myclass覆盖<<操作,以便您可以编写:
MyClass x;
ostringstream y;
y << x;

如果要追加内容,您需要拥有一个ToString方法(或类似的方法),因为您无法重写字符串的append函数。

对于某些代码片段,请使用您感觉更舒适的方式。 对于更大的项目,使用流式处理在能够简单地流式传输对象时非常有用。


但是正如Dieter Lücking指出的那样,您可以使用+来附加字符串。您可以轻松地覆盖+运算符。 - NickSoft
1
真的,但不是append函数。如果你覆盖了+运算符,当编译器决定先执行其他操作或者没有覆盖所有顺序时,可能会遇到麻烦。我建议不要覆盖+运算符,除非你的类是某种标量或向量值。 - Sorin

0
如果您关心速度,您应该进行性能分析和/或测试。理论上,std::string::append 不应该比它更简单(流必须处理区域设置、不同的格式化并且更通用)更慢。但是,只有通过测试才能真正了解一个解决方案比另一个解决方案更快。

1
每种类型调用不同的函数,而这个选择是在编译时进行的。我认为这个方面并不影响性能。 - Havenard
@Havenard 我说了什么反面的话吗? - Slava

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接