std :: string是通过函数的值还是“std :: string& s”作为参数返回的?

时间:2014-02-07 19:31:09

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

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

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)

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

3 个答案:

答案 0 :(得分:6)

让我微观优化您的第二版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());
    }
}

在这种情况下,你得到3个内存分配(假设temp的缓冲区不需要在循环内重新分配),即使你迭代1000000次!这是对价值回报方法的重大改进。

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

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

<击> 如果你发现这个例子过于扭曲,那么它不是:想想std::getline()

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


更新:

  1. R上。 Martinho Fernandes在评论中明确指出他的测量结果 gcc与我的结果相矛盾,但与我的主张一致 clang和libc ++;看到 GCCClang

  2. 在他指出这些之后,我对Andrei进行了测量 Alexandrescu的例子。此刻,我无法重现他的 结果;它需要进一步分析,以了解正在发生的事情 罩。

  3. 请耐心等待,给我一些时间来消除不一致之处。

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

答案 1 :(得分:4)

第一个替代方案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宏。两种内存访问方式都适用于同一个界面,没有任何牺牲。

答案 2 :(得分:3)

为了返回一个新创建的字符串,我肯定会采用按值返回的方法。按值返回对象的典型编译器实现是让编译器为调用函数中的对象分配空间,并向其传递指向该分配空间的指针,该指针与您的引用参数基本相同,但是 one < / em>重要区别:pass-by-reference输出参数需要将对完全构造的字符串的引用传递给被结果覆盖的函数,而在返回值中case,该函数构造对象本身。

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