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();
};
Код: Выделить всё
struct Foo {
std::function f{};
};
auto foo1 = Foo{[]{ return /* impl like Derived1 */; }};
auto foo2 = Foo{[]{ return /* impl like Derived2 */; }};
Однако , независимо от других плюсов и минусов каждого решения, мне интересно измерить разницу в производительности с помощью эталонного теста.
Я понимаю, что производительность, очевидно, будет варьироваться в зависимости от того, как реализован std::function, а также от компилятора и переданных ему параметров, операционной системы и черт знает чего еще. .
Но, учитывая все эти факторы, я думаю, можно измерить разницу в производительности, если она вообще существует.
Мне следует поясню, что мое намерение состоит в том, чтобы увидеть из первых рук, что действительно разницу между двумя подходами следует считать незначительной, за исключением очень специфических случаев использования, как я понял из связанных вопросов и других источников. Или чтобы доказать, что мое понимание неверно и действительно существует важная разница в производительности.
Моя попытка написать тест находится здесь:
Несколько пояснений по поводу различных его частей:
- Все f выше по-разному изменяют глобальное unsigned int ,
который я возвращаю из main, чтобы убедиться, что тело этих функций нельзя оптимизировать;
Код: Выделить всё
unsigned int RETURN{}; - Я' я изменил Derived1::f/и foo1/
Код: Выделить всё
Derived2::f(по отношению к приведенным выше фрагментам) таким образом, что они изменяют вышеупомянутый глобальный unsigned int:Код: Выделить всё
foo2Тела лямбдаКод: Выделить всё
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/
Код: Выделить всё
foo1Код: Выделить всё
foo2Код: Выделить всё
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 для удобного накопления измерения времени, выполняемого для каждого вызова виртуальной функции/
Код: Выделить всё
false:Код: Выделить всё
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