std :: function против шаблона

Благодаря 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 мс. Я предполагаю, что это связано с тем, что шаблоны могут быть хорошо встроены, в то время как functions покрывают внутреннее устройство с помощью виртуальных вызовов.

Очевидно, что у шаблонов есть свои проблемы, как я их вижу:

  • они должны быть предоставлены в виде заголовков, чего вы, возможно, не захотите делать при выпуске вашей библиотеки в виде закрытого кода,
  • они могут значительно увеличить время компиляции, если не будет введена extern template-подобная политика,
  • не существует (по крайней мере, известного мне) чистого способа представления требований (концепций, кого угодно?) шаблона, за исключением комментария, описывающего, какой тип функтора ожидается.

Могу ли я, таким образом, предположить, что functions можно использовать как де-факто стандарт передачи функторов и там, где ожидается высокая производительность, следует использовать шаблоны?


Редактировать:

Мой компилятор - Visual Studio 2012 без CTP.


person Red XIII    schedule 03.02.2013    source источник
comment
Используйте std::function тогда и только тогда, когда вам действительно нужна разнородная коллекция вызываемых объектов (т. Е. Во время выполнения не доступна никакая дополнительная различающая информация).   -  person Kerrek SB    schedule 04.02.2013
comment
Вы сравниваете не то. В обоих случаях используются шаблоны - это не std::function или шаблоны. Я думаю, что здесь проблема заключается в том, чтобы просто заключить лямбду в std::function, а не в std::function. На данный момент ваш вопрос подобен тому, что я предпочитаю яблоко или миску?   -  person Lightness Races in Orbit    schedule 04.02.2013
comment
Будь то 1 нс или 10 нс, оба ничего.   -  person ipc    schedule 04.02.2013
comment
@ipc: 1000% - это еще не ничего. Как указывает OP, вы начинаете заботиться о том, когда в него входит масштабируемость для каких-либо практических целей.   -  person Lightness Races in Orbit    schedule 04.02.2013
comment
Это может быть ужасно медленным на MSVC 10 и ниже. Огромная ошибка, приводившая к 10 копирующим конструкциям.   -  person doug65536    schedule 04.02.2013
comment
Как вы это измерили? Какие флаги компиляции использовались?   -  person Johannes Schaub - litb    schedule 04.02.2013
comment
@ipc Это в 10 раз медленнее, и это огромно. Скорость нужно сравнить с базовой; обманчиво думать, что это не имеет значения только потому, что это наносекунды.   -  person Paul Manta    schedule 04.02.2013
comment
возможно, уместно stackoverflow.com/questions/13722426/   -  person Stephan Dollberg    schedule 04.02.2013
comment
с boost::function, это просто занимает на 100% больше времени, чем версия шаблона (в 2 раза) (с GCC. Clang взял 0ns для версии шаблона. Похоже, он оптимизировал его). Я подозреваю, что вам следует указать реализацию, которую вы используете в тесте.   -  person Johannes Schaub - litb    schedule 04.02.2013
comment
@ doug65536, к счастью, вы можете исправить это, если заплатите за VC11.   -  person R. Martinho Fernandes    schedule 04.02.2013
comment
Может иметь значение, если он вызывается много раз, как в std::sort. В таких случаях я предпочитаю шаблоны (которые в любом случае в основном используются). Но в других случаях это действительно не имеет значения,   -  person ipc    schedule 04.02.2013
comment
@ R.MartinhoFernandes, поэтому я не буду использовать VC11.   -  person doug65536    schedule 04.02.2013
comment
В конце вы подвели итоги некоторых случаев, когда сами по себе шаблоны могут не справиться. Так что используйте std::function, когда он вам нужен. Куча недостатков простых аргументов функтора шаблона не волшебным образом делает std::function (который, как вы сами видели, имеет свои недостатки) стандартом де-факто.   -  person Christian Rau    schedule 04.02.2013
comment
@LightnessRacesinOrbit Ага, это в основном вопрос. Оборачивать лямбда / функтор или нет?   -  person Red XIII    schedule 05.02.2013
comment
@Christian: там, где одни шаблоны не могут его подрезать. Так что используйте std::function, когда он вам нужен. Проблема в том, что это ерунда, потому что std::function - это шаблон класса.   -  person Lightness Races in Orbit    schedule 06.02.2013
comment
@LightnessRacesinOrbit Да, я знаю. Я говорил в терминах OP, используя template для произвольно созданного аргумента функтора по сравнению с аргументом, заключенным в (более специализированный) std::function, игнорируя, что это использование template было немного неточно. Конечно, std::function тоже является шаблоном, но, в конце концов, он более специализирован / менее шаблонен, чем простой аргумент шаблона.   -  person Christian Rau    schedule 06.02.2013
comment
Да, гораздо менее шаблонный. Вы можете объявить переменную, например, std :: function ‹float (float)›, а затем сохраните все виды различных «функций» в этой одной переменной (например, назначив ей любой совместимый std :: bind). Применимые адаптеры созданы для обеспечения их совместимости с время выполнения с помощью одной операции «вызова». Это сильно отличает его от использования шаблона функции или шаблонного метода класса шаблона или чего-то еще, которые выполняют всю свою работу во время компиляции и генерируют другой код вызова в зависимости от того, что вызывается. Я поставил ответ, чтобы проиллюстрировать.   -  person greggo    schedule 05.03.2014
comment
И, как показано в нескольких ответах, время в этом примере не имеет значения. Вещи оптимизируются; а затраты на создание std :: function ложатся на вызов, что нетипично. В большинстве случаев, когда вам важна скорость, он будет создан один раз и вызываться много раз. Стоимость ctor / dtor может быть намного больше, чем стоимость звонка.   -  person greggo    schedule 05.03.2014
comment
Почему-то никто не сообщил, что часы разные. Оба now() должны быть на high_resolution_clock. Эта ошибка распространилась на все приведенные ниже фрагменты!   -  person akim    schedule 18.09.2014
comment
@akim действительно, на самом деле код OP не будет компилироваться в моем Clang, если обе часы не будут одними и теми же часами!   -  person johnbakers    schedule 14.02.2016
comment
Есть две проблемы с производительностью: выполнение шаблона vs std :: function и вызов конструктора std :: function. В режиме выпуска VS2017 результаты: 1000 против 7400. Когда я вытаскиваю конструкцию std :: function из цикла (и использую ссылочный аргумент const), разница составляет 1000 против 4477. Последнее число - это только разница в исполнении.   -  person gast128    schedule 16.09.2019


