Я изучал некоторый C ++, и мне часто приходилось возвращать большие объекты из функций, созданных внутри функции. Я знаю, что есть передача по ссылке, возвращаем указатель и возвращаем решения ссылочного типа, но я также читал, что компиляторы C ++ (и стандарт C ++) позволяют оптимизировать возвращаемое значение, что позволяет избежать копирования этих больших объектов через память, тем самым экономя время и память обо всем этом.
Теперь я чувствую, что синтаксис намного понятнее, когда объект явно возвращается по значению, и компилятор обычно использует RVO и делает процесс более эффективным. Не стоит ли полагаться на эту оптимизацию? Это делает код более понятным и более читабельным для пользователя, что крайне важно, но стоит ли опасаться, что компилятор поймает возможность RVO?
Это микрооптимизация или что-то, о чем я должен помнить при разработке своего кода?
источник
Ответы:
Используйте принцип наименьшего удивления .
Вы и только когда-либо будете использовать этот код, и уверены ли вы в том, что вы через 3 года не удивитесь тому, что вы делаете?
Тогда иди вперед.
Во всех остальных случаях используйте стандартный способ; в противном случае вы и ваши коллеги столкнетесь с трудностями при поиске ошибок.
Например, мой коллега жаловался на мой код, вызывающий ошибки. Оказывается, он отключил логическую оценку короткого замыкания в настройках своего компилятора. Я чуть не ударил его.
источник
Для этого конкретного случая определенно просто вернуть по значению.
RVO и NRVO являются хорошо известными и надежными оптимизациями, которые действительно должны быть сделаны любым достойным компилятором, даже в режиме C ++ 03.
Семантика перемещения гарантирует, что объекты будут удалены из функций, если (N) RVO не было. Это полезно только в том случае, если ваш объект использует динамические данные для внутреннего использования (как, например,
std::vector
делает), но это действительно так, если он слишком большой - переполнение стека представляет собой риск для больших автоматических объектов.C ++ 17 обеспечивает RVO. Так что не волнуйтесь, он не исчезнет и полностью завершится, как только обновятся компиляторы.
И, наконец, принудительное дополнительное динамическое выделение для возврата указателя или принудительное создание типа результата по умолчанию, чтобы вы могли передать его в качестве выходного параметра, являются как уродливым, так и не идиоматическим решением проблемы, которую вы, вероятно, никогда не будете имеют.
Просто напишите код, который имеет смысл, и поблагодарите авторов компилятора за правильную оптимизацию кода, который имеет смысл.
источник
Это не какая-то малоизвестная, приятная, микрооптимизация, о которой вы читаете в небольшом блоге с небольшим трафиком, и затем вы чувствуете себя умнее и превосходнее в использовании.
После C ++ 11 RVO является стандартным способом написания этого кода. Как правило, ожидаемый, обучаемый, упомянутый в беседах, упомянутых в блогах, упомянутых в стандарте, будет сообщаться как ошибка компилятора, если не реализована. В C ++ 17 язык идет на один шаг дальше и предписывает копирование в определенных сценариях.
Вы должны полностью положиться на эту оптимизацию.
Кроме того, возврат по значению приводит к гораздо более простому коду для чтения и управления, чем код, возвращаемый по ссылке. Семантика значения - это мощная вещь, которая сама по себе может привести к увеличению возможностей оптимизации.
источник
Правильность написанного вами кода никогда не должна зависеть от оптимизации. Он должен выводить правильный результат при выполнении на виртуальной машине C ++, которую они используют в спецификации.
Однако то, о чем вы говорите, это скорее вопрос эффективности. Ваш код работает лучше, если его оптимизировать с помощью оптимизирующего компилятора RVO. Это хорошо, по всем причинам, указанным в других ответах.
Однако, если вам требуется эта оптимизация (например, если конструктор копирования фактически приведет к сбою вашего кода), теперь вы находитесь в прихоти компилятора.
Я думаю, что лучший пример этого в моей собственной практике - оптимизация хвостовых вызовов:
Это глупый пример, но он показывает хвостовой вызов, где функция вызывается рекурсивно прямо в конце функции. Виртуальная машина C ++ покажет, что этот код работает должным образом, хотя я могу немного озадачиться тем, почему я вообще потрудился написать такую процедуру добавления. Однако в практических реализациях C ++ у нас есть стек, и он имеет ограниченное пространство. Если сделать это педантично, эта функция должна была бы помещать по крайней мере
b + 1
стековые кадры в стек, поскольку она выполняет добавление. Если я хочу рассчитатьsillyAdd(5, 7)
, это не имеет большого значения. Если я захочу рассчитатьsillyAdd(0, 1000000000)
, у меня могут возникнуть проблемы с запуском StackOverflow (и не очень хорошим ).Тем не менее, мы можем видеть, что как только мы дойдем до последней строки возврата, мы действительно закончим со всем в текущем кадре стека. Нам не нужно держать это вокруг. Оптимизация Tail Call позволяет вам «использовать» существующий кадр стека для следующей функции. Таким образом, нам нужен только 1 кадр стека, а не
b+1
. (Мы все еще должны делать все эти глупые добавления и вычитания, но они не занимают больше места.) По сути, оптимизация превращает код в:В некоторых языках оптимизация хвостового вызова явно требуется спецификацией. C ++ не является одним из них. Я не могу полагаться на компиляторы C ++, чтобы распознать эту возможность оптимизации хвостовых вызовов, если я не пойду в каждом конкретном случае. В моей версии Visual Studio версия выпуска выполняет оптимизацию хвостового вызова, а версия отладки - нет (по замыслу).
Таким образом, для меня было бы плохо зависеть от способности рассчитывать
sillyAdd(0, 1000000000)
.источник
#ifdef
блоки и имеют доступный обходной путь, соответствующий стандартам.b = b + 1
?На практике программы на C ++ ожидают некоторой оптимизации компилятора.
Обратите внимание на стандартные заголовки ваших стандартных реализаций контейнеров . С GCC вы можете запросить предварительно обработанную форму (
g++ -C -E
) и внутреннее представление GIMPLE (g++ -fdump-tree-gimple
или Gimple SSA с-fdump-tree-ssa
) для большинства исходных файлов (технически единиц перевода) с использованием контейнеров. Вы будете удивлены количеством оптимизации, которая сделана (сg++ -O2
). Таким образом, разработчики контейнеров полагаются на оптимизацию (и большую часть времени разработчик стандартной библиотеки C ++ знает, что должно произойти, и записывает реализацию контейнера с учетом этого; иногда он также записывает этап оптимизации в компиляторе для иметь дело с функциями, необходимыми для стандартной библиотеки C ++).На практике именно оптимизация компилятора делает C ++ и его стандартные контейнеры достаточно эффективными. Таким образом, вы можете положиться на них.
И также для случая RVO, упомянутого в вашем вопросе.
Стандарт C ++ был разработан совместно (в частности, путем экспериментов с достаточно хорошими оптимизациями, предлагая новые функции), чтобы хорошо работать с возможными оптимизациями.
Например, рассмотрим программу ниже:
скомпилируйте это с
g++ -O3 -fverbose-asm -S
. Вы обнаружите, что сгенерированная функция не выполняет никакихCALL
машинных инструкций. Таким образом , большинство шагов C ++ (строительство закрытия лямбда, его многократное применение, получениеbegin
иend
итераторы, и т.д ...) были оптимизированы. Машинный код содержит только цикл (который не указан явно в исходном коде). Без такой оптимизации C ++ 11 не будет успешным.добавлений
(добавлено 31 декабря ул 2017)
См. CppCon 2017: Мэтт Годболт «Что мой компилятор сделал для меня в последнее время? Откручиваем крышку компилятора » .
источник
Всякий раз, когда вы используете компилятор, понимаете, что он будет производить машинный или байт-код для вас. Он не гарантирует ничего о том, на что похож этот сгенерированный код, за исключением того, что он будет реализовывать исходный код в соответствии со спецификацией языка. Обратите внимание, что эта гарантия одинакова независимо от используемого уровня оптимизации, и поэтому, в общем, нет оснований считать один выход более «правильным», чем другой.
Кроме того, в тех случаях, как RVO, где это указано в языке, было бы бессмысленно изо всех сил избегать его использования, особенно если это делает исходный код проще.
Большие усилия прилагаются к тому, чтобы компиляторы производили эффективный вывод, и ясно, что цель заключается в том, чтобы использовать эти возможности.
Могут быть причины для использования неоптимизированного кода (например, для отладки), но случай, упомянутый в этом вопросе, по-видимому, не один (и если ваш код дает сбой только при оптимизации, и это не является следствием некоторой особенности устройство, на котором вы его запускаете, значит, где-то есть ошибка, и она вряд ли будет в компиляторе.)
источник
Я думаю, что другие хорошо освещали специфический взгляд на C ++ и RVO. Вот более общий ответ:
Когда дело доходит до правильности, вы не должны полагаться на оптимизацию компилятора или поведение компилятора в целом. К счастью, вы, кажется, этого не делаете.
Когда дело доходит до производительности, вы должны полагаться на поведение компилятора в целом и оптимизацию компилятора в частности. Стандартно-совместимый компилятор может свободно компилировать ваш код любым удобным для него способом, если скомпилированный код ведет себя в соответствии со спецификацией языка. И я не знаю какой-либо спецификации для основного языка, которая определяет, как быстро должна выполняться каждая операция.
источник
Оптимизация компилятора должна влиять только на производительность, а не на результаты. Опора на оптимизацию компилятора для удовлетворения нефункциональных требований не только разумна, но и часто является причиной выбора одного компилятора над другим.
Флаги, которые определяют, как выполняются определенные операции (например, условия индекса или переполнения), часто смешиваются с оптимизацией компилятора, но не должны. Они явно влияют на результаты расчетов.
Если оптимизация компилятора приводит к другим результатам, это ошибка - ошибка компилятора. Опираясь на ошибку в компиляторе, в долгосрочной перспективе ошибка - что произойдет, когда она будет исправлена?
Использование флагов компилятора, которые изменяют работу вычислений, должно быть хорошо документировано, но использоваться по мере необходимости.
источник
x*y>z
произвольно выдаст 0 или 1 в случае переполнения, при условии, что у нее нет других побочных эффектов , требуется, чтобы программист либо предотвращал переполнения любой ценой, либо заставлял компилятор оценивать выражение определенным образом. ненужные ухудшения оптимизации против высказывания того, что ...x*y
переводит свои операнды в какой-то произвольный более длинный тип (таким образом, допускаются формы подъема и уменьшения прочности, которые могут изменить поведение некоторых случаев переполнения). Однако многие компиляторы требуют, чтобы программисты либо предотвращали переполнение любой ценой, либо вынуждали компиляторы урезать все промежуточные значения в случае переполнения.Нет.
Это то, что я делаю все время. Если мне нужно получить доступ к произвольному 16-битному блоку в памяти, я делаю это
... и полагаться на то, что компилятор сделает все возможное для оптимизации этого куска кода. Код работает на ARM, i386, AMD64 и практически на каждой отдельной архитектуре. Теоретически, неоптимизирующий компилятор может вызывать
memcpy
, что приводит к совершенно плохой производительности, но для меня это не проблема, так как я использую оптимизацию компилятора.Рассмотрим альтернативу:
Этот альтернативный код не работает на машинах, которые требуют правильного выравнивания, если
get_pointer()
возвращает невыровненный указатель. Кроме того, могут быть проблемы с наложением в альтернативе.Разница между -O2 и -O0 при использовании
memcpy
хитрости велика: производительность контрольной суммы IP 3,2 Гбит / с против производительности контрольной суммы IP 67 Гбит / с. Разница на порядок больше!Иногда вам может понадобиться помощь компилятору. Так, например, вместо того, чтобы полагаться на компилятор для развертывания циклов, вы можете сделать это самостоятельно. Либо путем внедрения известного устройства Даффа , либо более чистым способом.
Недостаток использования оптимизаций компилятора состоит в том, что если вы запустите gdb для отладки своего кода, вы можете обнаружить, что многое было оптимизировано. Таким образом, вам может понадобиться перекомпилировать с -O0, что означает, что производительность будет отстойной при отладке. Я думаю, что этот недостаток стоит принять, учитывая преимущества оптимизации компиляторов.
Что бы вы ни делали, пожалуйста, убедитесь, что ваш путь на самом деле не неопределенное поведение. Конечно, доступ к некоторому случайному блоку памяти в виде 16-разрядного целого числа является неопределенным поведением из-за проблем с псевдонимами и выравниванием.
источник
Все попытки эффективного кода, написанного на чем угодно, кроме ассемблера, очень и очень сильно зависят от оптимизации компилятора, начиная с самого простого, такого как эффективное распределение регистров, чтобы избежать лишних разливов стека повсюду и, по крайней мере, достаточно хорошего, если не отличного, выбора команд. В противном случае мы вернулись бы в 80-е годы, когда нам приходилось ставить
register
подсказки повсеместно и использовать минимальное количество переменных в функции, чтобы помочь архаичным компиляторам Си или даже раньше, когдаgoto
была полезна оптимизация ветвления.Если бы мы не чувствовали, что можем полагаться на способность нашего оптимизатора оптимизировать наш код, мы все равно будем кодировать критичные к производительности пути выполнения в сборке.
Это действительно вопрос того, насколько надежно вы чувствуете, что можно провести оптимизацию, которую лучше всего разобрать, профилировав и изучив возможности имеющихся у вас компиляторов и, возможно, даже разобрав их, если есть горячая точка, которую вы не можете понять, где, по-видимому, компилятор не смогли сделать очевидную оптимизацию.
RVO - это то, что существует уже много лет, и, по крайней мере, за исключением очень сложных случаев, это то, что компиляторы надежно применяют на протяжении веков. Определенно не стоит обходить проблему, которой не существует.
Ошибка на стороне полагаться на оптимизатор, не опасаясь его
Напротив, я бы сказал, что ошибочно полагается на то, что слишком много полагается на оптимизацию компилятора, чем на слишком маленькое, и это предложение исходит от парня, который работает в областях, критически важных для производительности, где эффективность, ремонтопригодность и воспринимаемое качество среди клиентов все одно гигантское пятно. Я бы предпочел, чтобы вы слишком уверенно полагались на свой оптимизатор и находили некоторые непонятные крайние случаи, когда вы полагались слишком сильно, чем полагались слишком мало, и просто утаивали все время от суеверных страхов всю оставшуюся жизнь. Это, по крайней мере, заставит вас обратиться к профилировщику и должным образом расследовать, если что-то не выполняется так быстро, как должно, и получать ценные знания, а не суеверия.
У тебя хорошо получается положиться на оптимизатор. Так держать. Не становитесь похожим на того парня, который начинает явно просить встроить каждую функцию, вызванную в цикле, прежде чем даже профилировать из-за ошибочного страха перед недостатками оптимизатора.
профилирование
Профилирование действительно является окольным, но окончательным ответом на ваш вопрос. Новички, жаждущие написать эффективный код, с которым часто приходится бороться, не то, что нужно оптимизировать, а то, что не нужно оптимизировать, потому что они разрабатывают всевозможные ложные догадки о неэффективности, которые, хотя и интуитивно понятны, ошибочны в вычислительном отношении. Опыт работы с профилировщиком действительно даст вам правильную оценку не только возможностей оптимизации ваших компиляторов, на которые вы можете уверенно опираться, но и возможностей (а также ограничений) вашего оборудования. Возможно, в профилировании даже больше смысла изучать то, что не стоит оптимизировать, чем изучать то, что было.
источник
Программное обеспечение может быть написано на C ++ для самых разных платформ и для множества разных целей.
Это полностью зависит от назначения программного обеспечения. Если это будет легко поддерживать, расширять, исправлять, рефакторинг и т. Д. или другие вещи более важны, такие как производительность, стоимость или совместимость с некоторым конкретным оборудованием или время, необходимое для разработки.
источник
Я думаю, что скучный ответ на это: «это зависит».
Является ли плохой практикой писать код, основанный на оптимизации компилятора, который , вероятно, будет отключен, и где уязвимость не задокументирована, а рассматриваемый код не является модульным тестированием, чтобы в случае его поломки вы знали его ? Вероятно.
Является ли плохой практикой писать код, основанный на оптимизации компилятора, который вряд ли будет отключен , задокументирован и проверен модулем ? Возможно, нет.
источник
Если больше вы не говорите нам, это плохая практика, но не по той причине, которую вы предлагаете.
Возможно, в отличие от других языков, которые вы использовали ранее, возвращая значение объекта в C ++, вы получаете копию объекта. Если вы затем модифицируете объект, вы модифицируете другой объект . Это если у меня есть
Obj a; a.x=1;
аObj b = a;
затем я делаюb.x += 2; b.f();
, тоa.x
все равно равно 1, а не 3.Поэтому нет, использование объекта в качестве значения вместо ссылки или указателя не обеспечивает такую же функциональность, и вы можете получить ошибки в программном обеспечении.
Возможно, вы знаете это, и это не оказывает негативного влияния на ваш конкретный вариант использования. Однако, исходя из формулировки в вашем вопросе, кажется, что вы можете не знать о различии; формулировка, такая как «создать объект в функции».
«создать объект в функции» звучит как
new Obj;
где «вернуть объект по значению» звучит какObj a; return a;
Obj a;
иObj* a = new Obj;
очень, очень разные вещи; первый может привести к повреждению памяти, если он не используется должным образом и не понят, а последний может привести к утечке памяти, если не используется и не используется должным образом.источник
return
созданный в операторе, который является требованием для RVO. Кроме того, вы продолжаете говорить о ключевыхnew
словах и указателях, а это не то, чем занимается RVO. Я полагаю, вы либо не поняли вопрос, либо RVO, либо, возможно, оба.Питер Б абсолютно прав, рекомендуя наименьшее изумление.
Чтобы ответить на ваш конкретный вопрос, что это (наиболее вероятно) означает в C ++, что вы должны вернуть a
std::unique_ptr
к созданному объекту.Причина в том, что для разработчика на C ++ это более понятно, что происходит.
Хотя ваш подход, скорее всего, сработает, вы фактически сигнализируете, что объект является типом малого значения, хотя на самом деле это не так. Кроме того, вы отбрасываете любую возможность для абстракции интерфейса. Это может быть хорошо для ваших текущих целей, но часто очень полезно при работе с матрицами.
Я ценю, что если вы пришли с других языков, все символы могут сначала сбить с толку. Но будьте осторожны, не думайте, что, не используя их, вы сделаете свой код более понятным. На практике, скорее всего, верно обратное.
источник
std::make_unique
, а неstd::unique_ptr
напрямую. Во-вторых, RVO - это не какая-то эзотерическая, специфичная для поставщика оптимизация: она встроена в стандарт. Даже тогда, когда этого не было, его широко поддерживали и ожидали поведения. Нет смысла возвращать,std::unique_ptr
когда указатель не нужен в первую очередь.