【问题标题】:Should std::string be returned by value from a function or by "std::string &s" as an argument?std::string 应该由函数的值返回还是由“std::string &s”作为参数返回?
【发布时间】:2014-03-05 08:47:53
【问题描述】:

对于从函数中返回字符串,这两者中哪一个更有效(即我应该使用哪一个):

std::string f(const std::string& s)
{
    return s + "some text";
}

void f(const std::string& s, std::string &result)
{
    result = s + "some text";
}

我知道答案可能取决于特定的编译器。但我想知道现代 C++ 代码中推荐的方法(如果有的话)是什么。

基于下面的“轨道中的轻量竞赛”评论,以下是我在 stackoverflow 上发现的一些相关问题我问了这个问题:

Are the days of passing const std::string & as a parameter over?

Passing std::string by Value or Reference

Pass by value or const reference?

"std::string" or "const std::string&" argument? (the argument is internally copied and modified)

其中没有一个回答我关于从函数返回值与将字符串作为额外参数返回的特定问题。

【问题讨论】:

  • C++11 按值返回,因为如果没有省略则移动
  • returning std::string by value as an argument 是什么意思?
  • @LightnessRacesinOrbit 是的,这个问题的措辞有点不幸,但这是一个有趣的问题!请阅读我的回答。
  • @Ali 你的观点是正确的,但你至少应该对“过早的优化是万恶之源”表示赞同。
  • @MadScienceDreams 不完全是,请查看我的更新答案!

标签: c++ string performance c++11


【解决方案1】:

让我对你的f() 的第二个版本进行微优化,并称之为g()

#include <cstdio>
#include <string>
using namespace std;

string f(const string& s) {
    return s + "some text";
}

void g(const string& s, string &result) {
    result.clear();
    result += s;
    result += "some text";
}

现在,让我们将按值返回的方法 f() 与“输出参数”方法 g() 进行比较。

按值返回:

int main(int argc, char* argv[]) {

    string s(argv[1]);

    for (int i=0; i<10; ++i) {

      string temp = f(s); // at least 1 memory allocation in each iteration, ouch!

      fprintf(stderr, "%s\n", temp.c_str());
    }
}

在每次迭代中,都有一个内存分配。分配的总数将是迭代次数 + 1,在这种情况下为 11。

“外参数”方法:

int main(int argc, char* argv[]) {

    string s(argv[1]);

    string temp; // note that this time, it is outside the loop

    for (int i=0; i<10; ++i) {

      g(s, temp);

      fprintf(stderr, "%s\n", temp.c_str());
    }
}

在这种情况下,即使您迭代 1000000 次,您也会获得 3 次内存分配(假设 temp 的缓冲区不需要在循环内重新分配)!与按价值计算的回报方法相比,这是一个显着的改进。

按值返回并依赖复制省略或移动语义是一个很好的建议,但如示例所示,在某些情况下,外参数方法会胜出(例如,当您可以重新使用缓冲区)。

