如何提高C++14 "str1 + str2 + str3 + ..."效率?

How to improve the efficiency of "str1 + str2 + str3 + ..." in C++14?

本文关键字:str3 效率 str2 str1 何提高 C++14      更新时间:2023-10-16
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 +

引用限定符成员函数有帮助吗?

问题的前提是:

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

将需要 n 个分配不正确。

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

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;

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

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;

没有像过度工程那样的工程

在本例中,我创建了一个类型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",无需额外分配。

现场示例

您可以使用如下代码:

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对象。

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

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

虽然它不能保证只有一个分配,但我们可以期望一个stringstream被优化以积累相对大量的数据,所以很有可能(除非输入字符串很大(它将保持分配的数量非常少。

同时,特别是如果单个字符串相当小,它肯定会避免我们a + b + c + d期望的情况,例如(至少在 C++03 中(我们希望看到在计算表达式的过程中创建和销毁许多临时对象。事实上,我们通常可以期望它得到的结果与我们对表达式模板等东西所期望的结果几乎相同,但复杂性要低得多。

不过有一些缺点:iostreams(通常(有足够的行李,例如相关的语言环境,特别是如果字符串很小,创建流的开销可能比我们在单个分配中保存的开销要多。

使用当前的编译器/库,我希望创建流的开销会使其变慢。对于较旧的实现,我必须进行测试才能确定(而且我没有足够旧的编译器来做到这一点(。

这个怎么样:

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另一种为数据保留内存。