Comments 76
Заранее спасибо.
Не могли бы вы привести пример
Пример чего? Того, что там будет копирование? Это очевидно.
выразится в измеримых секундах
То, что это выражается — так же очевидно. Но вы начните с измерений move — после этого уже можно будет поговорить о копировании.
Заранее спасибо.
Пока что очевидно только то, что реального кода у вас нет, есть только упоение собственной крутостью.Не могли бы вы привести примерПример чего? Того, что там будет копирование? Это очевидно.
Но вы начните с измерений move — после этого уже можно будет поговорить о копировании.А давайте и попробуем. Заодно уж и на PF посмотрим. Вот такой вот PF:
#include <utility>
template <typename T1, typename T2>
long foo(T1&& x, T2&& y) {
return std::forward<T1>(x) + std::forward<T2>(y);
}
template long foo<long, long>(long&&, long&&);
long foo(long x, long y) {
return x + y;
}Годится? А к нему — вот такой вот std::move:#include <iostream>
#ifdef WITH_PF
template <typename T1, typename T2>
long foo(T1&& x, T2&& y);
#else
long foo(long x, long y);
#endif
int main() {
long x = 0;
for (long i = 0; i < 1000000000; ++i) {
#if defined(WITH_MOVE) || defined(WITH_PF)
long y = i;
x = foo(std::move(x), std::move(y));
#else
x = foo(x, i);
#endif
}
std::cout << x << std::endl;
}Пойдёт?А вот и результаты замеров:
$ g++ -O3 test1.cc test2.cc -o without_move && time ./without_move 499999999500000000 real 0m5.979s user 0m5.971s sys 0m0.008s $ g++ -O3 test1.cc test2.cc -DWITH_MOVE -o with_move && time ./with_move 499999999500000000 real 0m5.975s user 0m5.973s sys 0m0.001s $ g++ -O3 test1.cc test2.cc -DWITH_PF -o with_pf && time ./with_pf 499999999500000000 real 0m7.374s user 0m7.361s sys 0m0.008sКак видим
std::move — ничего не стоит (как и ожидалось), а вот ваш любимый pf… да, таки весьма небесплатен.P.S. Только не нужно рассказывать сказок о том, что так никто писать не будет и вообще пример из пальца высосан. Ибо я вполне наблюдал подобные эффекты в коде «уверовавших в pf» — вполне себе в production, не в специальных тестах. Могу даже попробовать рассказть где. А вот эффектов, на которые жалуется Antervis… не наблюдал. Я не говорю, что их нет и никогда не бывает — я просто предлагаю обсуждать их на примерах похожих на реальные. Где вы хотя бы могли объяснить — где вы видели подобный код, как часто и почему. Вот только после этого — можно будет решить: насколько это практически полезный совет.
тестировать мув против копирования на тривиальных типах? Это что-то новенькое.Уверяю вас: «тривиальные» типы встречаются в программах на два, а то и три порядка чаще, чем те типы, для которых pf и связанные с ним сложности (комбинаторный взрыв и всю возня с инстанциированием нужных вам типов аргументов) имеет смысл.
Напомню: бывают не только классы с дорогим копированием, но и с дорогим мувом.Разумеется. Любая экзотика, которую кто-либо когда-либо придумал может встретиться в реальном мире, если её специально не запретили.
Вопрос ведь не в этом. Вопрос, это… в цене вопроса. Насколько часто вы встречаете такие типы, насколько велика вероятность того, что для них вы не сделаете правильного конструктора и насколько разрушительным будут последствия для скорости работы программы и её размера.
Так вот, практически я ни разу не встречал класса, у которого был бы дорогой оператор перемещения, но при этом существовал бы оператор копирования. Вот ни разу. Да, наверное такие бывают, но… не встречал. Если вы с таким сталкиваетесь регулярно — то хоть расскажите откуда они берутся-то.
В то же время тривиальные типы (а «тривиальные типы», могут быть, в общем-то, и «замороченными»,
std::tuple<std::complex<float>, std::complex<float>>, например) — встречаются куда чаще.На моей практике «инвертированное» правило из Мейрса (используйте
Foo в качестве параметра конструктора в тех случаях, когда тип может копироваться и Foo&& в тех случаях, когда это невозможно) работает куда лучше, чем «камлание» на pf.Вот ни разу. Да, наверное такие бывают, но… не встречал
бустовые small_vector, flat_set/flat_map являются типовыми примерами.
В то же время тривиальные типы (а «тривиальные типы», могут быть, в общем-то, и «замороченными», std::tuple<std::complex<float>, std::complex<float>>, например) — встречаются куда чаще.
в вашем бенчмарке вы эксплуатировали особенности соглашения о вызовах x86. Тривиальные типы с несколькими полями могут не влазить в xmm и передаваться через стек. Тогда передача rvalue будет малость дешевле чем по значению.
В целом я согласен, что в подавляющем большинстве случаев передача по значению дает простейший код и сабоптимальное быстродействие. В статье, как я и отметил, меня смутило то, что автор даже не рассмотрел оптимальные варианты.
бустовые small_vector, flat_set/flat_map являются типовыми примерами.И, думаю, по этой самой причине у нас они и не используются. flat_map/flat_set у нас свои — у них
move дешёвый.В статье, как я и отметил, меня смутило то, что автор даже не рассмотрел оптимальные варианты.Статья, если бы вы внимательно её прочитали — вообще не о тонкостях C++. Она о подходах C++ в сравнении с другими языками. Делать акцент на варианты, которые используются редко — в этом случае было бы странно.
Так вот, практически я ни разу не встречал класса, у которого был бы дорогой оператор перемещения, но при этом существовал бы оператор копирования. Вот ни разу. Да, наверное такие бывают, но… не встречал. Если вы с таким сталкиваетесь регулярно — то хоть расскажите откуда они берутся-то.
Любой класс, написанный до C++11, или где автор просто забыл написать конструктор перемещения. И таких классов в реальном коде полно.
Я ничего, в общем, не имею против классов, которые нельзя копировать вообще — это нормально. Их принимать через ссылку — вполне разумно (собственно а как их иначе принять?). Но вот класс, который можно копировать, но нельзя перемещать… вы гигиеной кода в приниципе никогда не занимаетесь? Или считаете что раз написанный код никогда не должен меняться?
Просто удивляет этот подход: мы, когда-то давным-давно, сделали плохой дизайн… потому что тогда, давным-давно, ничего лучшего не получалось… давайте теперь городить костыли до скончания веков… так что ли?
А вам не приходило в голову, что люди иногда пользуются сторонними библиотеками?
Например, я на работе пользуюсь большим фреймворком, написанным начиная с ранних 90-х. Несколько лет назад авторы начали переделывать его на C++11, избавились от своего самописного аналога auto_ptr, от своего костыля заменяющего перемещение, и т.д., но работа не закончена.
А вам не приходило в голову, что люди иногда пользуются сторонними библиотеками?Приходило. Но мне казалось, что сторонние библиотеки используются тогда, когда они облегчают написание кода, а не усложняют его.
Например, я на работе пользуюсь большим фреймворком, написанным начиная с ранних 90-х.В этом случае вам остаётся только посочувствовать… и, разумеется, вам придётся следовать гайдлайнам этой библиотеки. Если речь идёт о том, чтобы получить результат «здесь и сейчас».
Но вот использовать какой-нибудь
boost::container::small_vector в случае, если у вас его ещё нет — я бы не стал. Выигрыш, скорее всего, себя не окупит.Но вот использовать какой-нибудь boost::container::small_vector в случае, если у вас его ещё нет — я бы не стал. Выигрыш, скорее всего, себя не окупит.
Я посмотрел код, и в boost::small_vector вроде определены конструктор перемещения и перемещающий оператор присваивания. Так что не знаю, на что жаловался Antervis
И да, откуда вы взяли вот это:
Но вот класс, который можно копировать, но нельзя перемещать…
Я ничего такого не говорил, опять вы выдумываете.
Я говорил про классы без определенного конструктора перемещения, у которых перемещение автоматически превращается в копию.
Об этом говорили не вы, но Скотт Мейерс.Но вот класс, который можно копировать, но нельзя перемещать…Я ничего такого не говорил, опять вы выдумываете.
Вся идея получения объектов в конструкторе (вместо ссылок), которые вы потом
std::moveаете в нужное вам поле — заключается в том, чтобы избежать комбинаторного взрыва (который в ином случае неизбежен: даже если вы используете шаблоны и pf — компилятору всё равно придётся породить множество вариантов, даже если у вас в коде конструктор будет один).Для объектов, которые вы можете перемещать, но не копировать — этой проблемы не существует (строго говоря и для объектов, которые можно копировать, но не перемещать — тоже… но это такая экзотика, что я об этом даже не задумывался никогда), так зачем для них этот трюк использовать?
Я говорил про классы без определенного конструктора перемещения, у которых перемещение автоматически превращается в копию.Замечание принимается. Я просто как-то позабыл о том, что в 2019м году такие классы могут использоваться где-либо, кроме «сурового Legacy». Но для них комбинаторного взрыва тоже нет, так что можно спокойно использовать ссылки, никакого PF, опять-таки, заводить не требуется.
Наверное можно попробовать использовать PF для того, чтобы разработать «универсальный путь к счастью»… но мне кажется, что проще начать, наконец, добавлять конструкторы перемещения в подобные классы — особенно если речь идёт о чём-то достаточно тяжёлом для того, чтобы копирование было неприемлемо по производительности.
Опять вы отвечаете на что-то, чего я никогда не говорил. Я не призывал использовать PF, вы меня путаете с кем-то другим.
Я же только хотел указать, что есть случаи, когда "передача по значению + move" является плохим вариантом. И да, в таком случае, единственный разумный вариант — это передавать по константной ссылке.
Я просто как-то позабыл о том, что в 2019м году такие классы могут использоваться где-либо, кроме «сурового Legacy».
Это встречается гораздо чаще, чем вы полагаете. В науке и промышленности — так повсеместно. Потому что сам код не является продуктом, и его качество не сильно кого-то беспокоит. Ну и работает — не трожь во всей красе.
Потому что сам код не является продуктом, и его качество не сильно кого-то беспокоит.Зачем он тогда создаётся на языке, единственным достоинством которого является строгое следование принципу «не плати за то, что не используешь» (что, собственно, даёт возможность сделать качественный код — и, в общем, ничего более)?
Вот это удивляет больше, чем что-либо другое…
Ну если так — то это совсем другая история, к обычному программированию на C++ это всё имеет мало отношения.
Я не знаю ни одного реального проекта, полностью реализованного в одной единице трансляции.
да и любые тесты надо проверять в ассемблере — тестировать вывод константы неинтересно.
я сам так обманывался при тестировании производительности много раз — даже хуже, думая что обманул оптимизатор, ан нет… =)
если что, то от вашей программы осталось
cout << 499999999500000000; test1.cc и test2.cc не зря приводил.Цикл есть, но разницы в коде нет…
gcc 6.3
std::move и без — то разницы и не должно быть (хотя иногда она есть: вот, например… кашу маслом, может, и не испортишь, а C++-программу лишним std::move — можно).Но главное отличие — вот оно. Ссылка может быть сколь угодно
perfect… но ссылка — это ссылка. Это не передача объекта по значению. То есть избавиться от того, чтобы думать… мы — всё-таки не можем.А это лишает предложения «используйте
std::forward и pf — и компилятор сам разберётся» главного преимущества: нет, если компилятор не видит всей программы — он не разберётся… а там где видит — так он разберётся и без pf… так почему мы должены на pf молиться, вдруг?string_view — это специальный класс, который не владеет данными. Если вы хотите сделать копию данных и передать её в класс, это нужно делать явно на стороне вызывающего.
вы хотите сделать копию данных и передать её в класс, это нужно делать явно на стороне вызывающего
Я? Нет, лично я не хочу делать копию.
Если class person хочет у себя внутри иметь копию строк — это его право и, в общем случае, детали реализации.
Сваливая бремя конструирования этих строк на клиента, вы потенциально загрязняете ему код:
std::string_view First = ..., Last = ...;
person Person{std::string{First}, std::string{Last}};— Красота же, да?
Принимайте string_view, он неявно и практически бесплатно конструируется из всего, и копируйте его потом себе как хотите (если надо).
Единственный «недостаток» — да, нельзя переместить rvalue-строку внутрь объекта (и этим сэкономить аж одно копирование). Если у вас есть сомнения в эффективности — сначала профилируйте, и только если вы действительно можете что-то выиграть на этих временных объектах — оптимизируйте.
class D { // Good
string s1;
public:
A(string_view v) : s1{v} { }
// GOOD: directly construct
};… могу ли я замувать существующую строку внутрь D так, чтобы было вообще ровно ноль копирований?
std::string s{"abc"}; // взята откуда-то и потом больше не нужна
D d{std::move(s)}; // orly?
А теперь представим, что там не firstname/lastname, а какие-до данные произвольной длины. Или же объект произвольной структуры (общий случай).
Единственный «недостаток» — да
sv — не си-строка, но прозрачно создаётся из си-строки со всеми вытекающими.
Если у вас есть сомнения в эффективности — сначала профилируйте, и только если вы действительно можете что-то выиграть на этих временных объектах — оптимизируйте.
Никакой профайлер ничего не покажет. Подобные советы говорят лишь о крайне странных представлениях о профайлере и отсутствии опыта работы с ним(кроме как в примитивных случаях).
Хорошо. Мы имеет оверхед на передаче. У нас тысячи функций, передаются десятки, сотни разных типов объектов. Всё это будет размазано и десяткам/сотням разных функций. Как мы вообще поймём, что проблема есть и как мы её локализуем? Да никак.
Даже если мы увидим много memcpy/malloc, то это не всегда нам позволит найти и локализовать причину. Ну узнаем мы, что из вызывают эти десятки/сотни функции(вместе с её тысячами других). Какие из этих конструкторов лишние?
Поэтому, никакой профайлер тут почти никогда не поможет. И проще изначально определиться с правильным подходом и его использовать.
Подобные советы говорят лишь о крайне странных представлениях о профайлере и отсутствии опыта работы с ним(кроме как в примитивных случаях).Не обязательно. Гораздо чаще они свидетельствуют о том, что человек никогда не пытался сравнивать свой код с кодом, который писался людьми, которые реально думают о производительности.
Я с этим феноменом сталкивался много раз: люди считают, что если они могут ускорить код раза в 3-4 с помощью профайлера — то это указывает на то, что они всё сделали «правильно». Тот факт, что можно сделать «неправильно», так что профайлер применить куда-либо будет очень сложно, но всё будет работать ещё раз так в 5-6 быстрее… им обычно в голову не приходит.
Всё это довольно-таки грустно и напоминает попытку поставить на автомобиль Формулы-1 автоматическую коробку передач и шипованную резину.
Если вас устраивает вариант, когда код работает в 3-5 раз медленнее, чем он мог бы работать… и вы готовы с подобным замедлением мириться ради «красоты кода»… то зачем вы вообще связались с C++? C# или Java дадут вам желаемое с куда меньшими затратами!
людьми, которые реально думают о производительности
«У меня есть
Люди, верящие, что они и без профайлера знают, что надо делать, могут себя не жалеть, о
Закончив в пятницу и убедившись, что они ускорили код в 10 (допустим) раз они идут на хабр делиться мудростью и предвкушать премию, а тем временем выясняется, что:
— 10 раз — это 1 мкс вместо 10 мкс;
— этот идеально оптимизированный код вызывается исключительно в контексте чтения из / записи в БД, выполняющемся на 3 порядка дольше, и теперь работа с БД занимает не 5 с, а 5 с;
— либо этот код вызывается исключительно когда пользователь заполняет форму, и заполнение формы теперь занимает не от 2 минут до 3 часов, а от 2 минут до 3 часов;
— либо этот код вызывается 1-2 раза за всё время работы, и теперь программа майнит монету не неделю, а неделю;
— либо этот код в 99 случаях из ста работает с джонами смитами, элтонами джонами и риками санчезами, и только в одном случае с даздрапермой череззаборногузадерищенской, так что SSO превращает все ручные оптимизации в тыкву;
— либо этот класс всегда живет меньше, чем его агрументы, поэтому можно было ничего не копировать и хранить указатели / ссылки;
— и т.д. и т.п.
А менеджеру такого гуру оптимизации теперь надо как-то объяснить клиенту, на что потрачена неделя и за что тот заплатил условный килобакс денег.
Ну и решить, что дальше делать с гурой.
«Premature optimization is the root of all evil». ©
Замеряйте.
«Premature optimization is the root of all evil». ©Да легко. Вспоминаем вторую фразу из той же самой статьи Кнута:
Замеряйте.
In established engineering disciplines a 12% improvement, easily obtained, is never considered marginal; and I believe the same viewpoint should prevail in software engineering.
А вот теперь — можно и заменять. И да — разумеется мерить нужно не скорость передачи аргументов в конструктор.
«У меня естьНе совсем так. C++ — это не один молоток. Это большая коллекция молотков. Самых разнообразных — но неизменно сложных и опасных.молотокC++ и теперь всё вокруггвоздинуждается в оптимизации».
Однако если мы начинаем обсуждать вопрос «а нам, так-то, гвозди забивать и не нужно» — то это значит, что мы неправильно выбрали ящик с инструментами. Изначально.
Потому что в большой коллекции C++ ничего, кроме молотков и нету. Иными словами: если вас устраивает код, в 3-5-10 медленнее, чем оптимальный — то вам не нужно использовать C++ вообще.
Не используйте «ящик с молотками» потому что он модный популярный, возьмите C#, Java (а то и Python/Mathlab какой-нибудь) — и не нужно будет произносить идиотских мантр про «premature optimizations».
«Premature optimization is the root of all evil». ©
Допустим, я знаю, что std::move «вот здесь»
>>Даже если мы увидим много memcpy/malloc
Уже будет хорошим поинтом чтобы насторожиться. Дальше в том же профилировщике открывается статистика по call site.
Тот же Втюн вполне может спуститься на уровень базовых блоков (basic block) и показать потерю времени в куске, связанном с конструктором.Однако ни один VTune не может рассказать вам как избежать кеш-промахов… а это — самое важное, что есть в оптимизации вообще. Один промах с необходимостью похода в память — эквивалентен сотням операций и даже L2/L3 даёт не такой большой выигрыш, чтобы этим можно было пренебречь.
L1 же имеет размер, во-первых крошечный, а во-вторых — фактически не меняющийся со временем: Zen2 имеет кеш в 32Kb, то есть примерно столько же, сколько было в кеше PowerPC 601 четверть века назад!
Уже будет хорошим поинтом чтобы насторожиться. Дальше в том же профилировщике открывается статистика по call site.Ну обнаружили мы, что это произошло из-за очень SOLIDной архитектуры, которая «размазала» десяток счётчиков (которые нам реально нужны для реализации алгоритма) по множеству разнообразных, хитро связанных между собой структур данных, коих у нас насчитывается порядка полутысячи (реальный пример из реальной программы). Ваши дальнейшие действия?
И не забывайте про аппаратный префетч, к которому можно добавить и программный.
>«размазала» десяток счётчиков (которые нам реально нужны для реализации алгоритма) по множеству разнообразных, хитро связанных между собой структур данных, коих у нас насчитывается порядка полутысячи…
Структуры не исполняются. Оптимизировать надо в первую очередь бутылочные горлышки, а не лазить по зиллиону сеттеров-геттеров.
Если у вас в приложении конструктор занимает 80% времени значит вы изначально что-то не то делаете.
Структуры не исполняются.Исполняется код, «ползающий» по этим структурам.
И не забывайте про аппаратный префетч, к которому можно добавить и программный.Но никакой префетч, ни программный, ни аппаратный не сделает прохождение по 3-5-10 уровням индирекции близким по скорости к обращению к локальной переменной или, ещё лучше, к регистру.
Оптимизировать надо в первую очередь бутылочные горлышки, а не лазить по зиллиону сеттеров-геттеров.Это если вам нужна занятость и вы хотите в течение 10 лет каждый месяц отчитываться об ускорении на 5-10-15% (вначале больле, потом меньше).
Если же вы хотите сделать быстрый код — то заниматься нужно совсем другим. Единственные, кому действительно полезно смотреть на VTune — это разработчики рантаймов. Так как у них код исполняет алгоритмы, созданные другими и, соответственно, на алгоритмы они повлиять никак не могут.
Если у вас в приложении конструктор занимает 80% времени значит вы изначально что-то не то делаете.Совершенно не обязательно. Если у вас 80% времени уходит на конструктор вспомогательного объекта, который вам, в сущности, не нужен — то это одно. А если это — часть «внутреннего цикла», делающего основную работу — то совсем другое.
Гляньте как-нибудь на профайл «вылизанных до упора» энкодеров видео. 80% «на конструктор» вы там, конечно, не увидите — но там будут весьма и весьма «горячие»' участки. Где будет как раз, суммарно, 80% времени проходить.
Вот так и выглядит оптимальная программа. А «ровный» спектр, где все «горячие участки» потушены — это как раз результат многолетнего бездумного применения VTune. Такая программу, обычно, можно ускорить в несколько раз — если подумать.
Это если вам нужна занятость и вы хотите в течение 10 лет каждый месяц отчитываться об ускорении на 5-10-15% (вначале больле, потом меньше).
Ну лазьте по ним и отчитывайтесь, я не понимаю вы спорите ради спора?
Если же вы хотите сделать быстрый код — то заниматься нужно совсем другим.
Сделайте сначала рабочий. Отладьте. Оптимизация прототипов и нерабочих, ошибочных участков хороший повод просто погреть атмосферу.
Совершенно не обязательно. Если у вас 80% времени уходит на конструктор вспомогательного объекта, который вам, в сущности, не нужен — то это одно. А если это — часть «внутреннего цикла», делающего основную работу — то совсем другое.
Совершенно обязательно. Программа должна считать данные, выполнять полезную нагрузку, а если она занимается «инкапсуляцией» 80% времени, это говорит о богатом внутреннем мире программиста и что неправильно был выбран тип данных.
Гляньте как-нибудь на профайл «вылизанных до упора» энкодеров видео. 80% «на конструктор» вы там, конечно, не увидите — но там будут весьма и весьма «горячие»' участки. Где будет как раз, суммарно, 80% времени проходить.Глядел лет 15 назад, потом ушел в HPC. К чему это замечание — мне не очень понятно. Все оптимизаторы это 80% и ковыряют, забив на остальные 20.
А «ровный» спектр, где все «горячие участки» потушены — это как раз результат многолетнего бездумного применения VTune.
С какого бодуна у вас родилась такая мысль — я не знаю. Я могу прнести вам пару тройку программ, не нюхавших Втюна с таким же профилем. Бездумное применение Втюна — это оксиморон. Не нравится вам инструмент — нечего других упрекать в его использовании.
Я попытался сделать сводную таблицу эффективности разных способов передачи

