std :: функція проти шаблону


161

Завдяки 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.


16
Використовуйте std::function якщо і лише тоді, коли вам справді потрібна неоднорідна колекція об'єктів, що дзвоняться (тобто додаткова дискримінаційна інформація недоступна під час виконання).
Керрек СБ

30
Ти порівнюєш неправильні речі. Шаблони використовуються в обох випадках - це не " std::functionабо шаблони". Я думаю, що тут проблема полягає в простому загортанні лямбда в, а std::functionне в упаковці лямбдаstd::function . На даний момент ваше запитання схоже на запитання: "чи варто віддати перевагу яблуку чи мисці?"
Гонки легкості на орбіті

7
Будь 1ns чи 10ns, і те й інше - це нічого.
ipc

23
@ipc: 1000% - це не нічого. Як визначає ОП, ви починаєте дбати про те, коли масштабність потрапляє в неї з будь-яких практичних цілей.
Гонки легкості на орбіті

18
@ipc Це в 10 разів повільніше, що величезно. Швидкість потрібно порівнювати з базовою; оманливо вважати, що це не має значення лише тому, що це наносекунд.
Пол Манта

Відповіді:


170

Взагалі, якщо ви стикаєтеся з дизайнерською ситуацією, яка дає вам вибір, використовуйте шаблони . Я наголосив на дизайні слів, тому що я думаю, що вам потрібно зосередитись на тому, щоб розрізняти випадки використання std::functionта шаблони, які сильно відрізняються.

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

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

Так, це правда, що підтримка шаблонів не є ідеальною, а C ++ 11 все ще не підтримує концепцій; проте я не бачу, як std::functionби врятувати вас у цьому відношенні. std::functionне є альтернативою шаблонам, а скоріше інструментом для дизайнерських ситуацій, коли шаблони не можна використовувати.

Один такий випадок використання виникає, коли вам потрібно вирішити виклик під час виконання шляхом виклику об'єкта, що викликається, який дотримується певної сигнатури, але конкретний тип якого невідомий під час компіляції. Зазвичай це має місце, коли у вас є колекція зворотних викликів потенційно різних типів , але які потрібно викликати рівномірно ; тип та кількість зареєстрованих зворотних зворотних дзвінків визначаються під час виконання на основі стану вашої програми та логіки програми. Деякі з цих зворотних викликів можуть бути функторами, деякі можуть бути простими функціями, деякі можуть бути результатом прив’язки інших функцій до певних аргументів.

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

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

Підводячи підсумок, сфокусуйтесь на дизайні та спробуйте зрозуміти, які є концептуальні випадки використання цих двох конструкцій. Якщо ви порівняєте їх так, як ви це робили, ви змушуєте їх вийти на арену, до якої, ймовірно, не належать.


23
Я думаю, що "це зазвичай так, коли у вас є колекція зворотних викликів потенційно різних типів, але які потрібно викликати рівномірно"; є важливим бітом. Моє правило: "Віддайте перевагу std::functionна кінці пам’яті та шаблоні Funв інтерфейсі".
Р. Мартіньо Фернандес

2
Примітка: техніку приховування типів бетону називають стиранням типу (не плутати з стиранням типу в керованих мовах). Він часто реалізується з точки зору динамічного поліморфізму, але є більш потужним (наприклад, unique_ptr<void>викликом відповідних деструкторів навіть для типів без віртуальних деструкторів).
екатмур

2
@ecatmur: Я погоджуюся з сутністю, хоча ми терміново не узгоджуємось. Динамічний поліморфізм для мене означає "прийняття різних форм під час виконання", на відміну від статичного поліморфізму, який я трактую як "припускання різних форм під час компіляції"; останнє неможливо досягти за допомогою шаблонів. Для мене тип стирання є, по-дизайнерськи, своєрідною передумовою для того, щоб взагалі домогтися динамічного поліморфізму: вам потрібен єдиний інтерфейс для взаємодії з об'єктами різних типів, а стирання типу - це спосіб абстрагувати від типу типу конкретна інформація.
Енді Проул

2
@ecatmur: Таким чином, динамічний поліморфізм є концептуальною схемою, тоді як стирання типу - це техніка, яка дозволяє його реалізувати.
Енді Проул

2
@Downvoter: Мені було б цікаво почути, що ти знайшов неправильно у цій відповіді.
Енді Проул

89

Енді Проул чудово висвітлив питання дизайну. Це, звичайно, дуже важливо, але я вважаю, що оригінальне питання стосується більшої кількості питань, пов'язаних із виконанням робіт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 вже була достатньою для цього.


Мені це дійсно цікаво, бажаючи довести швидкість коду за допомогою прикладів іграшок, які оптимізуються компілятором, оскільки вони не мають жодних побічних ефектів. Я б сказав, що рідко можна зробити ставку на такі вимірювання, не маючи реального виробничого коду.
Ghita

