Подтвердить что ты не робот

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

2013 Keynote: Chandler Carruth: оптимизация появляющихся структур С++

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

Может ли кто-нибудь поставить это в контексте этого ответа: qaru.site/info/94440/...

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

Мой вопрос, даже в контексте этого SO-ответа, есть способ рассказать, реструктурируя код каким-то другим эквивалентным способом, "ok now see, value semantics таким образом не теряет для версии выходного параметра", или комментарии Чандлера были специфическими для некоторых надуманных ситуаций? Я даже видел, как Андрей Александреску спорил об этом в беседе и говорил, что вы не сможете избежать использования ref ref для лучшей производительности.

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

4b9b3361

Ответ 1

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

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

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

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

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

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

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

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

Ответ 2

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

auto a = split(s, r);

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

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

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

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

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

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

Ответ 3

Я собирался реализовать решение, используя 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);
    }
}

Я использовал реализацию Marshall Clow string_view libС++, найденную в https://github.com/mclow/string_view.

Я опубликовал тайминги внизу ответного ответа.