Ответы (8)


Как правило, если вы сталкиваетесь с ситуацией дизайна, которая дает вам выбор, используйте шаблоны. Я подчеркнул слово дизайн, потому что считаю, что вам нужно сосредоточить внимание на различии между вариантами использования std::function и шаблонами, которые сильно отличаются.

В общем, выбор шаблонов - это всего лишь пример более широкого принципа: постарайтесь указать как можно больше ограничений во время компиляции. Обоснование простое: если вы можете обнаружить ошибку или несоответствие типов, даже до того, как ваша программа будет сгенерирована, вы не отправите клиенту программу с ошибками.

Более того, как вы правильно указали, вызовы шаблонных функций разрешаются статически (то есть во время компиляции), поэтому у компилятора есть вся необходимая информация для оптимизации и, возможно, встраивания кода (что было бы невозможно, если бы вызов выполнялся через vtable).

Да, это правда, что поддержка шаблонов несовершенна, и C ++ 11 все еще не поддерживает концепции; однако я не понимаю, как std::function спасет вас в этом отношении. std::function - это не альтернатива шаблонам, а скорее инструмент для ситуаций, когда шаблоны использовать нельзя.

Один из таких вариантов использования возникает, когда вам нужно разрешить вызов во время выполнения путем вызова вызываемого объекта, который придерживается определенной сигнатуры, но чей конкретный тип неизвестен во время компиляции. Обычно это происходит, когда у вас есть коллекция обратных вызовов потенциально разных типов, но которые вам нужно вызывать единообразно; тип и количество зарегистрированных обратных вызовов определяется во время выполнения на основе состояния вашей программы и логики приложения. Некоторые из этих обратных вызовов могут быть функторами, некоторые могут быть простыми функциями, некоторые могут быть результатом привязки других функций к определенным аргументам.

std::function и std::bind также предлагают естественную идиому для включения функционального программирования в C ++, где функции рассматриваются как объекты и естественным образом обрабатываются и объединяются для создания других функций. Хотя такого рода комбинация также может быть достигнута с помощью шаблонов, аналогичная ситуация проектирования обычно сочетается с вариантами использования, которые требуют определения типа объединенных вызываемых объектов во время выполнения.

Наконец, есть и другие ситуации, в которых std::function неизбежно, например если вы хотите написать рекурсивные лямбды; однако я считаю, что эти ограничения больше продиктованы технологическими ограничениями, чем концептуальными различиями.