@ Ghita: У цьому прикладі, щоб запобігти оптимізації коду, calc1можна взяти floatаргумент, який був би результатом попередньої ітерації. Щось подібне x = calc1(x, [](float arg){ return arg * 0.5f; });. Крім того, ми повинні забезпечити calc1використання x. Але цього ще недостатньо. Нам потрібно створити побічний ефект. Наприклад, після вимірювання, друк xна екрані. Незважаючи на те, я погоджуюся, що використання іграшкових кодів для вимірювання timimg не завжди може дати ідеальну ознаку того, що буде з реальним / виробничим кодом.
Кассіо Нері

Мені теж здається, що тест конструює об’єкт std :: function всередині циклу і викликає calc2 у циклі. Незалежно від того, що компілятор може або не може цього оптимізувати (і що конструктор може бути таким же простим, як і зберігання vptr), мене більше зацікавить випадок, коли функція побудована один раз, і перейде до іншої функції, яка викликає це в петлі. Тобто накладні виклики, а не час побудови (і виклик 'f', а не calc2). Також буде цікаво, якщо виклик f в циклі (у calc2), а не один раз, виграє від будь-якого підйому.
greggo

Чудова відповідь. 2 речі: чудовий приклад правильного використання для std::reference_wrapper(примушування шаблонів; це не лише для загального зберігання), і смішно бачити, що оптимізатор VS не зможе скинути порожній цикл ... як я помітив із цим помилкою GCCvolatile .
підкреслюй_d

37

У Clang немає різниці в роботі між ними

Використовуючи clang (3.2, магістраль 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 у значеннях. Перший - набагато більша різниця, ніж те, що розміщено в ОП (використовуючи візуальну студію). Насправді роздрукувати значення a end - це також гарна ідея запобігти видаленню коду без видимого результату (як-як правило). Кассіо Нері вже сказав це у своїй відповіді. Зауважте, наскільки різні результати - Ви повинні бути обережними, порівнюючи коефіцієнти швидкості кодів, які виконують різні обчислення.

Також, якщо бути справедливим, порівнювати різні способи багаторазового обчислення f (3.3), можливо, не так цікаво. Якщо вхід постійний, він не повинен знаходитися в циклі. (Оптимізатору це легко помітити)

Якщо я додаю аргумент значення, що надається користувачем, до calc1 та 2, коефіцієнт швидкості між calc1 та calc2 зводиться до значення 5, від 40! У візуальній студії різниця близька до коефіцієнта 2, а при кланге немає різниці (див. Нижче).

Крім того, оскільки примноження швидко, розмови про фактори уповільнення часто не такі цікаві. Більш цікаве питання полягає в тому, наскільки малі ваші функції, і чи є ці виклики вузьким місцем у реальній програмі?

Кланг:

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 

Підраховані результати всіх двійкових файлів однакові, і всі тести виконувались на одній машині. Було б цікаво, якби хтось із глибшими знаннями клаксону або 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 є подвійні> плаваючі перетворення. Видалення їх не змінює висновок для візуальної студії (обидва набагато швидше, але співвідношення приблизно однакове).


8
Що, мабуть, просто показує тест, є неправильним. IMHO цікавий випадок використання - це те, що викличний код отримує об’єкт функції з іншого місця, тому компілятор не знає походження функції std :: під час компіляції виклику. Тут компілятор точно знає склад функції std :: при його виклику шляхом розширення calc2 inline в main. Легко фіксується, роблячи calc2 'extern' у сеп. вихідний файл. Ви порівнюєте яблука з апельсинами; calc2 робить щось calc1 не може. І цикл може бути всередині calc (багато дзвінків до f); не навколо ctor об'єкта функції.
greggo

1
Коли я можу потрапити до відповідного компілятора. Наразі можна сказати, що (a) ctor для фактично std :: функції викликає "new"; (b) сам виклик досить мізерний, коли ціль відповідає фактичній функції; (c) у випадках з прив'язкою є фрагмент коду, який виконує адаптацію, вибрану кодом ptr у функції obj, і який збирає дані (прив'язані парми) з функції obj (d) функція "прив'язка" може бути вкладеним у цей адаптер, якщо компілятор може це бачити.
greggo

Додано нову відповідь із описаними налаштуваннями.
greggo

3
BTW Тест не є помилковим, питання ("std :: функція проти шаблону") є дійсним лише в межах одного блоку компіляції. Якщо ви перемістите функцію на інший блок, шаблон більше не можливий, тому немає з чим порівняти.
rustyx

13

Різне не те саме.

Це повільніше, оскільки він робить те, що шаблон не може зробити. Зокрема, він дозволяє викликати будь-яку функцію, яку можна викликати за допомогою заданих типів аргументів і тип повернення якої може бути конвертований у даний тип повернення з того самого коду .

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.


