Комментарии 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++, примеры)