Как повысить эффективность & quot; str1 + str2 + str3 + … & quot; в 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 +,

Есть ли реф-классификатор функции-члены помогают?

3

Решение

Предпосылка вопроса, что:

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

потребует выделения неверно.

Вместо этого он потребует O (Log (n)) выделения. Первый s1 + s1 будет генерировать временный. Впоследствии временный (rvalue) будет левый аргумент для всех последующих + операции. Стандарт определяет, что когда lhs a string + Это значение, что реализация просто добавляет к этому временному и удаляет его:

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;

Когда увеличение геометрической емкости сочетается с оптимизацией короткой строки, значение «предварительного резервирования» правильной емкости ограничивается. Я бы потрудился сделать это только в том случае, если такой код действительно обнаружил горячую точку в вашем тестировании производительности.

12

Другие решения

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;
7

Там нет инженерии, как над инженерией.

В этом случае я создаю тип string_builder::op<?> который достаточно эффективно собирает кучу строк для объединения, а когда std::string продолжает делать это.

Он хранит копии любых временных std::strings предоставлены, и ссылки на долгоживущие, как немного паранойи.

Это в конечном итоге сводится к:

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" без дополнительных ассигнований.

живой пример

4

Вы можете использовать код как:

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

Это выделит один безымянный временный (копия первой строки), а затем добавит к нему каждую из остальных строк. Интеллектуальный оптимизатор может оптимизировать этот код в тот же код, что и код + резервный код, добавленный другими, поскольку все эти функции обычно являются встроенными.

Это работает с использованием расширенной версии оператора +, которая определяется как (примерно)

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

в сочетании с RVO, это означает, что никаких дополнительных string объекты должны быть созданы или уничтожены.

1

Подумав, я думаю, что стоит хотя бы рассмотреть немного другой подход.

std::stringstream s;

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

Хотя это не гарантирует только одно распределение, мы можем ожидать stringstream быть оптимизированным для накопления относительно больших объемов данных, так что шансы весьма хороши, что (если входные строки не огромны), это будет держать количество выделений весьма минимальным.

В то же время, особенно если отдельные строки достаточно малы, это определенно позволяет избежать ситуации, которую мы ожидаем с чем-то вроде a + b + c + d где (по крайней мере, в C ++ 03) мы ожидать чтобы увидеть ряд временных объектов, созданных и уничтоженных в процессе оценки выражения. Фактически, мы можем ожидать, что это получит примерно такой же результат, который мы ожидаем от чего-то вроде шаблонов выражений, но с гораздо меньшей сложностью.

В этом есть и обратная сторона: у iostreams (в общем) достаточно багажа, связанного с такими локалями, что, особенно если строки небольшие, при создании потока может быть больше накладных расходов, чем мы сохраняем в отдельных выделениях.

С текущим компилятором / библиотекой я бы ожидал, что накладные расходы на создание потока сделают это медленнее. С более старой реализацией мне пришлось бы тестировать, чтобы иметь какую-либо определенность (и у меня нет достаточно старого компилятора, удобного для этого).

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 другой резервирует память для данных.

0
По вопросам рекламы [email protected]