Является ли тест, который я представляю ниже, справедливым способом сравнения подходов к полиморфизму, основанных на наследовании и подходах, основанных на 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 ,
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 для удобного накопления измерения времени, выполняемого для каждого вызова виртуальной функции/
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 по той же причине, что и выше: т. е. разрешить выбор каждого из двух сравниваемых случаев:
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 работает быстрее:
с третьей стороны, QuickBench (для которого мне пришлось отказаться от Boost и Range-v3), похоже, постоянно поддерживает, что std::function< Подход на основе /code> работает быстрее для GCC, Clang + LLVM, Clang + GNU.
[h4]tl;dr[/h4] Является ли тест, который я представляю ниже, справедливым способом сравнения подходов к полиморфизму, основанных на наследовании и подходах, основанных на std::function? [h4]Полный вопрос[/h4] Если нужны разные объекты, реализующие один и тот же интерфейс по-разному, а также необходимо иметь возможность поместить их в контейнер и поменять местами один с другое во время выполнения, наиболее популярное решение — использовать наследование: [code]struct Base { virtual void f() = 0; virtual ~Base() = default; }; struct Derived1 : Base { virtual void f(); }; struct Derived2 : Base { virtual void f(); }; [/code] Другое решение — создать один класс, но заменить виртуальный метод на std::function: [code]struct Foo { std::function f{}; }; auto foo1 = Foo{[]{ return /* impl like Derived1 */; }}; auto foo2 = Foo{[]{ return /* impl like Derived2 */; }}; [/code] (Некоторые вопросы о разнице между двумя подходами можно найти здесь, здесь и здесь.)
Однако , [b]независимо от других плюсов и минусов каждого решения[/b], мне интересно измерить разницу в производительности с помощью эталонного теста. Я понимаю, что производительность, очевидно, будет варьироваться в зависимости от того, как реализован std::function, а также от компилятора и переданных ему параметров, операционной системы и черт знает чего еще. . Но, учитывая все эти факторы, я думаю, можно измерить разницу в производительности, если она вообще существует. Мне следует поясню, что мое намерение состоит в том, чтобы увидеть из первых рук, что действительно разницу между двумя подходами следует считать незначительной, за исключением очень специфических случаев использования, как я понял из связанных вопросов и других источников. Или чтобы доказать, что мое понимание неверно и действительно существует важная разница в производительности.
Моя попытка написать тест находится здесь: Несколько пояснений по поводу различных его частей: [list] [*]Все f выше по-разному изменяют глобальное unsigned int , [code]unsigned int RETURN{}; [/code] который я возвращаю из main, чтобы убедиться, что тело этих функций нельзя оптимизировать; [*]Я' я изменил Derived1::f/[code]Derived2::f[/code] и foo1/[code]foo2Тела лямбда [/code] (по отношению к приведенным выше фрагментам) таким образом, что они изменяют вышеупомянутый глобальный unsigned int: [code]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; } };
[*]Перед кодом измерения я генерирую случайные логические значения, которые использую для случайного выбора между Derived1/[code]foo1[/code] и Derived2/[code]foo2[/code] [code]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); } [/code]
[*]Я использую Boost.Hana для удобного цикла по кортежу из 2 true во время компиляции/[code]false[/code], которые позволяют параметризовать два случая, и Range-v3 для удобного накопления измерения времени, выполняемого для каждого вызова виртуальной функции/[code]std::function[/code]: [code]using Time = duration;
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; }); [/code]
[*]Функция, которую нужно выбрать на основе этого bool-компонента времени выполненияean, — это следующая функция, которая также шаблонизирована на основе bool времени компиляцииean B, используемый для выбора между двумя сравниваемыми случаями: [code]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()}; }; } }(); [/code]
[*]После того, как объект выбран, его метод вызывается через следующий шаблон, созданный по шаблону bool B по той же причине, что и выше: т. е. разрешить выбор каждого из двух сравниваемых случаев: [code]template constexpr auto call = []{ if constexpr (B) { return [](Foo const& p){ p.f(); }; } else { return [](std::unique_ptr const& p){ p->f(); }; } }(); [/code]
[*]/* измерение времени */ следующее: [code]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}; [/code] где я исключил случайный выбор объекта из измерения, оставив в измерении только вызов. [/list]Результат: [list] [*]с несколькими повторениями, которые я могу запустить до того, как процессы будут завершены по тайм-ауту, CompilerExplorer, кажется, сообщает мне, что два подхода имеют примерно одинаковую производительность, что и процент, который я печатаю, который равен (i - f) / i (где i и f — время выполнения i >подходы, основанные на наследовании ифункции), изменения часто поддаются знаку; это относится как к Clang, так и к GCC [*]однако, выполняя программу на моей машине, хотя GCC, кажется, ведет себя одинаково, Clang (18.1.8) постоянно возвращает положительные результаты, подобные следующим: предполагая, что подход на основе std::function работает быстрее: [code]0.0648057 0.0716398 0.0636759 0.0649676 0.0673908 0.0756509 0.0780861 0.0890416 0.090532 0.094767 [/code]
[*]с третьей стороны, QuickBench (для которого мне пришлось отказаться от Boost и Range-v3), похоже, постоянно поддерживает, что std::function< Подход на основе /code> работает быстрее для GCC, Clang + LLVM, Clang + GNU. [/list]