如何提高C++14 "str1 + str2 + str3 + ..."效率?
How to improve the efficiency of "str1 + str2 + str3 + ..." in C++14?
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
另一种为数据保留内存。
- 为什么当我解模块化时,这个C++代代码"效率较低"?
- 代码的效率. 转到和函数调用
- 对于循环C++可能效率低下
- 内存效率表示最短路径的方法?
- 如何提高该函数的运行效率?
- 效率:标准::数组与标准::矢量
- 如何提高BST的搜索操作效率?
- 字符串引用参数的效率C++
- 提高基于组件的游戏引擎的效率
- 在 c++ 中使用带有映射的插入效率
- 关于效率的问题
- 在SQLITE数据库中写入记录需要花费大量时间.如何提高刀片操作效率?
- 寻求提高Microsoft密封库计算效率的方法
- 做对了一个类似竞争的问题,但需要帮助来提高效率
- C++ - 与 Numpy 中的矢量版本相比,Argsort 效率低的矢量版本实现
- C++,在对象内分配多个数据时,堆栈分配是否更有效? 在下面的程序中,类A_Heap的效率会更低吗?
- visual C++|循环效率
- 我创建了一个库,想知道设计是否效率低下
- 矩阵rowSums()与colSums(()在R与Rcpp与Armadillo中的效率
- 如何提高C++14 "str1 + str2 + str3 + ..."效率?