Подводя итог, сосредоточьтесь на дизайне и попытайтесь понять, каковы концептуальные варианты использования этих двух конструкций. Если вы сравниваете их так, как вы это делали, вы выталкиваете их на арену, к которой они, скорее всего, не принадлежат.

person Andy Prowl    schedule 03.02.2013
comment
Я думаю, что это типичный случай, когда у вас есть набор обратных вызовов потенциально разных типов, но которые вам нужно вызывать единообразно; это важный бит. Мое практическое правило: предпочитайте std::function на стороне хранилища и шаблон Fun на интерфейсе. - person R. Martinho Fernandes; 04.02.2013
comment
@ R.MartinhoFernandes: Я согласен с вами, хотя то, что я попытался передать в своем ответе, заключается в том, что OP должен сосредоточиться на вариантах использования, для которых две конструкции не перекрываются, а не на тех, для которых они перекрываются и могут быть по сравнению - с очевидными результатами. Технически ключевой дискриминант дизайна всегда один и тот же IMO: нужен ли динамический полиморфизм или нет. И да, упомянутая вами часть, вероятно, наиболее типична для проектных ситуаций, требующих динамического полиморфизма. - person Andy Prowl; 04.02.2013
comment
Примечание: метод скрытия конкретных типов называется стиранием типа (не путать со стиранием типа в управляемых языках). Он часто реализуется в терминах динамического полиморфизма, но является более мощным (например, unique_ptr<void> вызывает соответствующие деструкторы даже для типов без виртуальных деструкторов). - person ecatmur; 04.02.2013
comment
@ecatmur: Я согласен по существу, хотя мы немного не согласны по терминологии. Для меня динамический полиморфизм означает принятие различных форм во время выполнения, в отличие от статического полиморфизма, который я интерпретирую как принятие разных форм во время компиляции; последнее не может быть достигнуто с помощью шаблонов. Для меня стирание типа с точки зрения дизайна является своего рода предварительным условием для того, чтобы вообще можно было достичь динамического полиморфизма: вам нужен какой-то единый интерфейс для взаимодействия с объектами разных типов, а стирание типа - это способ абстрагироваться от типа. конкретная информация. - person Andy Prowl; 04.02.2013
comment
@ecatmur: В каком-то смысле динамический полиморфизм - это концептуальный паттерн, а стирание типов - это техника, позволяющая его реализовать. - person Andy Prowl; 04.02.2013
comment
@Downvoter: Мне было бы любопытно услышать, что вы нашли неправильным в этом ответе. - person Andy Prowl; 16.03.2013

Энди Проул прекрасно осветил проблемы дизайна. Это, конечно, очень важно, но я считаю, что исходный вопрос касается большего количества проблем с производительностью, связанных с std::function.

Прежде всего, небольшое замечание о методике измерения: 11 мс, полученные для calc1, не имеют никакого значения. Действительно, глядя на сгенерированную сборку (или отлаживая код сборки), можно увидеть, что оптимизатор VS2012 достаточно умен, чтобы понять, что результат вызова calc1 не зависит от итерации и перемещает вызов из цикла:

for (int i = 0; i < 1e8; ++i) {
}
calc1([](float arg){ return arg * 0.5f; });

Кроме того, он понимает, что вызов calc1 не имеет видимого эффекта, и полностью отбрасывает вызов. Следовательно, 111 мс - это время, необходимое для запуска пустого цикла. (Я удивлен, что оптимизатор сохранил цикл.) Так что будьте осторожны с измерениями времени в циклах. Это не так просто, как может показаться.

Как уже отмечалось, у оптимизатора больше проблем с пониманием std::function, и он не выводит вызов из цикла. Таким образом, 1241 мс - это справедливое измерение для calc2.

Обратите внимание, что std::function может хранить различные типы вызываемых объектов. Следовательно, он должен выполнить некоторую магию стирания типа для хранилища. Как правило, это подразумевает динамическое выделение памяти (по умолчанию через вызов new). Как известно, это довольно дорогостоящая операция.

Стандарт (20.8.11.2.1 / 5) кодирует реализации, чтобы избежать выделения динамической памяти для небольших объектов, что, к счастью, делает VS2012 (в частности, для исходного кода).

Чтобы понять, насколько он может работать медленнее при выделении памяти, я изменил лямбда-выражение, чтобы захватить три float. Это делает вызываемый объект слишком большим для применения оптимизации малого объекта:

float a, b, c; // never mind the values
// ...
calc2([a,b,c](float arg){ return arg * 0.5f; });

Для этой версии время составляет приблизительно 16000 мс (по сравнению с 1241 мс для исходного кода).