Потому что пример в той статье полностью корректен, и автор статьи абсолютно прав.
Нет, очевидно. Это просто самый наивный способ передачи. Так напишет любой, кто 1-2 раза видел С++, а автор его таки 1-2 раза и видел.
копирование обеих строк
Копирование есть, оно есть всегда.
Если не боитесь комбинаторного взрыва, то можете дать шанс && (но зачем? реального выигрыша в скорости не будет никакого, оптимизатор не дремлет):
Подобная аргументация ничего не стоит.
Очевидно, что это не одно и тоже. В случае с передачей по ссылке не будет копирования, когда как в случае с первым вариантов оно будет. Ещё на 20-40 байтовой строке можно что-то говорить о том, что копирование бесплатно, но объекты не всегда 20-40байт.
Даже если у вас не std::string, а какой-то объект собственноручно написанного большущего класса, и вы хотите людей заставить перемещать его (а не копировать), то в таком случае лучше запретить конструктор копирование у этого большущего класса, нежели везде передавать его по &&. Так надежнее, да и код короче.
Надёжнее и быстрее как раз через &&. К тому же, к чему определяется это ложное разделение? Одно не мешает другому.
(из пушки по воробьям)
И подобная тоже.
Почему так происходит, каков фундаментальный принцип? Он прост: объект, как правило, должен ВЛАДЕТЬ своими свойствами.
Что это за принцип такой и с чего он должен кого-то волновать? Всё это догматическое raii достало уже всех — оно показало свою полную несостоятельность. Ещё с C++11 от этого глупого догмата начали отходить.
Если объект не хочет чем-то владеть, то он может владеть shared_ptr'ом на это «что-то».
Зачем захламлять код этим мусором, когда есть нормальные ссылки и сторедж может быть не только в хипе. Это опять какой-то догматизм.
pf{std::move(pf)} // std::move здесь важен для производительности
Мы копируем что-бы потом замувать, хотя можно с тем же успехом передать по ссылке, но. Что мы этим получим.
1) Мы получим меньше оверхеда т.к. ненужно будет копировать данные.
2) Если так произошло, что что-то кинуло исключение до move sp, то мы получим те самые:
накладные расходы на блокировку счетчика ссылок shared_ptr в памяти (сотни циклов CPU) и на его инкремент.
Потому что в случае с ссылкой оверхеда на передачу не будет(он будет при копировании в поле), а в ситуации со значением будет.
3) ссылка более универсальна. Т.к. мы не всегда хотим копию объекта. Мы можем захотеть взять подстроку, либо просто использовать данные для какой-то операции внутри конструктора(не копируя/перемещая их в поле).
Мораль: не используйте const& повально. Смотрите по обстоятельствам.
Опять какое-то ложное разделение. Передача не ограничивается этими двумя вариантами.
В конечном итоге подобная передача сливает pf во всём. И рассказывать о её какой-то эффективности — глупость и неправда.
Можно говорить о какой-то простоте для начинающих, но опять же слишком много но. Даже const & куда более универсален и лучше начинающим использовать его.
Ненужно пытаться вести какие-то войны руководствуясь хейтом const & времён С++11. Нужно понимать почему и за что хейтили const &. const & не позволяет мувать временные объекты и для решения этой проблемы существует pf. Решение с «по значению» просто более понятное людям и в какой-то мере может служить заменой pf, но эта замена не полноценна.
Но, людям свойственно использовать некий общий паттер при передаче. И с учётом того, что в 80% случаев люди не пытаются сохранить переданные объекты — переда по значению не может являться универсальной, ведь люди её начнут использовать для этого. И подобные советы — вредные советы.
Единственным правильным и универсальным решением является только «универсальная ссылка» и pf на базе неё. Всё остальное — не является полноценным и эффективным. Даже в таком самом лучшем для «по значению» кейсе.
Напишите статью-опровержение.
Мне лень.
Только сначала перечитайте все примеры в этой, пожалуйста, я там немного добавил про std::forward в том числе.
Я прочитал и именно про forward я и говорил. Я нигде не отрицал, что вы показывали pf. Я говорил о том, что ваши выводы/сравнения pf и «по значению» неправильные. Так же, я объяснял почему.
Так же, я рассказал и о том, откуда взялся этот срыв покровов с «по-значению» с которым носятся неофиты. Они продолжают повторять одно и тоже уже сколько лет. Но проблема в том, что те кто изначально об этом рассказывал — объясняли всё. Но люди выдирают оттуда какие-то куски, ничего не понимания и неся эти откровения в массы.
«по-значению» + move достаточно хайповая вещь, которая никогда не претендовала на замену pf. Так же, было чётко сказано о её применении.
как правильно передавать аргументы в конструктор или сеттер.
Про это я так же говорил. const & имеет только одну проблему — нельзя мувать. Мы можем передать временный объект, но не можем его замувать(т.к. мы не знаем — какой там объект).
Именно поэтому «по-значению» имеет смысла только тогда, когда мы заходим мувать. Т.е. когда мы ходим скопировать/замувать аргумент в поле. Но, хоть и пример именно такой(а он чисто случайно такой), то нигде и никак на это явно не указывается.
Так же, я не стал говорить о том, что существуют концепты. Есть/будет auto && | String && и pf уже не так сложно писать.
Так же, я не стал говорить о том, что существуют концепты. Есть/будет auto && | String && и pf уже не так сложно писать.Проблема не в том — сложно ли это написать. Проблема в том — стоит ли это делать.
Достали, если честно, теоретики, не знающие что реально происходит в компьютере вообще и в C++ в частности. Возьмите хотя бы чудесатый совет никогда не передавать «сырые» указатели, а всегда передавать либо
unique_ptr либо ещё какой «умный» указатель. Ну потому что скорость та же, а надёжность — таки выше… а вы уверены, что скорость — таки та же? Точно уверены? Ну так гляньте сюды и ужаснитесь.В действительности всё совсем не так, как на самом деле. Увы. И потому слепо верить в концепты,
auto&& | Sring&& и прочее… я бы поостерёгся.Вот выйдут эти самые концепты, поработаем с ними — и можно будет уже сказать: так оно — или не так. А пока — статья описывает далеко не самый худший подход. Уж всяко получше, чем бездумное использование pf где нужно и где не нужно будет…
P.S. Кстати, история с
unique_ptr — вот как раз типичная история с C++. Теоретически вроде как бы unique_ptr должен вести себя так же, как простой указатель в смысле эффективности… Однако же… не ведёт. Даже и близко не ведёт. И да — это, как бы, теоретически, в принципе, можно было бы исправить. И, я думаю, когда-нибудь это таки исправят. В конце-концов знаменитый бенчмарк Степанова довели, в конце-концов, до единицы, так ведь? Ну и тут — тоже, теоретически, ничего не мешает… Теоретически-то ничего не мешает, а вот практически — скоро 10 лет пройдёт, а std::unique_ptr — всё ещё не так эффективен, как «сырой» указатель…Так это… надо ж смотреть не только как функция в вакууме выглядит, а еще и как ее вызов заинлайнится. И на -O3.
В случае заинлайнивания тоже unique_ptr будет выглядеть отлично от обычного указателя? Или даже если не заинлайнится, то не сгенерирует ли компилятор две версии пролога, один когда надо занулять оригинал и один когда не надо?
Естественно что в вакууме ему надо занулить старый unique_ptr, о том и ассемблер.
В случае заинлайнивания тоже unique_ptr будет выглядеть отлично от обычного указателя?В случае заинлайнивания — всё будет в порядке. Но тут вроде как неглупые люди «топят» за то, как раз, чтобы в интерфейсе использовать тоже
std::unique_ptr.Или даже если не заинлайнится, то не сгенерирует ли компилятор две версии пролога, один когда надо занулять оригинал и один когда не надо?Нет, не сгенерирует. Вопрос не в необходимости «занулять оригинал», а в ABI. «Мелкие» структуры все современные компиляторы передают на регистрах… за исключением случая, когда оная структура содержит нетривиальный деструтктор… как несложно догадаться — «умный» указатель без нетривиального деструктора окажется, как бы помягче, не слишком «умным».
Естественно что в вакууме ему надо занулить старый unique_ptr, о том и ассемблер.К сожалению ему не «в вакууме» нужно это делать. А в реальном коде с реальным ABI. А в вакууме, как раз, он может делать что угодно.
Возьмите хотя бы чудесатый совет никогда не передавать «сырые» указатели, а всегда передавать либо unique_ptr либо ещё какой «умный» указатель.
Никто там такого не советует. Вы намеренно исказили совет и теперь его опровергаете.
Вообще, khim, вас как будто подменили. Когда-то вы писали на забре разумные вещи, а последнее время часто несете какую-то чепуху.
Никто там такого не советует. Вы намеренно исказили совет и теперь его опровергаете.Вы, конечно, формально, правы. Да, там есть примечание: Sometimes older code can’t be modified because of ABI compatibility requirements or lack of resources. И там предлагается разумная альтернатива: использовать
gsl::owner.Но вы точно уверены, что все читающие воспримут «свеженаписанный код, под самую наираспоследнюю версию одного из самых продвинутых компиляторов» (в данном случае неважно какой из компиляторов вы считаете более продвинутым — Clang или Icc) как «older code that can’t be modified»? Гложут меня смутные сомнения в этом…
Когда-то вы писали на забре разумные вещи, а последнее время часто несете какую-то чепуху.Ну если для вас скорость работы реального кода, скомпилированного реальными компиляторами и запущенного на реальном компьютере — это чепуха, то да, можно продолжать делать вид, что ничего, кроме C++ стандарта не существует.
Ошибки и эффективность в malloc'ах.
www.youtube.com/watch?v=PNRju6_yn3o
Сам я в С++ не бум-бум, но посмотреть было интересно.
Ошибки и эффективность в malloc'ах.
Состоятельность подобного сравнения сомнительна, а вернее отсутствует. Существует не только malloc, а ещё и копирование. К тому же, на тех строках которые он показывал — аллокация и так не будет — будет оверхед только на копировании.
Как я уже писал — существует множество объект тяжелее условно-бесплатных строк. Хотя даже они не так что-бы и совсем бесплатны.
www.youtube.com/watch?v=PNRju6_yn3o
Это такие же трюки. Это работает только в одном кейсе и этой истории десяток лет. Я не понимаю почему её до сих пор везде рассказываю и выдают за что-то универсальное и то, что может кого-то удивить.
В конечном итоге этот трюк сливает pf, но автор нашел вывод — это похаять pf за шаблоны. Но это не проблема pf. Это общая проблема шаблонов которые нельзя ограничить и приходится обкладываться sfinae. Но её уже решили концептами.
К тому же, это решение не универсально. Оно работает только если нам нужно копировать объекты «как есть» вовнутрь объекта. Такое далеко не всегда нужно, а вернее чаще ненужно. А когда нужны все эти кейсы тривиальны и проще просто открыть все поля и инициализировать объекты через список инициализации. Это будет и проще и нагляднее и быстрее: cust{.first = «Niko»}
isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#f16-for-in-parameters-pass-cheaply-copied-types-by-value-and-others-by-reference-to-const
У меня просто мысль такая, что не стоит использовать std::move вообще, кроме каких-то действительно нужных для этого случаев (unique_ptr там). Что-то вроде этого совета isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#es56-write-stdmove-only-when-you-need-to-explicitly-move-an-object-to-another-scope
Это “for in parameters”, т.е. где семантика владения не завязана. А в статье речь про конструкторы и сеттеры. Это более частный случай.
Но тут важно помнить, что «Core Guidelines» написаны с точки зрения кода, который затачивается под гипотетический «идеальный» компилятор C++99 (но который, по какой-то причине, вы всё-таки вынуждены писать и запускать с сегодняшними, далеко не идеальными, компиляторами).
Если вас интересует вариант, который можно использовать «здесь и сейчас» — то далеко не все Core Guidelines оказываются «одинаково полезными».
По поводу "а зачем?" с шаблонным вариантом — смысл таки есть. Конечная цель — инициализировать член данных (а не создавать/копировать/перемещать временные объекты такого же типа; это уже детали реализации). Если класс данных умеет создаваться не только из подобных, а из неких совершенно других параметров, то шаблонный вариант просто пробросит их прямо к конечному конструктору члена, минуя вообще любое перемещение/копирование.
В зависимости от того, к чему вы стремитесь — это может быть и хорошо и плохо.
Но в случае, если перемещение дешёвое, а копирование более дорогое (а это, всё-таки — типичная ситуация), лучше иметь один конструктор.
Ну, всё же не во всех, а только там, где используется. К тому же практика показывает, что в конечном варианте код, как правило, в инлайне. Т.е. явно выраженного конструктора "верхнего" объекта в виде обособленной функции вообще нет. К тому же шаблон вовсе не обязывает использовать его для множества разных типов и случаев. Он всего лишь скрывает детали. Если итоговая функция в конечном итоге вызовется дважды или даже единожды для одного-двух типов аргументов — даже там проще инкапсулировать их в шаблоне, чем приводить подробности этих самых типов.
По поводу копирования/перемещения — простая иллюстрация:
struct train_c
{
int m_x = 0;
train_c(int x) : m_x (x) { std::cout << "\n-CTR train_c(x) " << m_x << " " << this; }
train_c(train_c&& c) : m_x(c.m_x) { c.m_x = 0; std::cout << "\n-MOVE train ctr "
<< m_x << " " << this << " from " << c.m_x << " " << &c;}
// ... copy c-tr, copy and move assignment, etc...
~train_c() { std::cout << "\n-DTR train " << m_x << " " << this; m_x = 0;}
};
struct helper_c
{
int pad = 0; // just to distinquish this from &m_h
train_c m_h;
template <typename TRAIN_C>
helper_c ( TRAIN_C&& c ): m_h { std::forward<TRAIN_C> ( c ) }
{
std::cout << "\nHELPER_TT " << this << " from " << &c << " " << &m_h << " " << m_h.m_x;
}
// ...
~helper_c() { std::cout << "\n~HELPER " << this; }
};
template <typename TRAIN_C>
helper_c* make_helper ( TRAIN_C&& c )
{
std::cout << "\n====> called make_helper with " << &c;
return new helper_c ( std::forward<TRAIN_C>(c) );
}
helper_c* make_helper_byval( train_c c )
{
std::cout << "\n====> called make_helper_byval with " << &c;
return new helper_c( std::move( c ));
}
TEST ( functions, trainer )
{
std::cout << "\n\n==> indirect ctr";
auto fee = make_helper (11);
std::cout << "\n==> made fee " << fee->m_h.m_x;
delete fee;
}
TEST ( functions, trainer_by_val )
{
std::cout << "\n\n==> indirect ctr";
auto fee = make_helper_byval( 11 );
std::cout << "\n==> made fee " << fee->m_h.m_x;
delete fee;
}
Запускаем. Получаем:
[ RUN ] functions.trainer
==> indirect ctr
====> called make_helper with 0x7ffcb3a44b6c
-CTR train_c(x) 11 0x5623c3404e44
HELPER_TT 0x5623c3404e40 from 0x7ffcb3a44b6c 0x5623c3404e44 11
==> made fee 11
~HELPER 0x5623c3404e40
-DTR train 11 0x5623c3404e44
[ RUN ] functions.trainer_by_val
==> indirect ctr
-CTR train_c(x) 11 0x7ffcb3a44b6c
====> called make_helper_byval with 0x7ffcb3a44b6c
-MOVE train ctr 11 0x5623c3404e44 from 0 0x7ffcb3a44b6c
HELPER_TT 0x5623c3404e40 from 0x7ffcb3a44b6c 0x5623c3404e44 11
-DTR train 0 0x7ffcb3a44b6c
==> made fee 11
~HELPER 0x5623c3404e40
-DTR train 11 0x5623c3404e44Собственно, в варианте с шаблоном видим вызов конструктора и деструктора. Всё! PF + RVO полностью избавляют и от копирования, и от перемещения. Внутрь до самой сути пробрасывается исходный int.
А в варианте с передачей по значению RVO таки не избавляет от + move ctr + dtr, покуда всё равно конструируется и перемещается временный объект.
И вот ещё про "кучу кода во всех единицах"...
// head.h
#include <bits/move.h>
struct train_c
{
int m_x = 0;
template<typename INT>
train_c ( INT && param )
: m_x ( param ) {}
};
struct helper_c
{
int pad = 0;
train_c m_h;
template<typename TRAIN_C>
helper_c ( TRAIN_C && c ): m_h {std::forward<TRAIN_C> ( c )}
{
}
};
//foo.cpp
#include "head.h"
int ret42 ()
{
return 42;
}
//bar.cpp
#include "head.h"
int ret42 ()
{
return helper_c ( 42 ).m_h.m_x;
}Смотрим выхлоп от g++ -S foo.cpp (где должна "напородиться куча кода"). Файл целиком:
.file "foo.cpp"
.text
.globl _Z5ret42v
.type _Z5ret42v, @function
_Z5ret42v:
.LFB19:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $42, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE19:
.size _Z5ret42v, .-_Z5ret42v
.ident "GCC: (Ubuntu 7.4.0-1ubuntu1~18.04.1) 7.4.0"
.section .note.GNU-stack,"",@progbits
Не видим вообще упоминаний; что, в общем-то, очевидно.
В случае вызова с оптимизацией О2 g++ -O2 -S bar.cpp результат аналогичен для обоих файлов (кроме имён самих файлов)
.file "bar.cpp"
.text
.p2align 4,,15
.globl _Z5ret42v
.type _Z5ret42v, @function
_Z5ret42v:
.LFB19:
.cfi_startproc
movl $42, %eax
ret
.cfi_endproc
.LFE19:
.size _Z5ret42v, .-_Z5ret42v
.ident "GCC: (Ubuntu 7.4.0-1ubuntu1~18.04.1) 7.4.0"
.section .note.GNU-stack,"",@progbitsТ.е. ровно одна значимая команда (кроме ret). Что тоже достаточно очевидно.
И внушительный процент кода — это как раз автоматически напорождавшийся код шаблонных функций.
Как я уже сказал: иногда — в этом есть смысл. Но нужно чётко отдавать себе отчёт в том, что, где и для чего вы желаете.
Кстати, std::string в случае SSO (small string optimization), не относится к объектам с дешевым перемещением.
Кстати, std::string в случае SSO (small string optimization), не относится к объектам с дешевым перемещением.Ээээ… А вы почему так решили???
Перемещение такой строки — это семь инструкций процессора вместо четырёх для «пустого буфера» того же размера.
И то — последние три инструкции нужны только из-за того, что компилятор «не видит», что полученная строка более не нужна. Если видит — то разницы в перемещении строки и буфера соотвесттвующего размера нет вообще.
Ээээ… А вы почему так решили???
В данном контексте цену перемещения нужно измерять не саму по себе, а по отношению к копированию. И для коротких строк перемещение оказывается таким же как и копия. А следовательно, метод "передача по значению + move" оказывается примерно вдвое дороже, чем "передача по константной ссылке + копия", например.
В данном контексте цену перемещения нужно измерять не саму по себе, а по отношению к копированию.Ok, принято, давайте сравним. Семь инструкций — перемещение, двести инструкций — копирование. Минипулистическая разница, афигеть! Даже код, который исполняется для коротких строк — это где-то 12-15 инструкций вместо 7 (смотря по какой «ставке» защитать «лишние» push'ы и pop'ы).
Кстати это всё для случая
std::basic_string<int>, если использовать просто std::string, то у вас там материализуется просто вызов конструктора — а это весьма дорого, на самом деле, дороже, чем перемещение строки, независимо от того, что там внутри самого этого конструктора происходит.Да, в некоторых случаях копирования строк будет достаточно дешёвым (ради того SSO и существует, собственно). Но в общем случае… операция перемещения строки — это дёшево, копирования — «заметно дороже» (если строка всё-таки не «короткая», то её копирование — это весьма немалая работа… причём разница там не на проценты, а в разы). В типичной программе — разница обычно в 3-5 раз, насколько я помню, хотя могут быть отклонения в разные стороны в зависимости от того, что это за строки и что вы с ними делаете. Чтобы разница была менее, чем двукратной — нужно будет стандартную библиотеку использовать нестандартным образом, чтобы они копирование коротких строк не отдельной функцией оформляла, а инлайнила (вы часто это делаете?).
А следовательно, метод «передача по значению + move» оказывается примерно вдвое дороже, чем «передача по константной ссылке + копия», например.«передача по значению + move» не будет делать никаких копий, если они не нужны. И это будет заметно дешевле, чем если вам таки копию нужно будет сделать из-за того, что вы выбрали стратегию «передача по константной ссылке + копия».
Вариант, когда можно получить выигрыш от использования стратегии «передача по константной ссылке + копия» — это случай, когда избежать копирования строк почти никогда не удаётся, процент «длинных» строк не мал, а черезвычайно мал и, в довершение всего, когда вы собрали стандартную библиотеку особым образом.
Так что перед тем, как пытаться получить выигрыш от использования стратегии «передача по константной ссылке + копия» неплохо было для начала понять: а что это вы такое со строками делаете, что они вдруг у вас так часто копируются? Вы точно про
std::move не забываете?Выигрыш, вами описанный, так-то, в принципе, возможен — но чтобы его получить на практике вам придётся весьма и весьма попотеть… потому я бы остался, скорее, с тем, что написано в обсуждаемой тут статье. Если нет веских причин делать по-другому.
P.S. И это мы ещё обсудили класс, который специально делали таким, чтобы копирование было как можно более быстрым в большинстве случаев. Почти все остальные контейнеры ничего подобного не имеют, там перемещение — дешевле, а копирование — дороже, чем для строк.
А следовательно, метод «передача по значению + move» оказывается примерно вдвое дороже, чем «передача по константной ссылке + копия», например.
однако копирование не короткой строки на несколько порядков больше этих трех инструкций и с лихвой их перекроет даже если у вас длинных строк — одна из тысячи.
std::moveакте строку, а позволяете ей скопироваться, то будет вызываться и конструктор копирования и конструктор перемещения. Но это только в том случае, если строки копируются часто, а перемещаются редко. Так как «свежевычесленные» строки, как правило, перемещаются, то на практике это обычно не так.P.P.S.
В заключение еще одна вещь: std::move() на самом деле ничего не перемещает и сам по себе транслируется в ноль ассемблерных инструкций. Все, что делает std::move(), — это как бы навешивает специальный «липкий ярлык» на ссылку, превращая ее в && — rvalue reference. И дальше можно с этим ярлыком отдельно «мэтчить» тип параметра функции (например, иметь отдельную перегрузку функции для &&-параметра и отдельную для &-параметра). Смысл &&-ярлыка — дать возможность вызывающему коду сказать вызываемому: «если хочешь, ты можешь съесть значение по этой ссылке, оно мне больше не нужно; но только если съешь, косточки-то оставь, мне еще деструктор потом вызывать для оставшегося скелета». С тем же успехом можно было бы передавать и обычные &-ссылки (по ним же тоже можно объект «съесть»), но с && получается лучше семантика, т.к. не перепутаешь: где можно съесть, а где можно только понюхать.
В этой связи название std::move() следует признать крайне неудачным. Правильно было бы назвать его std::eat_me_if_you_want() или std::bon_appetit(). Но std::move() короче.
То есть да, формально вы правы — можно было бы использовать & вместо && и это всё только упростило… однако в 98м про
move-семантику никто не думал и потому есть куча кода, которые передают значение по ссылку не ожидая, что оно будет «съедено». И да, всё верно: rvalue-ссылки и std::move/std::forward именно из-за этого пришлось придумывать.P.S. Кстати
std::forward тоже ничего никуда не форварит, это, фактически, «условный» std::move…Перемещение такой строки — это семь инструкций процессора вместо четырёх для «пустого буфера» того же размера.только лишний мув надо сравнивать не с копированием буфера, а с нулем инструкций. т.е. 7 инструкций вместо нуля инструкций (передача по значению выливается в лишний мув, а не во что-то другое)
иногда этим можно пренебречь, но как минимум надо иметь в виду, что мув конструктор(не путать с std::move) не бесплатен(даже за пределами ссо)
Или вот то же самое, только с шаблонами (но опять же, зачем?):
чтобы делать меньше ненужной работы в обоих случаях?
пруф: godbolt.org/z/T9TGi9
только конечно лучше ограничить допустимые аргументы с помощью enable_if или концептов
Потому что пример в той статье полностью корректен, и автор статьи абсолютно прав.автор в примере написал два лишних std:: как на мой вкус
// НЕТ и НЕТ: это мегаопасно, никогда не сохраняйте константные
// ссылки в свойствах объекта
А почему? Чем это хуже
// так можно иногда, но лучше все же воспользоваться shared_ptr:
// будет медленнее, но безопасно
Вообще, прикольно. Не знал. alenacpp.blogspot.com/2008/01/const.html
Потому что в С++ явно специфицировано, что если привязать временный объект к ссылке на const в стеке, то жизнь временного объекта будет продлена. Теперь он будет жить столько, сколько живет константная ссылка на него. В приведенном примере все валидно, время жизни s заканчивается с закрывающей фигурной скобкой.
Это все относится только к объектам в стеке. На члены класса это не действует.
en.cppreference.com/w/cpp/language/reference_initialization#Lifetime_of_a_temporary
Ликбез по передаче параметров по значению в конструкторы и сеттеры (современный C++, примеры)