2
Просто хочу зазначити, що коли 'fun = f2' зроблено, об’єкт 'fun' закінчується, вказуючи на приховану функцію, яка перетворює int в подвійний, викликає f2 і перетворює подвійний результат назад в int. (У фактичному прикладі , 'f2' може вписатись у цю функцію). Якщо призначити std :: bind to fun, об’єкт 'fun' може в кінцевому підсумку містити значення, які будуть використані для прив’язаних параметрів. Щоб підтримати цю гнучкість, присвоєння функції "веселощі" (або init of) може передбачати розподіл / розміщення пам'яті, і це може зайняти більше часу, ніж фактичні накладні витрати.
greggo

8

Ви вже маєте кілька хороших відповідей, тому я не збираюся їм суперечити, коротше кажучи, порівнювати std :: функцію з шаблонами, як порівняння віртуальних функцій з функціями. Ніколи не слід «віддавати перевагу» віртуальним функціям функціям, а скоріше використовувати віртуальні функції, коли це відповідає проблемі, переміщуючи рішення з часу компіляції на час виконання. Ідея полягає в тому, що замість того, щоб вирішувати проблему за допомогою індивідуального рішення (наприклад, таблиці стрибків), ви використовуєте щось, що дає компілятору кращі шанси оптимізувати для вас. Це також допомагає іншим програмістам, якщо ви використовуєте стандартне рішення.


6

Ця відповідь покликана сприяти набору існуючих відповідей, що, на мою думку, є більш значущим орієнтиром для вартості виконання викликів std :: function.

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

На відміну від шаблонного підходу, це працює без необхідності перекомпілювати функцію бібліотеки для різних випадків; відповідно, небагато зайвого складеного коду потрібно для кожного додаткового випадку. Це завжди можна було зробити, але це вимагало деяких незручних механізмів, і користувачеві бібліотеки, ймовірно, доведеться побудувати адаптер навколо своєї функції, щоб змусити його працювати. std :: функція автоматично створює необхідний адаптер, щоб отримати загальний інтерфейс для виклику виконання для всіх випадків, що є новою та дуже потужною функцією.

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

Я зробив тест нижче, подібний до ОП; але основні зміни:

  1. Кожен випадок замикається в 1 мільярд разів, але об'єкти функції std :: функціонують лише один раз. Я виявив, дивлячись на вихідний код, що "оператор новий" викликається при побудові фактичних викликів std ::: (можливо, не, коли вони оптимізовані).
  2. Тест розділений на два файли, щоб запобігти небажаній оптимізації
  3. Мої випадки: (a) функція вбудована (b) функція передається звичайним покажчиком функції (c) функція є сумісною функцією, оберненою як std :: function (d) функція є несумісною функцією, сумісною з std :: прив’язувати, завернути як std :: function

Отримані результати:

  • випадок (а) (вбудований) 1,3 нсек

  • всі інші випадки: 3,3 нсек.

Справа (d), як правило, трохи повільніше, але різниця (приблизно 0,05 нсек) поглинається шумом.

Висновок полягає в тому, що функція std :: може бути порівняною накладними (під час виклику) з використанням функціонального вказівника, навіть коли є проста адаптація на прив'язку до фактичної функції. Вбудована лінія на 2 нс швидша, ніж інші, але це очікуваний компроміс, оскільки інлайнер - єдиний випадок, який є "жорстким" під час виконання.

Коли я запускаю код Йохана-Лундберга на тій же машині, я бачу близько 39 нс на цикл, але в циклі є набагато більше, включаючи власне конструктор і деструктор функції std ::, який, ймовірно, досить високий оскільки це передбачає нове та видалення.

-O2 gcc 4.8.1 до цілі x86_64 (ядро 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 ++ filt:

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)

1
З клаксом 3.4.1 x64 результати: (a) 1.0, (b) 0.95, (c) 2.0, (d) 5.0.
rustyx

4

Я вважав ваші результати дуже цікавими, тому я трохи копав, щоб зрозуміти, що відбувається. По-перше, як сказали багато інших, маючи результати результатів обчислення стан програми, який компілятор просто оптимізує це. По-друге, маючи постійну 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 і отримав час 330ms для 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 :: може змінитися, тому її не можна векторизувати.

Це змусило мене спробувати щось інше, щоб побачити, чи зможу я змусити компілятора виконати таку ж оптимізацію у версії std :: function. Замість того, щоб переходити у функцію, я роблю std :: функцію як глобальний var, і це називається.

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 :: функція: 2702ms
  • global std :: функція: 330 мс

Тож мій висновок - необмежена швидкість функції std :: функція проти функтора-шаблона майже однакова. Однак це ускладнює роботу оптимізатора.


1
Вся справа в тому, щоб передати функтор як параметр. Ваша calc3справа не має сенсу; calc3 тепер жорстко кодується для виклику f2. Звичайно, це можна оптимізувати.
rustyx

дійсно, саме це я намагався показати. Цей calc3 еквівалентний шаблону, і в цій ситуації фактично є конструкція часу компіляції так само, як шаблон.
Джошуа Ріттерман
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.