当使用具有使用 clang 和级别优化的值类型的constexpr
增强 boost.units的克隆时,我看到大约 10% 的运行时开销。这与我一直在研究的库的一些更复杂的应用程序一起出现。鉴于这种情况,我有两个问题我真的很想解决并希望得到帮助:float
-O3
- Boost 单元应该是一个零开销的库,那么为什么我会看到开销呢?
- 更重要的是,除了不使用 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
运行它。没有任何开销的可靠迹象,这似乎消除了我的问题理论。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
吗?问题是他们没有完全优化到与 for 相同的代码float
吗?似乎这就是问题所在,如果是这样的话,那就是进步,但我仍然想弄清楚可以做些什么来真正实现零开销。