2

当使用具有使用 clang 和级别优化的值类型的constexpr增强 boost.units的克隆时,我看到大约 10% 的运行时开销。这与我一直在研究的库的一些更复杂的应用程序一起出现。鉴于这种情况,我有两个问题我真的很想解决并希望得到帮助:float-O3

  1. Boost 单元应该是一个零开销的库,那么为什么我会看到开销呢?
  2. 更重要的是,除了不使用 boost.units 之外,我怎样才能让开销消失?

细节...

我一直在研究用 C++14 编写的交互式物理引擎。由于它使用了许多不同的物理量和单位,我喜欢使用 boost.units 提供的编译时强制单位和量。不幸的是,启用升压单元似乎伴随着这种运行时成本。该引擎附带一个基准应用程序,该应用程序使用谷歌的基准库来提供这种洞察力,并且需要一些更复杂的模拟才能看到开销。

目前,由于开销的原因,引擎默认构建时不使用升压单元。通过定义正确的预处理器宏名称,可以使用增强单元构建引擎。我使用如下代码实现了这种切换:

// #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宏所做的事情让我觉得有点怀疑,因为它采用了增强单元类型并将其转化为值。然而,这使得在使用或不使用 boost 单元之间切换更容易,因为无论哪种方式的表达式都可以在3.0f * Second没有警告的情况下编译。检查 clang 和 gcc 对这些表达式的作用似乎证实它们足够聪明,可以避免运行时乘法3.0f * 1.0f,并且只是将表达式识别为3.0f. 无论如何,我想知道这是否是开销的原因,或者是否是我所做的其他事情。

我还想知道问题是否源于constexpr我正在使用的增强代码,或者该代码的作者是否对这种开销有任何想法。在互联网上搜索时,我发现提到了普通升压单元库的开销,因此似乎可以安全地假设增强单元没有故障。我的询问中提出的一个建议(感谢 GitHub 用户 muggenhor)如下:

我预计这可能是由编译器完成的内联量引起的。由于操作符的包装函数,这至少添加了一个函数调用,每个操作需要内联。对于取决于子表达式结果的表达式,这需要首先内联子表达式。因此,我希望内联传递的最小数量能够正确优化您的代码,使其等于生成的表达式树的深度......

对我来说,这听起来像是一个非常可行的理论。不幸的是,我不知道如何测试它,而且我承认我现在更喜欢挖掘自己的代码而不是 clang/LLVM 代码。我试过使用-inline-threshold=10000,但这似乎并没有让开销消失。至少就我对 clang 的理解而言,我不认为这会特别增加内联传递的数量。还有另一个命令行参数吗?或者在clang的源代码中是否有参数可以让我将其作为重新编译clang并尝试修改后的编译器的起点?

我的另一个理论是使用是否float是问题所在。我可以重建我的物理引擎来double代替使用,并比较启用和不启用增强单元支持的构建之间的基准测试结果。我在使用时发现double开销至少似乎减少了。我想知道double即使我float在它的quantity模板中使用提升单元是否也在使用,这可能会导致开销。

最后,我performance使用增强功能构建了 boost 单元的示例,并使用和constexpr运行它。没有任何开销的可靠迹象,这似乎消除了我的问题理论。doublefloatfloat

更新数据和代码

在这方面得到了一些更孤立的数据和代码,我似乎看到了超过 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吗?问题是他们没有完全优化到与 for 相同的代码float吗?似乎这就是问题所在,如果是这样的话,那就是进步,但我仍然想弄清楚可以做些什么来真正实现零开销。

4

0 回答 0