Наконец, обратите внимание, что время жизни лямбды включает время жизни std::function. В этом случае вместо сохранения копии лямбда std::function может хранить "ссылку" на нее. Под «ссылкой» я подразумеваю std::reference_wrapper, который легко создается функциями std::ref и std::cref. Точнее, используя:

auto func = [a,b,c](float arg){ return arg * 0.5f; };
calc2(std::cref(func));

время уменьшается примерно до 1860 мс.

Я писал об этом некоторое время назад:

http://www.drdobbs.com/cpp/efficient-use-of-lambda-expressions-and/232500059

Как я сказал в статье, эти аргументы не совсем подходят для VS2010 из-за его плохой поддержки C ++ 11. На момент написания была доступна только бета-версия VS2012, но ее поддержка C ++ 11 уже была достаточно хорошей для этого.

person Cassio Neri    schedule 22.02.2013
comment
Я нахожу это действительно интересным, желая доказать скорость кода, используя игрушечные примеры, которые оптимизируются компилятором, потому что у них нет никаких побочных эффектов. Я бы сказал, что редко можно сделать ставку на такие измерения без реального / производственного кода. - person Ghita; 23.02.2013
comment
@ Ghita: В этом примере, чтобы предотвратить оптимизацию кода, calc1 может принимать аргумент float, который был бы результатом предыдущей итерации. Что-то вроде x = calc1(x, [](float arg){ return arg * 0.5f; });. Кроме того, мы должны убедиться, что calc1 использует x. Но этого еще недостаточно. Нам нужно создать побочный эффект. Например, после измерения печать x на экране. Хотя я согласен с тем, что использование игрушечных кодов для измерения времени не всегда может дать точное представление о том, что произойдет с реальным / производственным кодом. - person Cassio Neri; 24.02.2013
comment
Мне тоже кажется, что тест конструирует объект std :: function внутри цикла и вызывает в нем calc2. Независимо от того, что компилятор может или не может оптимизировать это (и что конструктор может быть таким же простым, как сохранение vptr), меня больше интересует случай, когда функция создается один раз и передается другой функции, которая вызывает это в петле. Т.е. накладные расходы на вызов, а не время построения (и вызов 'f', а не calc2). Также было бы интересно, если бы вызов f в цикле (в calc2), а не один раз, выиграл бы от любого подъема. - person greggo; 03.03.2014
comment
Отличный ответ. Две вещи: хороший пример допустимого использования std::reference_wrapper (для принуждения шаблонов; это не только для общего хранилища), и забавно видеть, что оптимизатор VS не может отбросить пустой цикл ... как я заметил с помощью эта ошибка GCC повторно volatile. - person underscore_d; 17.07.2016

С Clang нет разницы в производительности между двумя

Используя clang (3.2, trunk 166872) (-O2 в Linux), двоичные файлы из двух случаев фактически идентичны.

-Я вернусь лязгать в конце поста. Но сначала gcc 4.7.2:

Уже есть много идей, но я хочу отметить, что результаты вычислений calc1 и calc2 не совпадают из-за встраивания и т. Д. Сравните, например, сумму всех результатов:

float result=0;
for (int i = 0; i < 1e8; ++i) {
  result+=calc2([](float arg){ return arg * 0.5f; });
}

с calc2, который становится

1.71799e+10, time spent 0.14 sec

в то время как с calc1 становится

6.6435e+10, time spent 5.772 sec

это примерно 40 раз в разнице скоростей и примерно 4 раза в значениях. Первое - это гораздо большая разница, чем то, что опубликовал OP (с использованием Visual Studio). На самом деле распечатка значения в конце также является хорошей идеей, чтобы компилятор не удалял код без видимого результата (правило as-if). Кассио Нери уже сказал об этом в своем ответе. Обратите внимание, насколько разные результаты - следует быть осторожными при сравнении коэффициентов скорости кодов, выполняющих разные вычисления.

Кроме того, честно говоря, сравнение различных способов многократного вычисления f (3.3), возможно, не так уж интересно. Если ввод постоянный, он не должен быть в цикле. (Оптимизатор легко заметит)

Если я добавлю аргумент значения, задаваемый пользователем, к calc1 и 2, коэффициент скорости между calc1 и calc2 снизится до 5, а не 40! С Visual Studio разница близка к коэффициенту 2, а с clang нет никакой разницы (см. Ниже).

Кроме того, поскольку умножение происходит быстро, говорить о факторах замедления часто не так интересно. Более интересный вопрос: насколько малы ваши функции и являются ли эти вызовы узким местом в реальной программе?

Лязг:

