Краткая версия: во многих языках программирования обычно возвращаются большие объекты, такие как векторы / массивы. Допустим ли этот стиль в C ++ 0x, если в классе есть конструктор перемещения, или программисты на C ++ считают его странным / уродливым / мерзким?
Расширенная версия: в C ++ 0x это все еще считается плохим тоном?
std::vector<std::string> BuildLargeVector();
...
std::vector<std::string> v = BuildLargeVector();
Традиционная версия выглядела бы так:
void BuildLargeVector(std::vector<std::string>& result);
...
std::vector<std::string> v;
BuildLargeVector(v);
В более новой версии возвращаемое значение BuildLargeVector
является rvalue, поэтому v будет построен с использованием конструктора перемещения std::vector
, если (N) RVO не имеет места.
Даже до C ++ 0x первая форма часто была «эффективной» из-за (N) RVO. Однако (N) RVO остается на усмотрение компилятора. Теперь, когда у нас есть ссылки на rvalue, гарантировано, что не будет глубокого копирования.
Изменить : вопрос действительно не об оптимизации. Обе представленные формы имеют почти одинаковую производительность в реальных программах. Тогда как в прошлом первая форма могла иметь на порядок худшие характеристики. В результате первая форма долгое время была основным запахом кода в программировании на C ++. Надеюсь, больше нет?
Ответы:
Дэйв Абрахамс провел довольно всесторонний анализ скорости передачи / возврата значений .
Короткий ответ: если вам нужно вернуть значение, верните значение. Не используйте выходные ссылки, потому что компилятор все равно это делает. Конечно, есть предостережения, поэтому вам следует прочитать эту статью.
источник
x / 2
наx >> 1
forint
s, но вы предполагаете, что это произойдет. Стандарт также ничего не говорит о том, как компиляторы должны реализовывать ссылки, но вы предполагаете, что они эффективно обрабатываются с помощью указателей. В стандарте также ничего не говорится о v-таблицах, поэтому нельзя быть уверенным в эффективности вызовов виртуальных функций. По сути, вам нужно время от времени доверять компилятору.По крайней мере, IMO, это обычно плохая идея, но не из соображений эффективности. Это плохая идея, потому что рассматриваемая функция обычно должна быть написана как общий алгоритм, который производит свой вывод через итератор. Практически любой код, который принимает или возвращает контейнер вместо работы с итераторами, следует считать подозрительным.
Не поймите меня неправильно: бывают случаи, когда имеет смысл передавать объекты, подобные коллекциям (например, строки), но в приведенном примере я считаю передачу или возврат вектора плохой идеей.
источник
Суть такова:
Copy Elision и RVO позволяют избежать «страшных копий» (компилятор не требуется для реализации этих оптимизаций, а в некоторых ситуациях его нельзя применить)
Ссылки C ++ 0x RValue допускают реализацию строк / векторов, которые это гарантируют .
Если вы можете отказаться от более старых реализаций компиляторов / STL, возвращайте векторы свободно (и убедитесь, что ваши собственные объекты тоже поддерживают это). Если ваша кодовая база должна поддерживать "меньшие" компиляторы, придерживайтесь старого стиля.
К сожалению, это сильно влияет на ваши интерфейсы. Если C ++ 0x не подходит, и вам нужны гарантии, в некоторых сценариях вы можете вместо этого использовать объекты с подсчетом ссылок или копированием при записи. Однако у них есть недостатки с многопоточностью.
(Я бы хотел, чтобы только один ответ на C ++ был простым, понятным и без условий).
источник
Действительно, так как C ++ 11, стоимость копирования
std::vector
исчезает в большинстве случаев.Однако следует иметь в виду, что затраты на создание нового вектора (а затем его разрушение ) все еще существуют, и использование выходных параметров вместо возврата по значению по-прежнему полезно, когда вы хотите повторно использовать емкость вектора. Это задокументировано как исключение в F.20 Руководящих принципов C ++ Core.
Сравним:
с участием:
Теперь предположим, что нам нужно вызвать эти методы несколько
numIter
раз в жестком цикле и выполнить какое-то действие. Например, давайте посчитаем сумму всех элементов.Используя
BuildLargeVector1
, вы бы сделали:Используя
BuildLargeVector2
, вы бы сделали:В первом примере происходит много ненужных динамических распределений / высвобождений, которые во втором примере предотвращаются за счет использования выходного параметра старым способом, повторно используя уже выделенную память. Стоит ли делать эту оптимизацию или нет, зависит от относительной стоимости выделения / освобождения по сравнению со стоимостью вычисления / изменения значений.
Контрольный показатель
Давайте поиграем со значениями
vecSize
иnumIter
. Мы будем поддерживать значение vecSize * numIter постоянным, так что «теоретически» это должно занять одинаковое время (= есть такое же количество присваиваний и добавлений, с точно такими же значениями), а разница во времени может возникать только из-за стоимости выделения, освобождения и лучшее использование кеша.В частности, давайте использовать vecSize * numIter = 2 ^ 31 = 2147483648, потому что у меня 16 ГБ ОЗУ, и это число гарантирует, что выделено не более 8 ГБ (sizeof (int) = 4), гарантируя, что я не переключаюсь на диск ( все остальные программы были закрыты, у меня при запуске теста было доступно ~ 15Гб).
Вот код:
И вот результат:
(Intel i7-7700K @ 4,20 ГГц; 16 ГБ DDR4 2400 МГц; Kubuntu 18.04)
Обозначение: mem (v) = v.size () * sizeof (int) = v.size () * 4 на моей платформе.
Неудивительно, что когда
numIter = 1
(например, mem (v) = 8 ГБ), времена абсолютно идентичны. Действительно, в обоих случаях мы выделяем в памяти только один раз огромный вектор размером 8 ГБ. Это также доказывает, что при использовании BuildLargeVector1 () не было копирования: у меня не хватило бы ОЗУ для копирования!Когда
numIter = 2
повторное использование емкости вектора вместо перераспределения второго вектора происходит в 1,37 раза быстрее.Когда
numIter = 256
повторное использование емкости вектора (вместо выделения / освобождения вектора снова и снова 256 раз ...) происходит в 2,45 раза быстрее :)Мы можем заметить, что time1 в значительной степени постоянен от
numIter = 1
доnumIter = 256
, что означает, что выделение одного огромного вектора размером 8 ГБ примерно так же дорого, как выделение 256 векторов размером 32 МБ. Однако выделение одного огромного вектора размером 8 ГБ определенно дороже, чем выделение одного вектора размером 32 МБ, поэтому повторное использование емкости вектора обеспечивает повышение производительности.От
numIter = 512
(mem (v) = 16MB) доnumIter = 8M
(mem (v) = 1kB) - это золотая середина: оба метода работают так же быстро и быстрее, чем все другие комбинации numIter и vecSize. Вероятно, это связано с тем, что размер кэша L3 моего процессора составляет 8 МБ, так что вектор в значительной степени полностью помещается в кеш. На самом деле я не объясняю, почему внезапный скачокtime1
для mem (v) = 16MB, более логично было бы произойти сразу после того, как mem (v) = 8MB. Обратите внимание, что, как ни странно, в этой золотой зоне отказ от повторного использования емкости на самом деле немного быстрее! Я действительно не объясняю этого.Когда
numIter > 8M
все становится некрасиво. Оба метода работают медленнее, но возврат вектора по значению становится еще медленнее. В худшем случае, когда вектор содержит только один единственныйint
, повторное использование емкости вместо возврата по значению происходит в 3,3 раза быстрее. Предположительно, это связано с фиксированными затратами на malloc (), которые начинают преобладать.Обратите внимание на то, что кривая для time2 более гладкая, чем кривая для time1: не только повторное использование векторной емкости, как правило, быстрее, но, что, возможно, более важно, оно более предсказуемо .
Также обратите внимание, что в лучшем случае мы смогли выполнить 2 миллиарда сложений 64-битных целых чисел за ~ 0,5 с, что вполне оптимально для 64-битного процессора с тактовой частотой 4,2 ГГц. Мы могли бы добиться большего, распараллелив вычисления, чтобы использовать все 8 ядер (в приведенном выше тесте одновременно используется только одно ядро, что я проверил, повторно запустив тест при мониторинге использования ЦП). Наилучшая производительность достигается при mem (v) = 16 КБ, что соответствует порядку величины кеша L1 (кэш данных L1 для i7-7700K составляет 4x32 КБ).
Конечно, различия становятся все менее и менее значимыми, чем больше вычислений вам действительно нужно выполнить с данными. Ниже приведены результаты, если мы заменим его
sum = std::accumulate(v.begin(), v.end(), sum);
наfor (int k : v) sum += std::sqrt(2.0*k);
:Выводы
Результаты могут отличаться на других платформах. Как обычно, если производительность имеет значение, напишите тесты для вашего конкретного варианта использования.
источник
Я по-прежнему считаю это плохой практикой, но стоит отметить, что моя команда использует MSVC 2008 и GCC 4.1, поэтому мы не используем последние компиляторы.
Ранее многие горячие точки, отображаемые в vtune с MSVC 2008, сводились к копированию строк. У нас был такой код:
... обратите внимание, что мы использовали наш собственный тип String (это было необходимо, потому что мы предоставляем комплект для разработки программного обеспечения, в котором авторы плагинов могут использовать разные компиляторы и, следовательно, разные несовместимые реализации std :: string / std :: wstring).
Я сделал простое изменение в ответ на сеанс профилирования выборки графа вызовов, показывающий, что String :: String (const String &) занимает значительное количество времени. Методы, подобные в приведенном выше примере, внесли наибольший вклад (на самом деле сеанс профилирования показал, что выделение и освобождение памяти является одной из самых больших горячих точек, причем конструктор копирования String является основным участником выделения).
Я сделал простое изменение:
И все же это имело огромное значение! Горячая точка исчезла в последующих сеансах профилировщика, и в дополнение к этому мы проводим много тщательного модульного тестирования, чтобы отслеживать производительность нашего приложения. После этих простых изменений время тестирования производительности всех видов значительно сократилось.
Заключение: мы не используем самые последние компиляторы, но мы все еще не можем зависеть от компилятора, оптимизирующего копирование для надежного возврата по значению (по крайней мере, не во всех случаях). Это может быть не так для тех, кто использует более новые компиляторы, такие как MSVC 2010. Я с нетерпением жду, когда мы сможем использовать C ++ 0x и просто использовать ссылки rvalue, и никогда не придется беспокоиться о том, что мы пессимизируем наш код, возвращая сложный классы по стоимости.
[Edit] Как указал Нейт, RVO применяется к возврату временных файлов, созданных внутри функции. В моем случае таких временных файлов не было (за исключением неверной ветки, в которой мы создаем пустую строку), и поэтому RVO не был бы применим.
источник
<::
или??!
с условным оператором?:
(иногда называемым тернарным оператором ).Немного придираемся: во многих языках программирования не принято возвращать массивы из функций. В большинстве из них возвращается ссылка на массив. В C ++ наиболее близкой аналогией будет возврат
boost::shared_array
источник
shared_ptr
и закончите.Если производительность является реальной проблемой, вы должны понимать, что семантика перемещения не всегда быстрее, чем копирование. Например, если у вас есть строка, в которой используется оптимизация небольшой строки, то для небольших строк конструктор перемещения должен выполнять точно такой же объем работы, что и обычный конструктор копирования.
источник