Затраты времени выполнения с boost.units?

Я вижу около 10% времени выполнения при использовании клона constexpr улучшенные бустеры с float тип значения с использованием Clang и -O3 оптимизация уровня. Это проявляется в некоторых более сложных приложениях библиотеки, над которыми я работал. Учитывая эту ситуацию, у меня есть два вопроса, которые мне бы очень хотелось решить, и я хотел бы помочь с:

  1. Предполагается, что повышающие модули - это библиотека с нулевыми издержками, так почему я вижу накладные расходы?
  2. Что еще более важно, помимо того, что я не использую 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? Похоже, что это проблема, и если это так, то это прогресс, но я все еще хочу выяснить, что можно сделать, чтобы реально получить нулевые накладные расходы.

0 ответов

Другие вопросы по тегам