Clang (я использовал 3.2) фактически создавал идентичные двоичные файлы, когда я переключаюсь между calc1 и calc2 для примера кода (размещенного ниже). В исходном примере, опубликованном в вопросе, оба они также идентичны, но совсем не занимают времени (циклы просто полностью удаляются, как описано выше). В моем модифицированном примере с -O2:

Количество секунд для выполнения (максимум 3):

clang:        calc1:           1.4 seconds
clang:        calc2:           1.4 seconds (identical binary)

gcc 4.7.2:    calc1:           1.1 seconds
gcc 4.7.2:    calc2:           6.0 seconds

VS2012 CTPNov calc1:           0.8 seconds 
VS2012 CTPNov calc2:           2.0 seconds 

VS2015 (14.0.23.107) calc1:    1.1 seconds 
VS2015 (14.0.23.107) calc2:    1.5 seconds 

MinGW (4.7.2) calc1:           0.9 seconds
MinGW (4.7.2) calc2:          20.5 seconds 

Расчетные результаты всех двоичных файлов одинаковы, и все тесты выполнялись на одной машине. Было бы интересно, если бы кто-нибудь с более глубоким знанием clang или VS мог прокомментировать, какие оптимизации могли быть сделаны.

Мой модифицированный тестовый код:

#include <functional>
#include <chrono>
#include <iostream>

template <typename F>
float calc1(F f, float x) { 
  return 1.0f + 0.002*x+f(x*1.223) ; 
}

float calc2(std::function<float(float)> f,float x) { 
  return 1.0f + 0.002*x+f(x*1.223) ; 
}

int main() {
    using namespace std::chrono;

    const auto tp1 = high_resolution_clock::now();

    float result=0;
    for (int i = 0; i < 1e8; ++i) {
      result=calc1([](float arg){ 
          return arg * 0.5f; 
        },result);
    }
    const auto tp2 = high_resolution_clock::now();

    const auto d = duration_cast<milliseconds>(tp2 - tp1);  
    std::cout << d.count() << std::endl;
    std::cout << result<< std::endl;
    return 0;
}

Обновление:

Добавлен vs2015. Я также заметил, что в calc1, calc2 есть преобразования double-> float. Их удаление не меняет вывода для Visual Studio (оба работают намного быстрее, но соотношение примерно одинаковое).

person Johan Lundberg    schedule 23.02.2013
comment
Что, возможно, просто показывает, что эталонный тест ошибочен. IMHO интересный вариант использования - это когда вызывающий код получает объект функции откуда-то еще, поэтому компилятор не знает происхождение std :: function при компиляции вызова. Здесь компилятор точно знает состав std :: function при ее вызове, расширяя calc2 inline в main. Легко исправить, сделав calc2 'extern' в sep. исходный файл. Затем вы сравниваете яблоки с апельсинами; calc2 делает то, что не может calc1. И цикл может быть внутри calc (много вызовов f); не вокруг ctor функционального объекта. - person greggo; 04.03.2014
comment
Когда я смогу добраться до подходящего компилятора. На данный момент можно сказать, что (а) ctor для фактического std :: function вызывает 'new'; (б) сам вызов довольно скудный, когда целью является совпадающая фактическая функция; (c) в случаях с привязкой есть фрагмент кода, который выполняет адаптацию, выбранный кодом ptr в функции obj, и который собирает данные (связанные параметры) из функции obj (d) «связанная» функция может быть встроенным в этот адаптер, если компилятор его видит. - person greggo; 05.03.2014
comment
Добавлен новый ответ с описанной настройкой. - person greggo; 05.03.2014
comment
Кстати, эталонный тест не ошибочен, вопрос (std :: function vs template) действителен только в рамках одного и того же модуля компиляции. Если вы переместите функцию в другой модуль, использование шаблона станет невозможным, поэтому сравнивать не с чем. - person rustyx; 02.04.2016

Другое - это не одно и то же.

Он медленнее, потому что делает то, чего не может сделать шаблон. В частности, он позволяет вызывать любую функцию, которая может быть вызвана с заданными типами аргументов и тип возврата которой может быть преобразован в заданный тип возврата из того же кода.

void eval(const std::function<int(int)>& f) {
    std::cout << f(3);
}

int f1(int i) {
    return i;
}

float f2(double d) {
    return d;
}

int main() {
    std::function<int(int)> fun(f1);
    eval(fun);
    fun = f2;
    eval(fun);
    return 0;
}

Обратите внимание, что тот же объект функции, fun, передается в оба вызова eval. Он выполняет две разные функции.

Если в этом нет необходимости, не используйте std::function.

