Как измерить разницу в производительности между виртуальной функцией + наследованием и элементом std::function без наслеC++

Программы на C++. Форум разработчиков
Ответить
Anonymous
 Как измерить разницу в производительности между виртуальной функцией + наследованием и элементом std::function без насле

Сообщение Anonymous »

tl;dr

Является ли тест, который я представляю ниже, справедливым способом сравнения подходов к полиморфизму, основанных на наследовании и подходах, основанных на std::function?

Полный вопрос

Если нужны разные объекты, реализующие один и тот же интерфейс по-разному, а также необходимо иметь возможность поместить их в контейнер и поменять местами один с другое во время выполнения, наиболее популярное решение — использовать наследование:

Код: Выделить всё

struct Base {
virtual void f() = 0;
virtual ~Base() = default;
};
struct Derived1 : Base {
virtual void f();
};
struct Derived2 : Base {
virtual void f();
};
Другое решение — создать один класс, но заменить виртуальный метод на std::function:

Код: Выделить всё

struct Foo {
std::function f{};
};
auto foo1 = Foo{[]{ return /* impl like Derived1 */; }};
auto foo2 = Foo{[]{ return /* impl like Derived2 */; }};
(Некоторые вопросы о разнице между двумя подходами можно найти здесь, здесь и здесь.)

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

Моя попытка написать тест находится здесь:
Несколько пояснений по поводу различных его частей:
  • Все f выше по-разному изменяют глобальное unsigned int ,

    Код: Выделить всё

    unsigned int RETURN{};
    
    который я возвращаю из main, чтобы убедиться, что тело этих функций нельзя оптимизировать;
  • Я' я изменил Derived1::f/

    Код: Выделить всё

    Derived2::f
    и foo1/

    Код: Выделить всё

    foo2Тела лямбда 
    (по отношению к приведенным выше фрагментам) таким образом, что они изменяют вышеупомянутый глобальный unsigned int:

    Код: Выделить всё

    struct Base {
    virtual void f() = 0;
    virtual ~Base() = default;
    };
    struct Derived1 : Base {
    virtual void f() { RETURN += 1; }
    };
    struct Derived2 : Base {
    virtual void f() { RETURN += 2; }
    };
    
    struct Foo {
    std::function f{};
    };
    auto const foo1 = Foo{[]{ RETURN += 1; }};
    auto const foo2 = Foo{[]{ RETURN += 2; }};
    
  • Перед кодом измерения я генерирую случайные логические значения, которые использую для случайного выбора между Derived1/ и Derived2/

    Код: Выделить всё

    std::random_device rd;
    std::mt19937 gen{rd()};
    std::bernoulli_distribution randBool{0.5};
    constexpr int N = 1000000;
    
    std::array bools;
    for (bool& b : bools) {
    b = randBool(gen);
    }
    
  • Я использую Boost.Hana для удобного цикла по кортежу из 2 true во время компиляции/, которые позволяют параметризовать два случая, и Range-v3 для удобного накопления измерения времени, выполняемого для каждого вызова виртуальной функции/

    Код: Выделить всё

    std::function
    :

    Код: Выделить всё

    using Time = duration;
    
    std::array times; // 0: std::function-based, 1: inheritance-based
    
    hana::for_each(hana::make_basic_tuple(hana::false_c, hana::true_c), [&](auto hb) {
    constexpr bool B = hb;
    auto const elapsed = ranges::accumulate(bools, Time{}, [](auto acc, auto b){
    /* time measurement */;
    });
    times[!B] = elapsed;
    });
    
  • Функция, которую нужно выбрать на основе этого bool-компонента времени выполненияean, — это следующая функция, которая также шаблонизирована на основе bool времени компиляцииean B, используемый для выбора между двумя сравниваемыми случаями:

    Код: Выделить всё

    template
    constexpr auto bool2Obj = []{
    if constexpr (B) {
    return [](bool b){
    return b
    ? foo1
    : foo2;
    };
    } else {
    using BasePtr = std::unique_ptr;
    return [](bool b){
    return b
    ? BasePtr{std::make_unique()}
    : BasePtr{std::make_unique()};
    };
    }
    }();
    
  • После того, как объект выбран, его метод вызывается через следующий шаблон, созданный по шаблону bool B по той же причине, что и выше: т. е. разрешить выбор каждого из двух сравниваемых случаев:

    Код: Выделить всё

    template
    constexpr auto call = []{
    if constexpr (B) {
    return [](Foo const& p){ p.f(); };
    } else {
    return [](std::unique_ptr const& p){ p->f(); };
    }
    }();
    
  • /* измерение времени */ следующее:

    Код: Выделить всё

    auto obj = bool2Obj(b);
    auto const start = high_resolution_clock::now();
    call(obj);
    auto const end = high_resolution_clock::now() - start;
    return acc + Time{end};
    
    где я исключил случайный выбор объекта из измерения, оставив в измерении только вызов.
Результат:
  • с несколькими повторениями, которые я могу запустить до того, как процессы будут завершены по тайм-ауту, CompilerExplorer, кажется, сообщает мне, что два подхода имеют примерно одинаковую производительность, что и процент, который я печатаю, который равен (i - f) / i (где i и f — время выполнения i >подходы, основанные на наследовании ифункции), изменения часто поддаются знаку; это относится как к Clang, так и к GCC
  • однако, выполняя программу на моей машине, хотя GCC, кажется, ведет себя одинаково, Clang (18.1.8) постоянно возвращает положительные результаты, подобные следующим: предполагая, что подход на основе std::function работает быстрее:

    Код: Выделить всё

    0.0648057
    0.0716398
    0.0636759
    0.0649676
    0.0673908
    0.0756509
    0.0780861
    0.0890416
    0.090532
    0.094767
    
  • с третьей стороны, QuickBench (для которого мне пришлось отказаться от Boost и Range-v3), похоже, постоянно поддерживает, что std::function< Подход на основе /code> работает быстрее для GCC, Clang + LLVM, Clang + GNU.


Подробнее здесь: https://stackoverflow.com/questions/790 ... ritance-an
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «C++»