out-parameters 的危险在于,在调用站点,仅通过查看代码就必须很明显,该函数正在修改它的一些参数。函数的名称必须强烈表明它正在改变它的一些参数。否则你会得到令人惊讶的结果...... :(

如果你觉得这个例子太扭曲了,那不是:想想std::getline()

对于那些认为这是过早优化的人:如果是 std::getline(),那肯定不是! 如果您将文件的行推入 std::vector 并分配一个新字符串对于每一行,它将比外参数方法(80 字节的行)慢 1.6 倍。这听起来很疯狂,因为文件 IO 应该是瓶颈,但事实并非如此,这是不必要的内存分配。详情请见Andrei Alexandrescu: Writing Quick Code in C++, Quickly 约 48 分钟。


更新:

  1. R. Martinho Fernandes 在下面的 cmets 中亲切地指出,他的测量值与 gcc 与我的结果相矛盾,但与我的主张一致 铿锵声和libc++;看 GCCClang

  2. 在他指出这些之后,我对安德烈进行了测量 亚历山德雷斯库的例子。目前,我无法复制他的 结果;它需要进一步分析,以了解在 引擎盖。

请耐心等待,给我一些时间来解决不一致的地方。

这个故事的要点是始终测量。测量了答案中提到的内存分配数量,这仍然可以(至少在我的机器)。

【讨论】:

  • @Ondřej Čertík:如果您的函数是按照您建议的方式定义的,那么分配给result 只会影响按值传递给该函数的本地副本。一旦函数返回,这些更改就会丢失,因此在这种情况下您不会返回任何内容。
  • 不,我的答案不会改变。 void f(const std::string&amp; s, std::string result) 不返回任何内容。调用者将看到未更改的结果,因为在这种情况下f() 处理了副本,并且在f() 返回后,调用者看到了他原来的result。您必须通过引用 (string&amp; result) 否则您看不到 f()result 所做的事情。
  • 我的测量结果似乎与您关于超参数的性能优势的断言相矛盾 (dl.dropboxusercontent.com/u/13779444/bench/by-val-by-ref-2.html)
  • @R.MartinhoFernandes 你能详细说明一下吗?你的实验的源代码在哪里?硬盘、SSD 或内存映射文件?你在什么机器/操作系统上运行你的代码?哪些编译器和优化标志?输入是什么?我可以继续,但长话短说,我需要更多细节。注意:我只是提到了 Alexandrescu 的实验,我没有没有在这个特定的例子中自己进行测量。
【解决方案2】:

第一个替代方案return s + "some text"; 更简单。它在内存分配方面的行为也很简单:首先评估s + "some text,可能会导致分配一个具有足够容量来保存结果的新string 对象。该对象是返回值,假设复制省略,否则发生移动。

正如 Ali 所说,第二个接口让用户有机会在多次调用中重用字符串缓冲区。使用该功能需要更多的代码,并且会产生更多的复杂性。

此外,根据他的测量结果,通常很难判断哪个真正获胜。幸运的是,有一条中庸之道:

#if STRING_BUFFER_REUSE_OPTIMIZATION

string h( string const & s, string && result = {} ) {
    result.clear();
    result += s;
    result += "some text";
    return std::move( result );
}

#else

string const no_hint = {};

string h( string const & s, string const & hint = no_hint ) {
    return s + "some text";
}

#endif

有了这个,您可以根据每个构建目标上的即时测量来设置STRING_BUFFER_REUSE_OPTIMIZATION 宏。两种内存访问方式都适用于相同的接口,没有任何牺牲。

【讨论】:

  • +1 来自我。注:进一步的测量由 R. Martinho Fernandes 完成;他指出,他的时间安排与我的说法相矛盾。我仍然必须了解代码的奇怪行为。你的回答很好,也回答了这个问题,点赞!
  • 如果您有兴趣:I cannot reproduce R. Martinho Fernandes' timings。我仍然没有看到它证明在这种情况下按值传递方法可能会更快。
【解决方案3】:

对于返回一个新创建的字符串,我肯定会采用按值返回的方法。按值返回对象的典型编译器实现是让编译器在调用函数中为对象分配空间,并将指向该分配空间的指针传递给它,这与您的引用参数基本相同,但使用 one 重要区别:按引用传递输出参数需要将对完全构造字符串的引用传递给被结果覆盖的函数,而在按值返回中在这种情况下,函数自己构造对象。

请注意,在一个特定的用例中,传递引用的解决方案更快:如果调用者重复调用此函数来更改同一个变量,则函数内部的覆盖正是需要的,同时返回和在调用者中分配将导致结果在一个临时对象中构造,该临时对象被(移动)分配给调用者端的变量。如果您使用 C++11 之前的编译器,它甚至会被复制分配。

【讨论】:

  • 感谢迈克尔的回答。它认为这与阿里的回答是一致的。我接受了他的,因为它提供了详细的代码。希望没事。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-11-10
  • 2016-10-26
  • 1970-01-01
  • 2013-10-15
  • 2020-02-17
相关资源
最近更新 更多