person Pete Becker    schedule 03.02.2013
comment
Просто хочу отметить, что после выполнения 'fun = f2' объект 'fun' в конечном итоге указывает на скрытую функцию, которая преобразует int в double, вызывает f2 и преобразует результат double обратно в int. (В фактическом примере , 'f2' может быть встроено в эту функцию). Если вы назначите std :: bind для fun, объект fun может в конечном итоге содержать значения, которые будут использоваться для связанных параметров. для поддержки этой гибкости назначение функции fun (или инициализации) может включать выделение / освобождение памяти, что может занять больше времени, чем фактические накладные расходы на вызов. - person greggo; 06.03.2014

У вас уже есть хорошие ответы, поэтому я не собираюсь им противоречить, короче говоря, сравнение std :: function с шаблонами похоже на сравнение виртуальных функций с функциями. Вы никогда не должны «предпочитать» виртуальные функции функциям, вы скорее используете виртуальные функции, когда это соответствует задаче, перемещая решения от времени компиляции к времени выполнения. Идея состоит в том, что вместо того, чтобы решать проблему с помощью специального решения (например, таблицы переходов), вы используете что-то, что дает компилятору больше шансов на оптимизацию для вас. Это также помогает другим программистам, если вы используете стандартное решение.

person TheAgitator    schedule 21.02.2013

Этот ответ призван внести вклад в набор существующих ответов, что я считаю более значимым эталоном стоимости выполнения вызовов std :: function.

Механизм std :: function следует распознать по тому, что он предоставляет: любой вызываемый объект может быть преобразован в std :: function соответствующей сигнатуры. Предположим, у вас есть библиотека, которая соответствует поверхности функции, определенной как z = f (x, y), вы можете написать ее так, чтобы она принимала std::function<double(double,double)>, и пользователь библиотеки может легко преобразовать в нее любую вызываемую сущность; будь то обычная функция, метод экземпляра класса или лямбда, или что-то еще, что поддерживается std :: bind.

В отличие от шаблонных подходов, это работает без необходимости перекомпилировать библиотечную функцию для разных случаев; соответственно, для каждого дополнительного случая требуется немного дополнительного скомпилированного кода. Это всегда было возможно, но раньше для этого требовались некоторые неудобные механизмы, и пользователю библиотеки, вероятно, нужно было создать адаптер для своей функции, чтобы заставить ее работать. std :: function автоматически создает любой адаптер, необходимый для получения общего интерфейса вызова времени выполнения для всех случаев, что является новой и очень мощной функцией.

На мой взгляд, это наиболее важный вариант использования std :: function с точки зрения производительности: меня интересует стоимость вызова std :: function много раз после того, как она была построена один раз, и ей необходимо быть ситуацией, когда компилятор не может оптимизировать вызов, зная, какая функция действительно вызывается (т.е. вам нужно скрыть реализацию в другом исходном файле, чтобы получить надлежащий тест).

Я сделал тест ниже, похожий на OP; но основные изменения:

  1. Каждый case повторяется 1 миллиард раз, но объекты std :: function создаются только один раз. Посмотрев на выходной код, я обнаружил, что оператор new вызывается при создании фактических вызовов std :: function (возможно, не тогда, когда они оптимизированы).
  2. Тест разделен на два файла, чтобы предотвратить нежелательную оптимизацию.
  3. Мои случаи: (a) функция встроена (b) функция передается обычным указателем на функцию (c) функция является совместимой функцией, завернутой как std :: function (d) функция является несовместимой функцией, совместимой с std :: привязать, завернутый как std :: function

Я получаю следующие результаты:

  • case (a) (встроенный) 1,3 нс

  • во всех остальных случаях: 3,3 нс.

Случай (d) имеет тенденцию быть немного медленнее, но разница (около 0,05 нс) поглощается шумом.

Вывод состоит в том, что std :: function накладные расходы (во время вызова) сопоставимы с использованием указателя функции, даже если существует простая адаптация «привязки» к фактической функции. Встроенный на 2 нс быстрее, чем другие, но это ожидаемый компромисс, поскольку встроенный - единственный случай, который «жестко закреплен» во время выполнения.

Когда я запускаю код johan-lundberg на той же машине, я вижу около 39 нс на цикл, но там гораздо больше в цикле, включая фактический конструктор и деструктор std :: function, который, вероятно, довольно высок. так как он предполагает создание и удаление.

-O2 gcc 4.8.1, для цели x86_64 (core i5).

Обратите внимание: код разбит на два файла, чтобы компилятор не расширял функции, в которых они вызываются (за исключением одного случая, когда он предназначен).

