如何在C ++ 14中提高“str1 + str2 + str3 + ...”的效率?

时间:2014-09-09 23:58:08

标签: c++ c++11 move-semantics rvalue-reference ref-qualifier

std::string Concatenate(const std::string& s1,
                        const std::string& s2,
                        const std::string& s3,
                        const std::string& s4,
                        const std::string& s5)
{
    return s1 + s2 + s3 + s4 + s5;
}

默认情况下,return s1 + s2 + s3 + s4 + s5;可能等同于以下代码:

auto t1 = s1 + s2; // Allocation 1
auto t2 = t1 + s3; // Allocation 2
auto t3 = t2 + s4; // Allocation 3

return t3 + s5; // Allocation 4

有一种优雅的方法可以将分配时间减少到1吗?我的意思是保持return s1 + s2 + s3 + s4 + s5;不变,但效率会自动提高。如果可能,它还可以避免程序员滥用std::string::operator +

ref-qualifier 成员函数有帮助吗?

6 个答案:

答案 0 :(得分:12)

问题的前提是:

s1 + s2 + s3 + s4 + s5 + ... + sn

将要求n分配不正确。

相反,它将需要O(Log(n))分配。第一个s1 + s1将生成一个临时的。随后,临时(rvalue)将成为所有后续+操作的左参数。该标准规定,当string +的lhs为rvalue时,该实现只是附加到该临时值并将其移出:

operator+(basic_string<charT,traits,Allocator>&& lhs,
          const basic_string<charT,traits,Allocator>& rhs);

Returns: std::move(lhs.append(rhs))

该标准还规定了字符串的容量将以几何方式增长(1.5和2之间的因子是常见的)。因此,在每次分配时,容量将以几何方式增长,并且该容量沿着+操作链传播。更具体地说,原始代码:

s = s1 + s2 + s3 + s4 + s5 + ... + sn;

实际等同于:

s = s1 + s2;
s += s3;
s += s4;
s += s5;
// ...
s += sn;

当几何容量增长与短串优化相结合时,“预先保留”正确容量的值是有限的。如果这样的代码实际上显示为性能测试的热点,我只会费心去做。

答案 1 :(得分:7)

std::string combined;
combined.reserve(s1.size() + s2.size() + s3.size() + s4.size() + s5.size());
combined += s1;
combined += s2;
combined += s3;
combined += s4;
combined += s5;
return combined;

答案 2 :(得分:4)

没有像过度工程这样的工程。

在这种情况下,我创建了一个类型string_builder::op<?>,它可以合理有效地收集一堆字符串以进行连接,并在转换为std::string时继续这样做。

它存储所提供的任何临时std::string的副本,以及对较长寿的副本的引用,作为一点偏执。

最终减少到:

std::string retval;
retval.reserve(the right amount);
retval+=perfect forwarded first string
...
retval+=perfect forwarded last string
return retval;

但它包含了大量的合成糖。

namespace string_builder {
  template<class String, class=std::enable_if_t< std::is_same< String, std::string >::value >>
  std::size_t get_size( String const& s ) { return s.size(); }
  template<std::size_t N>
  constexpr std::size_t get_size( const char(&)[N] ) { return N; }
  template<std::size_t N>
  constexpr std::size_t get_size( char(&)[N] ) { return N; }
  std::size_t get_size( const char* s ) { return std::strlen(s); }
  template<class Indexes, class...Ss>
  struct op;
  struct tuple_tag {};
  template<size_t... Is, class... Ss>
  struct op<std::integer_sequence<size_t, Is...>, Ss...> {
    op() = default;
    op(op const&) = delete;
    op(op&&) = default;
    std::tuple<Ss...> data;
    template<class... Tuples>
    op( tuple_tag, Tuples&&... ts ): data( std::tuple_cat( std::forward<Tuples>(ts)... ) ) {}
    std::size_t size() const {
      std::size_t retval = 0;
      int unused[] = {((retval+=get_size(std::get<Is>(data))), 0)..., 0};
      (void)unused;
      return retval;
    }
    operator std::string() && {
      std::string retval;
      retval.reserve( size()+1 );
      int unused[] = {((retval+=std::forward<Ss>(std::get<Is>(data))), 0)..., 0};
      (void)unused;
      return retval;
    }
    template<class S0>
    op<std::integer_sequence<size_t, Is..., sizeof...(Is)>, Ss..., S0>
    operator+(S0&&s0)&& {
      return { tuple_tag{}, std::move(data), std::forward_as_tuple( std::forward<S0>(s0) ) };
    }
    auto operator()()&& {return std::move(*this);}
    template<class T0, class...Ts>
    auto operator()(T0&&t0, Ts&&... ts)&&{
      return (std::move(*this)+std::forward<T0>(t0))(std::forward<Ts>(ts)...);
    }
  };
}
string_builder::op< std::integer_sequence<std::size_t> >
string_build() { return {}; }

