Затраты времени выполнения с boost.units?
Я вижу около 10% времени выполнения при использовании клона constexpr
улучшенные бустеры с float
тип значения с использованием Clang и -O3
оптимизация уровня. Это проявляется в некоторых более сложных приложениях библиотеки, над которыми я работал. Учитывая эту ситуацию, у меня есть два вопроса, которые мне бы очень хотелось решить, и я хотел бы помочь с:
- Предполагается, что повышающие модули - это библиотека с нулевыми издержками, так почему я вижу накладные расходы?
- Что еще более важно, помимо того, что я не использую boost.units, как я могу избавиться от накладных расходов?
Подробности...
Я работал над интерактивным физическим движком, написанным на C++14. Имея множество различных физических величин и единиц, которые он использует, я люблю использовать принудительные единицы времени и количества, которые предоставляет boost.units. К сожалению, включение бустеров, похоже, идет с такими затратами времени выполнения. Движок поставляется с приложением-бенчмарком, которое использует библиотеку бенчмарков от Google для предоставления этой информации, и требуются некоторые из более сложных симуляций, чтобы увидеть накладные расходы.
В настоящее время из-за накладных расходов двигатель по умолчанию собирается без использования блоков наддува. Определяя правильное имя макроса препроцессора, двигатель может быть собран с буст-блоками. Я добился этого переключения, используя следующий код:
// #define USE_BOOST_UNITS
#if defined(USE_BOOST_UNITS)
...
#include <boost/units/systems/si/time.hpp>
...
#endif // defined(USE_BOOST_UNITS)
#if defined(USE_BOOST_UNITS)
#define QUANTITY(BoostDimension) boost::units::quantity<BoostDimension, float>
#define UNIT(Quantity, BoostUnit) Quantity{BoostUnit * float{1}}
#define DERIVED_UNIT(Quantity, BoostUnit, Ratio) Quantity{BoostUnit * float{Ratio}}
#else // defined(USE_BOOST_UNITS)
#define QUANTITY(BoostDimension) float
#define UNIT(Quantity, BoostUnit) float{1}
#define DERIVED_UNIT(Quantity, BoostUnit, Ratio) float{Ratio}}
#endif // defined(USE_BOOST_UNITS)
using Time = QUANTITY(boost::units::si::time);
constexpr auto Second = UNIT(Time, boost::units::si::second);
Что я сделал с UNIT
макрос кажется мне немного подозрительным в том смысле, что он принимает тип блока повышения и превращает его в значение. Это делает переключение между использованием или неиспользованием бустеров проще, так как выражения в любом случае 3.0f * Second
компилировать без предупреждения. Проверка того, что clang и gcc делают с такими выражениями, показала, что они достаточно умны, чтобы избежать умножения во время выполнения. 3.0f * 1.0f
и просто узнал выражение как 3.0f
, В любом случае, мне интересно, является ли это причиной накладных расходов или это что-то еще, что я сделал.
Я также задавался вопросом, может быть, проблема кроется в constexpr
код расширения, который я использую, или если автор (ы) этого кода имел представление об этих затратах. В поиске в интернете я обнаружил упоминание о накладных расходах с библиотекой обычных бустеров, так что, кажется, можно с уверенностью предположить, что улучшенные блоки не виноваты. Тем не менее, предложение, которое пришло из моего запроса (и моя благодарность за него пользователю muggenhor из GitHub), было следующим:
Я ожидаю, что это, вероятно, вызвано количеством вставок, сделанных компилятором. Из-за функций-оболочек для операторов это добавляет по крайней мере один вызов функции, который должен быть встроен для каждой операции. Для выражений, зависящих от результата подвыражений, это требует, чтобы подвыражения были вставлены в первую очередь. В результате я ожидаю, что минимальное количество проходов для встраивания сможет правильно оптимизировать ваш код, чтобы он был равен глубине созданного дерева выражений...
Это звучит как довольно жизнеспособная теория для меня. К сожалению, я не знаю, как это проверить, и, по общему признанию, в настоящее время мне больше нравится копаться в моем собственном коде, чем в коде clang/LLVM. Я пытался использовать -inline-threshold=10000
но это, кажется, не заставляет накладные расходы уходить. По крайней мере, насколько я понимаю, что такое лязг, я не верю, что это увеличивает количество проходов. Есть ли другой аргумент командной строки, который делает? Или в исходниках clang есть параметры, на которые кто-то может указать, что я могу начать с того, что, возможно, перекомпилирую clang и попробует модифицированный компилятор?
Еще одна теория, которая у меня была, это использование float
это проблема. Я могу перестроить свой физический движок, чтобы использовать double
Вместо этого сравните результаты тестов между построением с включенной поддержкой бустеров и без нее. Что я нахожу при использовании double
в том, что накладные расходы, по крайней мере, уменьшаются. Я задавался вопросом, может быть, где-то используются бустеры? double
даже когда я использую float
в его quantity
шаблон и, возможно, это вызывает накладные расходы.
Наконец, я построил буст performance
пример с constexpr
улучшения и запустили его с обоими double
а также float
, У меня нет достоверных признаков каких-либо накладных расходов, которые, кажется, устраняют мою теорию float
быть проблемой.
Обновление с данными и кодом
У меня есть немного более изолированных данных и кода, и кажется, что я вижу, что накладные расходы значительно превышают 10%...
Некоторые контрольные данные, где Length
в основном boost::units::si::length
:
LesserLength/1000 953 ns 953 ns 724870
LesserFloat/1000 590 ns 590 ns 1093647
LesserDouble/1000 619 ns 618 ns 1198938
Как выглядит соответствующий код:
static void LesserLength(benchmark::State& state)
{
const auto vals = RandPairs(static_cast<unsigned>(state.range()),
-100.0f * playrho::Meter, 100.0f * playrho::Meter);
auto c = 0.0f * playrho::Meter;
for (auto _: state)
{
for (const auto& val: vals)
{
const auto a = std::get<0>(val);
const auto b = std::get<1>(val);
static_assert(std::is_same<decltype(b), const playrho::Length>::value, "not Length");
const auto v = (a < b)? a: b;
benchmark::DoNotOptimize(c = v);
}
}
}
static void LesserFloat(benchmark::State& state)
{
const auto vals = RandPairs(static_cast<unsigned>(state.range()),
-100.0f, 100.0f);
auto c = 0.0f;
for (auto _: state)
{
for (const auto& val: vals)
{
const auto a = std::get<0>(val);
const auto b = std::get<1>(val);
const auto v = (a < b)? a: b;
static_assert(std::is_same<decltype(v), const float>::value, "not float");
benchmark::DoNotOptimize(c = v);
}
}
}
static void LesserDouble(benchmark::State& state)
{
const auto vals = RandPairs(static_cast<unsigned>(state.range()),
-100.0, 100.0);
auto c = 0.0;
for (auto _: state)
{
for (const auto& val: vals)
{
const auto a = std::get<0>(val);
const auto b = std::get<1>(val);
const auto v = (a < b)? a: b;
static_assert(std::is_same<decltype(v), const double>::value, "not double");
benchmark::DoNotOptimize(c = v);
}
}
}
Получив подсказку, я проверил Godbolt с помощью следующего кода, чтобы увидеть, что сгенерирует clang 5.0.0 и gcc 7.2:
#include <algorithm>
#include <boost/units/systems/si/length.hpp>
#include <boost/units/cmath.hpp>
using length = boost::units::quantity<boost::units::si::length, float>;
float f(float a, float b)
{
return a < b? a: b;
}
length f(length a, length b)
{
return a < b? a: b;
}
Я вижу, что сгенерированная сборка выглядит совершенно по-разному между двумя функциями и между clang и gcc. Вот суть соответствующей сборки от Clang (с надписью "Boost" здесь просто показано как length
):
f(float, float): # @f(float, float)
minss xmm0, xmm1
ret
f(length, length)
movss xmm0, dword ptr [rdx] # xmm0 = mem[0],zero,zero,zero
ucomiss xmm0, dword ptr [rsi]
cmova rdx, rsi
mov eax, dword ptr [rdx]
mov dword ptr [rdi], eax
mov rax, rdi
ret
Не должны ли оба эти компилятора использовать -O3
оптимизация будет возвращать ту же сборку, хотя для length
версия, как они делают для float
версия? Проблема в том, что они не совсем оптимизируют весь тот же код, что и для float
? Похоже, что это проблема, и если это так, то это прогресс, но я все еще хочу выяснить, что можно сделать, чтобы реально получить нулевые накладные расходы.