----- первый исходный файл --------------

#include <functional>


// simple funct
float func_half( float x ) { return x * 0.5; }

// func we can bind
float mul_by( float x, float scale ) { return x * scale; }

//
// func to call another func a zillion times.
//
float test_stdfunc( std::function<float(float)> const & func, int nloops ) {
    float x = 1.0;
    float y = 0.0;
    for(int i =0; i < nloops; i++ ){
        y += x;
        x = func(x);
    }
    return y;
}

// same thing with a function pointer
float test_funcptr( float (*func)(float), int nloops ) {
    float x = 1.0;
    float y = 0.0;
    for(int i =0; i < nloops; i++ ){
        y += x;
        x = func(x);
    }
    return y;
}

// same thing with inline function
float test_inline(  int nloops ) {
    float x = 1.0;
    float y = 0.0;
    for(int i =0; i < nloops; i++ ){
        y += x;
        x = func_half(x);
    }
    return y;
}

----- второй исходный файл -------------

#include <iostream>
#include <functional>
#include <chrono>

extern float func_half( float x );
extern float mul_by( float x, float scale );
extern float test_inline(  int nloops );
extern float test_stdfunc( std::function<float(float)> const & func, int nloops );
extern float test_funcptr( float (*func)(float), int nloops );

int main() {
    using namespace std::chrono;


    for(int icase = 0; icase < 4; icase ++ ){
        const auto tp1 = system_clock::now();

        float result;
        switch( icase ){
         case 0:
            result = test_inline( 1e9);
            break;
         case 1:
            result = test_funcptr( func_half, 1e9);
            break;
         case 2:
            result = test_stdfunc( func_half, 1e9);
            break;
         case 3:
            result = test_stdfunc( std::bind( mul_by, std::placeholders::_1, 0.5), 1e9);
            break;
        }
        const auto tp2 = high_resolution_clock::now();

        const auto d = duration_cast<milliseconds>(tp2 - tp1);  
        std::cout << d.count() << std::endl;
        std::cout << result<< std::endl;
    }
    return 0;
}

Для тех, кто интересуется, вот адаптер, созданный компилятором, чтобы сделать 'mul_by' похожим на float (float) - он 'вызывается', когда вызывается функция, созданная как bind (mul_by, _1,0.5):

movq    (%rdi), %rax                ; get the std::func data
movsd   8(%rax), %xmm1              ; get the bound value (0.5)
movq    (%rax), %rdx                ; get the function to call (mul_by)
cvtpd2ps    %xmm1, %xmm1        ; convert 0.5 to 0.5f
jmp *%rdx                       ; jump to the func

(так что, возможно, было бы немного быстрее, если бы я написал 0.5f в привязке ...) Обратите внимание, что параметр 'x' прибывает в% xmm0 и просто остается там.

Вот код в области, где построена функция, перед вызовом test_stdfunc - выполните c ++ filter:

movl    $16, %edi
movq    $0, 32(%rsp)
call    operator new(unsigned long)      ; get 16 bytes for std::function
movsd   .LC0(%rip), %xmm1                ; get 0.5
leaq    16(%rsp), %rdi                   ; (1st parm to test_stdfunc) 
movq    mul_by(float, float), (%rax)     ; store &mul_by  in std::function
movl    $1000000000, %esi                ; (2nd parm to test_stdfunc)
movsd   %xmm1, 8(%rax)                   ; store 0.5 in std::function
movq    %rax, 16(%rsp)                   ; save ptr to allocated mem

   ;; the next two ops store pointers to generated code related to the std::function.
   ;; the first one points to the adaptor I showed above.

movq    std::_Function_handler<float (float), std::_Bind<float (*(std::_Placeholder<1>, double))(float, float)> >::_M_invoke(std::_Any_data const&, float), 40(%rsp)
movq    std::_Function_base::_Base_manager<std::_Bind<float (*(std::_Placeholder<1>, double))(float, float)> >::_M_manager(std::_Any_data&, std::_Any_data const&, std::_Manager_operation), 32(%rsp)


call    test_stdfunc(std::function<float (float)> const&, int)
person greggo    schedule 05.03.2014
comment
С clang 3.4.1 x64 результаты: (a) 1.0, (b) 0.95, (c) 2.0, (d) 5.0. - person rustyx; 01.04.2016

Мне показались ваши результаты очень интересными, поэтому я немного покопался, чтобы понять, что происходит. Во-первых, как говорили многие другие, не имея результатов вычислений, состояние программы компилятор просто оптимизирует это. Во-вторых, имея константу 3.3 в качестве вооружения для обратного вызова, я подозреваю, что будут и другие оптимизации. Имея это в виду, я немного изменил ваш тестовый код.