template<class... Strings>
auto
string_build(Strings&&...strings) {
  return string_build()(std::forward<Strings>(strings)...);
}

现在我们得到:

std::string Concatenate(const std::string& s1,
                        const std::string& s2,
                        const std::string& s3,
                        const std::string& s4,
                        const std::string& s5)
{
  return string_build() + s1 + s2 + s3 + s4 + s5;
}

或更具普遍性和效率:

template<class... Strings>
std::string Concatenate(Strings&&...strings)
{
  return string_build(std::forward<Strings>(strings)...);
}

有无关的动作,但没有多余的分配。它适用于原始"strings"而没有额外的分配。

live example

答案 3 :(得分:1)

您可以使用以下代码:

std::string(s1) + s2 + s3 + s4 + s5 + s6 + ....

这将分配一个未命名的临时(第一个字符串的副本),然后将每个其他字符串附加到它。智能优化器可以将其优化为与其他人发布的保留+附加代码相同的代码,因为所有这些功能通常都是可以内联的。

这可以通过使用operator +的移动增强版本来实现,其定义为(粗略地)

std::string operator+(std::string &&lhs, const std::string &rhs) {
    return std::move(lhs.append(rhs));
}

结合RVO,意味着不需要创建或销毁其他string个对象。

答案 4 :(得分:0)

经过一番思考,我认为至少考虑一种稍微不同的方法可能是值得的。

std::stringstream s;

s << s1 << s2 << s3 << s4 << s5;
return s.str();

虽然它并不能保证只有一次分配,但我们可以期望优化stringstream来累积相对大量的数据,所以很有可能(除非输入字符串很大)它会使分配数量保持在最低水平。

同时,特别是如果单个字符串相当小,它肯定避免了我们期望的情况,例如a + b + c + d,其中(至少在C ++ 03中)我们期望在评估表达式的过程中查看创建和销毁的一些临时对象。实际上,我们通常可以预期这会得到与表达模板之类的结果相同的结果,但复杂性要低得多。

虽然存在一些不利因素:iostream(一般情况下)有足够的行李,例如关联的语言环境,特别是如果字符串很小,创建流的开销可能比我们保存在单独的分配中要多。

使用当前的编译器/库,我预计创建流的开销会使速度变慢。对于较旧的实现,我必须进行测试以确定是否具有任何确定性(并且我没有足够的编译器方便这样做)。

答案 5 :(得分:0)

这个怎么样:

std::string Concatenate(const std::string& s1,
                        const std::string& s2,
                        const std::string& s3,
                        const std::string& s4,
                        const std::string& s5)
{
    std::string ret;
    ret.reserve(s1.length() + s2.length() + s3.length() + s4.length() + s5.length());
    ret.append(s1.c_str());
    ret.append(s2.c_str());
    ret.append(s3.c_str());
    ret.append(s4.c_str());
    ret.append(s5.c_str());
    return ret;
}

有两个分配,一个非常小,可以为数据构建std::string另一个预留内存。