семантика значений против выходных параметров с большими структурами данных

2013 Keynote: Чендлер Каррут: Оптимизация новых структур C ++

  • 42:45
    Вам не нужны выходные параметры, у нас есть семантика значений в C ++. … Каждый раз, когда вы видите, что кто-то утверждает, что nonono я не собираюсь возвращать по значению, потому что копирование будет стоить слишком дорого, кто-то, работающий над оптимизатором, говорит, что они не правы. Отлично? Я никогда еще не видел кусок кода, где этот аргумент был правильным. … Люди не понимают, насколько важна семантика значений для оптимизатора, потому что он полностью проясняет сценарии наложения.

Может кто-нибудь поставить это в контексте этого ответа: https://stackoverflow.com/a/14229152

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

Мой вопрос, даже в контексте этого SO ответа, есть ли способ сказать, реструктурируя код какой-то другой эквивалент Кстати, «хорошо, теперь видите, семантика значений таким образом не теряется для версии выходного параметра», или комментарии Чендлера были специфичны для некоторых надуманных ситуаций? Я даже видел, как Андрей Александреску спорил в разговоре и сказать, что вы не можете избежать использования ref output для лучшей производительности.

Для другого взгляда на комментарии Андрея см. Эрик Ниблер: Out-параметры, семантика перемещения и алгоритмы с сохранением состояния.

10

Решение

Это либо преувеличение, обобщение, шутка, либо идея Чендлера «Совершенно разумная производительность» (с использованием современных наборов инструментов / библиотек C ++) неприемлема для моих программ.

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

Конечно, RBV может быть отличной вещью — он не подходит для всех случаев. Даже ссылка, на которую вы ссылались, указала, как можно избежать тонны распределений / освобождений. Реальные программы и найденные в них структуры данных намного сложнее.

Позже в разговоре он продолжает критиковать использование функций-членов (ссылка: S::compute()). Конечно, есть смысл отнять, но действительно ли разумно избегать использования этих языковых функций полностью, потому что это облегчает работу оптимизатора? Нет. Всегда ли это приведет к созданию более читаемых программ? Нет. Будут ли эти преобразования кода всегда приводить к значительно более быстрым программам? Нет. Оправдывают ли изменения, необходимые для преобразования вашей кодовой базы, то время, которое вы вкладываете? Иногда. Можете ли вы убрать некоторые моменты и принять более обоснованные решения, которые влияют на некоторые из ваших существующих или будущих кодовых баз? Да.

Иногда это помогает понять, как именно будет выполняться ваша программа или как она будет выглядеть в C.

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

Это нормально, чтобы рассмотреть предложенные изменения; в идеале вы должны измерить влияние таких изменений в реальном времени выполнения и влияние на ваш исходный код, прежде чем принимать эти предложения.

Для вашего примера: даже копирование + присвоение больших структур по значению может иметь значительные затраты. Помимо затрат на запуск конструкторов и деструкторов (наряду с их созданием / очисткой ресурсов, которые они приобретают и владеют, как указано в ссылке, на которую вы ссылаетесь), даже такие простые вещи, как избежание ненужных копий структуры, могут сэкономить вам массу ЦП. время, если вы используете ссылки (при необходимости). Копия структуры может быть такой же простой, как memcpy, Это не надуманные проблемы; они появляются в реальных программах, и их сложность может значительно возрасти. Оправдывает ли сокращение псевдонимов некоторой памяти и других оптимизаций затраты, и приводит ли это к «Совершенно разумной производительности»? Не всегда.

3

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

Проблема с выходными параметрами, как описано в связанном вопросе, состоит в том, что они обычно составляют общий случай вызова (т. Е. У вас нет vector хранилище использовать уже) гораздо более многословно, чем обычно. Например, если вы использовали return by value:

auto a = split(s, r);

если вы использовали выходные параметры:

std::vector<std::string> a;
split(s,r,a);

Второй выглядит много менее красивым для моих глаз. Кроме того, как упоминал Чендлер, оптимизатор может сделать гораздо больше с первым, чем со вторым, в зависимости от остальной части вашего кода.

Есть ли способ, которым мы можем получить лучшее из обоих миров? многозначительно да, используя семантику перемещения:

std::vector<std::string> split(const std::string &s, const std::regex &r, std::vector<std::string> v = {})
{
auto rit = std::sregex_token_iterator(s.begin(), s.end(), r, -1);
auto rend = std::sregex_token_iterator();
v.clear();
while(rit != rend)
{
v.push_back(*rit);
++rit;
}

return v;
}

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

int main()
{
const std::regex r(" +");
std::vector<std::string> a;
for(auto i=0; i < 1000000; ++i)
a = split("a b c", r, std::move(a));
return 0;
}

Это работает так же быстро, как метод выходного аргумента, и то, что происходит, совершенно ясно. Вам не нужно постоянно усложнять использование вашей функции, просто чтобы иногда добиться хорошей производительности.

1

Я собирался реализовать решение с помощью string_view а также диапазоны а потом нашел это:

Это одобряет семантика значения вывод по возврату и я принимаю это как красивее, чем текущий выбор в указанном SO ответе. Этот дизайн также будет принимать характер источник вне функции, хотя это возвращается. Простая иллюстрация: можно получить внешнюю зарезервированный вектор существо повторно заполнено возвращенным диапазоном.

В любом случае, я не уверен, есть ли такая версия split может помочь оптимизатору в любом смысле (я говорю в контексте разговора Чендлера здесь).

уведомление

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

Пример решения

В то время как std::split не прибыл, я осуществил семантика значения вывод по возврату версия таким образом:

#include <string>
#include <string_view>
#include <boost/regex.hpp>
#include <boost/range/iterator_range.hpp>
#include <boost/iterator/transform_iterator.hpp>

using namespace std;
using namespace std::experimental;
using namespace boost;

string_view stringfier(const cregex_token_iterator::value_type &match) {
return {match.first, static_cast<size_t>(match.length())};
}

using string_view_iterator =
transform_iterator<decltype(&stringfier), cregex_token_iterator>;

iterator_range<string_view_iterator> split(string_view s, const regex &r) {
return {
string_view_iterator(
cregex_token_iterator(s.begin(), s.end(), r, -1),
stringfier
),
string_view_iterator()
};
}

int main() {
const regex r(" +");
for (size_t i = 0; i < 1000000; ++i) {
split("a b c", r);
}
}

Я использовал Маршалла Клоу string_view Реализация libc ++ найдена в https://github.com/mclow/string_view.

Я разместил время в нижней части рекомендованный ответ.

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