template <typename F>
float calc1(F f, float i) { return -1.0f * f(i) + 666.0f; }
float calc2(std::function<float(float)> f, float i) { return -1.0f * f(i) + 666.0f; }
int main() {
    const auto tp1 = system_clock::now();
    for (int i = 0; i < 1e8; ++i) {
        t += calc2([&](float arg){ return arg * 0.5f + t; }, i);
    }
    const auto tp2 = high_resolution_clock::now();
}

Учитывая это изменение кода, я скомпилировал с помощью gcc 4.8 -O3 и получил время 330 мс для calc1 и 2702 для calc2. Таким образом, использование шаблона было в 8 раз быстрее, это число показалось мне подозрительным, скорость в 8 часто указывает на то, что компилятор что-то векторизовал. когда я посмотрел на сгенерированный код для версии шаблонов, он был явно векторизован

.L34:
cvtsi2ss        %edx, %xmm0
addl    $1, %edx
movaps  %xmm3, %xmm5
mulss   %xmm4, %xmm0
addss   %xmm1, %xmm0
subss   %xmm0, %xmm5
movaps  %xmm5, %xmm0
addss   %xmm1, %xmm0
cvtsi2sd        %edx, %xmm1
ucomisd %xmm1, %xmm2
ja      .L37
movss   %xmm0, 16(%rsp)

Где как версии std :: function не было. Для меня это имеет смысл, поскольку с шаблоном компилятор точно знает, что функция никогда не изменится на протяжении всего цикла, но с передаваемым в нем std :: function может измениться, поэтому не может быть векторизован.

Это побудило меня попробовать что-то еще, чтобы увидеть, смогу ли я заставить компилятор выполнить ту же оптимизацию для версии std :: function. Вместо передачи функции я делаю std :: function как глобальную переменную и вызываю ее.

float calc3(float i) {  return -1.0f * f2(i) + 666.0f; }
std::function<float(float)> f2 = [](float arg){ return arg * 0.5f; };

int main() {
    const auto tp1 = system_clock::now();
    for (int i = 0; i < 1e8; ++i) {
        t += calc3([&](float arg){ return arg * 0.5f + t; }, i);
    }
    const auto tp2 = high_resolution_clock::now();
}

В этой версии мы видим, что компилятор таким же образом векторизовал код, и я получаю те же результаты тестов.

  • шаблон: 330 мс
  • std :: function: 2702 мс
  • глобальный std :: function: 330 мс

Итак, я пришел к выводу, что чистая скорость std :: function по сравнению с функтором шаблона практически одинакова. Однако это значительно усложняет работу оптимизатора.

person Joshua Ritterman    schedule 18.08.2014
comment
Все дело в том, чтобы передать функтор в качестве параметра. Ваш calc3 случай не имеет смысла; calc3 теперь жестко запрограммирован для вызова f2. Конечно, это можно оптимизировать. - person rustyx; 03.06.2016
comment
действительно, это то, что я пытался показать. Этот calc3 эквивалентен шаблону и в этой ситуации фактически является конструкцией времени компиляции, как и шаблон. - person Joshua Ritterman; 22.07.2016

Если вы используете шаблон вместо std::function в C ++ 20, вы можете написать свою собственную концепцию с вариативными шаблонами для него (вдохновлено выступлением Хендрика Нимейера о концепциях C ++ 20):

template<class Func, typename Ret, typename... Args>
concept functor = std::regular_invocable<Func, Args...> && 
                  std::same_as<std::invoke_result_t<Func, Args...>, Ret>;

Затем вы можете использовать его как functor<Ret, Args...> F>, где Ret - возвращаемое значение, а Args... - переменные входные аргументы. Например. functor<double,int> F например

template <functor<double,int> F>
auto CalculateSomething(F&& f, int const arg) {
  return f(arg)*f(arg);
}

требуется функтор в качестве аргумента шаблона, который должен перегружать оператор () и имеет возвращаемое значение double и единственный входной аргумент типа int. Точно так же functor<double> будет функтором с типом возврата double, который не принимает никаких входных аргументов.

Попробуйте здесь!

Вы также можете использовать его с вариативными функциями, такими как

template <typename... Args, functor<double, Args...> F>
auto CalculateSomething(F&& f, Args... args) {
  return f(args...)*f(args...);
}

Попробуйте здесь!

person 2b-t    schedule 01.06.2021