Благодаря C ++ 11 мы получили std::function
семейство упаковщиков функторов. К сожалению, я продолжаю слышать только плохие вещи об этих новых дополнениях. Наиболее популярным является то, что они ужасно медленные. Я проверил это, и они действительно сосут по сравнению с шаблонами.
#include <iostream>
#include <functional>
#include <string>
#include <chrono>
template <typename F>
float calc1(F f) { return -1.0f * f(3.3f) + 666.0f; }
float calc2(std::function<float(float)> f) { return -1.0f * f(3.3f) + 666.0f; }
int main() {
using namespace std::chrono;
const auto tp1 = system_clock::now();
for (int i = 0; i < 1e8; ++i) {
calc1([](float arg){ return arg * 0.5f; });
}
const auto tp2 = high_resolution_clock::now();
const auto d = duration_cast<milliseconds>(tp2 - tp1);
std::cout << d.count() << std::endl;
return 0;
}
111 мс против 1241 мс Я предполагаю, что это потому, что шаблоны могут быть красиво встроены, в то время как function
s покрывают внутреннее пространство посредством виртуальных вызовов.
Очевидно, шаблоны имеют свои проблемы, как я их вижу:
- они должны быть представлены в виде заголовков, а это не то, что вы, возможно, не захотите делать при выпуске своей библиотеки в виде закрытого кода,
- они могут значительно увеличить время компиляции, если
extern template
не введена политика, подобная - не существует (по крайней мере, мне известно) чистого способа представления требований (концепций, кого-либо?) шаблона, за исключением комментария, описывающего, какой тип функтора ожидается.
Могу ли я, таким образом, предположить, что function
s можно использовать в качестве фактического стандарта проходящих функторов, и в тех местах, где ожидается высокая производительность, следует использовать шаблоны?
Редактировать:
Мой компилятор - Visual Studio 2012 без CTP.
источник
std::function
тогда и только тогда, когда вам действительно нужна гетерогенная коллекция вызываемых объектов (т. Е. Во время выполнения дополнительная информация не доступна).std::function
или шаблоны». Я думаю, что здесь проблема заключается в том, чтобы просто обернуть лямбду в, аstd::function
не в лямбдуstd::function
. На данный момент ваш вопрос похож на вопрос: "Мне лучше выбрать яблоко или миску?"Ответы:
В общем, если вы сталкиваетесь с ситуацией проектирования, которая предоставляет вам выбор, используйте шаблоны . Я подчеркнул слово дизайн, потому что я думаю, что вам нужно сосредоточиться на различии между вариантами использования
std::function
и шаблонами, которые довольно разные.В общем, выбор шаблонов - это всего лишь пример более широкого принципа: попытайтесь указать как можно больше ограничений во время компиляции . Обоснование простое: если вы можете обнаружить ошибку или несоответствие типов, даже до того, как ваша программа будет сгенерирована, вы не отправите программу с ошибками своему клиенту.
Более того, как вы правильно указали, вызовы шаблонных функций разрешаются статически (т. Е. Во время компиляции), поэтому у компилятора есть вся необходимая информация для оптимизации и, возможно, встроенного кода (что было бы невозможно, если бы вызов выполнялся через виртуальные таблицы).
Да, это правда, что поддержка шаблонов не идеальна, и в C ++ 11 все еще отсутствует поддержка концепций; Однако я не понимаю, как
std::function
бы спасти вас в этом отношении.std::function
это не альтернатива шаблонам, а инструмент для разработки ситуаций, когда шаблоны не могут быть использованы.Один из таких случаев использования возникает, когда вам нужно разрешить вызов во время выполнения, вызывая вызываемый объект, который придерживается определенной сигнатуры, но конкретный тип которого неизвестен во время компиляции. Это обычно тот случай, когда у вас есть коллекция обратных вызовов потенциально разных типов , но которые вы должны вызывать единообразно ; тип и количество зарегистрированных обратных вызовов определяется во время выполнения на основе состояния вашей программы и логики приложения. Некоторые из этих обратных вызовов могут быть функторами, некоторые могут быть простыми функциями, некоторые могут быть результатом привязки других функций к определенным аргументам.
std::function
аstd::bind
также предлагает естественную идиому для включения функционального программирования на C ++, где функции обрабатываются как объекты, и их естественное каррирование и объединение для генерации других функций. Хотя такого рода комбинация может быть достигнута и с помощью шаблонов, аналогичная ситуация проектирования обычно сочетается с вариантами использования, которые требуют определения типа объединенных вызываемых объектов во время выполнения.Наконец, есть другие ситуации, когда
std::function
это неизбежно, например, если вы хотите написать рекурсивные лямбды ; однако эти ограничения в большей степени продиктованы технологическими ограничениями, чем концептуальными различиями, на мой взгляд.Подводя итог, сфокусируйтесь на дизайне и попытайтесь понять, каковы концептуальные варианты использования этих двух конструкций. Если вы сравниваете их так, как вы это делали, вы заставляете их выходить на арену, которой они, скорее всего, не принадлежат.
источник
std::function
на стороне хранилища и шаблонFun
на интерфейсе».unique_ptr<void>
вызывает соответствующие деструкторы даже для типов без виртуальных деструкторов).Энди Prowl хорошо освещал вопросы дизайна. Это, конечно, очень важно, но я полагаю, что первоначальный вопрос касается большего количества проблем, связанных с производительностью
std::function
.Прежде всего, краткое замечание о методике измерения: полученные
calc1
значения 11 мс не имеют никакого значения. Действительно, глядя на сгенерированную сборку (или отлаживая код сборки), можно увидеть, что оптимизатор VS2012 достаточно умен, чтобы понять, что результат вызоваcalc1
не зависит от итерации, и перемещает вызов из цикла:Кроме того, он понимает, что вызов
calc1
не имеет видимого эффекта, и полностью сбрасывает вызов. Следовательно, 111 мс - это время, которое требуется пустому циклу. (Я удивлен, что оптимизатор сохранил цикл.) Так что будьте осторожны с измерениями времени в циклах. Это не так просто, как может показаться.Как уже указывалось, оптимизатор имеет больше проблем для понимания
std::function
и не перемещает вызов из цикла. Таким образом, 1241ms является справедливым показателем дляcalc2
.Обратите внимание, что
std::function
может хранить различные типы вызываемых объектов. Следовательно, он должен выполнить некоторую магию стирания типа для хранилища. Как правило, это подразумевает динамическое выделение памяти (по умолчанию через вызовnew
). Хорошо известно, что это довольно дорогостоящая операция.Стандарт (20.8.11.2.1 / 5) кодирует реализации, чтобы избежать динамического выделения памяти для небольших объектов, что, к счастью, делает VS2012 (в частности, для исходного кода).
Чтобы понять, насколько медленнее может быть процесс выделения памяти, я изменил лямбда-выражение на три
float
секунды. Это делает вызываемый объект слишком большим, чтобы применить оптимизацию маленького объекта:Для этой версии время составляет приблизительно 16000 мс (по сравнению с 1241 мс для исходного кода).
Наконец, обратите внимание, что время жизни лямбды включает в себя время жизни
std::function
. В этом случае вместо хранения копии лямбдыstd::function
можно хранить «ссылку» на нее. Под «ссылкой» я подразумеваю элемент,std::reference_wrapper
который легко построить с помощью функцийstd::ref
иstd::cref
. Точнее, используя:время уменьшается примерно до 1860 мс.
Я писал об этом некоторое время назад:
http://www.drdobbs.com/cpp/efficient-use-of-lambda-expressions-and/232500059
Как я уже говорил в статье, аргументы не совсем подходят для VS2010 из-за его слабой поддержки C ++ 11. На момент написания статьи была доступна только бета-версия VS2012, но его поддержка C ++ 11 уже была достаточно хороша для этого вопроса.
источник
calc1
можно принятьfloat
аргумент, который будет результатом предыдущей итерации. Нечто подобноеx = calc1(x, [](float arg){ return arg * 0.5f; });
. Кроме того, мы должны убедиться, чтоcalc1
используетx
. Но этого еще недостаточно. Нам нужно создать побочный эффект. Например, после измерения печатьx
на экране. Несмотря на то, что я согласен, что использование игрушечных кодов для измерений Timimg не всегда может дать точное представление о том, что произойдет с реальным / производственным кодом.std::reference_wrapper
(для принуждения шаблонов; это не только для общего хранения), и забавно видеть, что оптимизатору VS не удается отбросить пустой цикл ... как я заметил с этой ошибкой GCCvolatile
.С Clang нет никакой разницы в производительности между двумя
Используя clang (3.2, trunk 166872) (-O2 в Linux), двоичные файлы из этих двух случаев фактически идентичны .
-Я вернусь к лязгу в конце поста. Но сначала gcc 4.7.2:
Уже есть много понимания, но я хочу отметить, что результаты вычислений calc1 и calc2 не совпадают из-за встраивания и т. Д. Сравните, например, сумму всех результатов:
с calc2, который становится
в то время как с calc1 это становится
разница в скорости составляет ~ 40, а значения - ~ 4. Первый - намного большая разница, чем то, что опубликовал OP (используя visual studio). На самом деле распечатка значения a end также является хорошей идеей, чтобы запретить компилятору удалять код без видимого результата (как будто правило). Кассио Нери уже сказал это в своем ответе. Обратите внимание, насколько различны результаты. Следует соблюдать осторожность при сравнении коэффициентов скорости кодов, которые выполняют различные вычисления.
Также, чтобы быть справедливым, сравнение различных способов многократного вычисления f (3.3), возможно, не так интересно. Если вход постоянный, он не должен быть в цикле. (Оптимизатору легко заметить)
Если я добавлю аргумент пользовательского значения в calc1 и 2, коэффициент скорости между calc1 и calc2 снизится до 5 с 40! С визуальной студией разница близка к коэффициенту 2, а с лязгом нет никакой разницы (см. Ниже).
Кроме того, поскольку умножения быстры, говорить о факторах замедления часто не так интересно. Более интересный вопрос: насколько малы ваши функции, и являются ли эти вызовы узким местом в реальной программе?
Clang:
Clang (я использовал 3.2) фактически генерировал идентичные двоичные файлы, когда я переключался между calc1 и calc2 для кода примера (выложенного ниже). Исходный пример, размещенный в вопросе, также идентичен, но совсем не требует времени (циклы просто полностью удалены, как описано выше). В моем модифицированном примере с -O2:
Количество секунд для выполнения (лучшее из 3):
Результаты вычислений всех двоичных файлов одинаковы, и все тесты были выполнены на одной машине. Было бы интересно, если бы кто-то с более глубоким знанием лязга или VS мог прокомментировать, какие оптимизации могли быть сделаны.
Мой модифицированный тестовый код:
Обновить:
Добавлено vs2015. Я также заметил, что в calc1, calc2 есть преобразования double-> float. Удаление их не меняет вывод для визуальной студии (оба намного быстрее, но соотношение примерно одинаковое).
источник
Разное не то же самое.
Это медленнее, потому что он делает то, что шаблон не может сделать. В частности, он позволяет вам вызывать любую функцию, которая может быть вызвана с заданными типами аргументов и чей тип возврата может быть преобразован в заданный тип возврата из того же кода .
Обратите внимание, что один и тот же объект функции
fun
, передается обоим вызовамeval
. Он имеет две разные функции.Если вам не нужно это делать, то вам не следует использовать
std::function
.источник
У вас уже есть несколько хороших ответов, поэтому я не собираюсь противоречить им, вкратце, сравнение std :: function с шаблонами похоже на сравнение виртуальных функций с функциями. Вы никогда не должны «отдавать предпочтение» виртуальным функциям, а скорее использовать виртуальные функции, когда они соответствуют проблеме, перемещая решения от времени компиляции к времени выполнения. Идея состоит в том, что вместо того, чтобы решать проблему с помощью специального решения (например, таблицы переходов), вы используете что-то, что дает компилятору больше шансов для оптимизации. Это также помогает другим программистам, если вы используете стандартное решение.
источник
Этот ответ призван внести в набор существующих ответов то, что я считаю более значимым эталоном стоимости времени выполнения вызовов std :: function.
Механизм std :: function должен быть распознан за то, что он обеспечивает: любую вызываемую сущность можно преобразовать в std :: function соответствующей сигнатуры. Предположим, у вас есть библиотека, которая подгоняет поверхность к функции, определенной с помощью z = f (x, y), вы можете написать ее, чтобы принять a
std::function<double(double,double)>
, и пользователь библиотеки может легко преобразовать в нее любую вызываемую сущность; будь то обычная функция, метод экземпляра класса, или лямбда, или все, что поддерживается std :: bind.В отличие от шаблонных подходов, это работает без перекомпиляции библиотечной функции для разных случаев; соответственно, для каждого дополнительного случая требуется немного дополнительного скомпилированного кода. Это всегда было возможно сделать, но раньше требовалось несколько неуклюжих механизмов, и пользователю библиотеки, вероятно, понадобилось бы создать адаптер вокруг своей функции, чтобы он работал. std :: function автоматически создает любой адаптер, необходимый для получения общего интерфейса вызова во время выполнения для всех случаев, что является новой и очень мощной функцией.
На мой взгляд, это наиболее важный вариант использования std :: function с точки зрения производительности: меня интересует стоимость вызова std :: function много раз после того, как она была создана один раз, и она должна может быть ситуация, когда компилятор не может оптимизировать вызов, зная функцию, которая фактически вызывается (т.е. вам нужно скрыть реализацию в другом исходном файле, чтобы получить надлежащий тест).
Я сделал тест ниже, похожий на ОП; но основные изменения:
Результаты, которые я получаю:
case (a) (inline) 1,3 нс
все остальные случаи: 3,3 нс.
Случай (d) имеет тенденцию быть немного медленнее, но разница (около 0,05 нсек) поглощается шумом.
Вывод состоит в том, что std :: function сравнимо по накладным расходам (во время вызова) с использованием указателя на функцию, даже когда есть простая адаптация «привязки» к реальной функции. Встроенный на 2 нс быстрее, чем другие, но это ожидаемый компромисс, так как встроенный - единственный случай, который «запрограммирован» во время выполнения.
Когда я запускаю код Йохана-Лундберга на той же машине, я вижу около 39 нсек на цикл, но в цикле есть намного больше, включая фактический конструктор и деструктор функции std :: function, которая, вероятно, довольно высока так как включает в себя новый и удалить.
-O2 gcc 4.8.1, до цели x86_64 (ядро i5).
Обратите внимание, что код разбит на два файла, чтобы компилятор не расширял функции, где они вызываются (кроме одного случая, для которого он предназначен).
----- первый исходный файл --------------
----- второй исходный файл -------------
Для тех, кто интересуется, вот адаптер, который компилятор создал так, чтобы 'mul_by' выглядел как float (float) - он вызывается, когда вызывается функция, созданная как bind (mul_by, _1,0.5):
(так что это могло бы быть немного быстрее, если бы я написал 0.5f в привязке ...) Обратите внимание, что параметр 'x' прибывает в% xmm0 и просто остается там.
Вот код в области, где строится функция, до вызова test_stdfunc - запускается через c ++ фильтром:
источник
Я нашел ваши результаты очень интересными, поэтому я немного покопался, чтобы понять, что происходит. Прежде всего, как и многие другие, не имея результатов вычислений, влияет на состояние программы, которую компилятор просто оптимизирует. Во-вторых, имея константу 3.3 в качестве вооружения для обратного вызова, я подозреваю, что будут происходить другие оптимизации. Имея это в виду, я немного изменил ваш тестовый код.
Учитывая это изменение кода, я скомпилировал с помощью gcc 4.8 -O3 и получил время 330 мс для calc1 и 2702 для calc2. Таким образом, использование шаблона было в 8 раз быстрее, это число показалось мне подозрительным, скорость степени 8 часто указывает на то, что компилятор что-то векторизовал. когда я посмотрел на сгенерированный код для версии шаблона, он был явно vectoreized
Где как версии std :: function не было. Это имеет смысл для меня, так как с шаблоном компилятор точно знает, что функция никогда не изменится в цикле, но с передачей в нее std :: function может измениться, поэтому ее нельзя векторизовать.
Это заставило меня попробовать что-то еще, чтобы посмотреть, смогу ли я заставить компилятор выполнить ту же оптимизацию для версии std :: function. Вместо того, чтобы передавать функцию, я создаю std :: function как глобальную переменную и вызываю ее.
В этой версии мы видим, что компилятор теперь векторизовал код таким же образом, и я получил те же результаты тестов.
Таким образом, мой вывод заключается в том, что грубая скорость функции std :: function против шаблона функтора практически одинакова. Однако это значительно усложняет работу оптимизатора.
источник
calc3
дело не имеет смысла; calc3 теперь жестко запрограммирован для вызова f2. Конечно, это можно оптимизировать.