如何以最小的性能损失同步 C 和 C++ 库?
-
08-07-2019 - |
题
我有一个 C 库,其中包含许多用于处理向量、矩阵、四元数等的数学例程。它需要保留在 C 语言中,因为我经常将它用于嵌入式工作并作为 Lua 扩展。此外,我还有 C++ 类包装器,可以使用 C API 为数学运算提供更方便的对象管理和运算符重载。包装器仅包含一个头文件,并尽可能多地使用内联。
与将实现直接移植并内联到 C++ 类中相比,包装 C 代码是否有明显的损失?该库用于时间关键的应用程序。那么,消除间接带来的提升是否可以弥补两个端口的维护难题?
C接口示例:
typedef float VECTOR3[3];
void v3_add(VECTOR3 *out, VECTOR3 lhs, VECTOR3 rhs);
C++ 包装器示例:
class Vector3
{
private:
VECTOR3 v_;
public:
// copy constructors, etc...
Vector3& operator+=(const Vector3& rhs)
{
v3_add(&this->v_, this->v_, const_cast<VECTOR3> (rhs.v_));
return *this;
}
Vector3 operator+(const Vector3& rhs) const
{
Vector3 tmp(*this);
tmp += rhs;
return tmp;
}
// more methods...
};
解决方案
您的包装器本身将被内联,但是,您对C库的方法调用通常不会。 (这需要在技术上可行的链接时优化,但在当今的工具中最好是AFAIK最基本的)
通常,这样的函数调用并不是非常昂贵。循环成本在过去几年中大幅下降,并且可以很容易地预测,因此呼叫惩罚可以忽略不计。
但是,内联打开了更多优化的大门:如果你有v = a + b + c,你的包装类会强制生成堆栈变量,而对于内联调用,大多数数据都可以保存在FPU中堆。此外,内联代码允许简化指令,考虑常量值等。
因此,虽然衡量投资规则之前的规则是正确的,但我希望这里有一些改进空间。
典型的解决方案是将C实现带入一种格式,使其既可以用作内联函数,也可以用作“C”形式。体:
// V3impl.inl
void V3DECL v3_add(VECTOR3 *out, VECTOR3 lhs, VECTOR3 rhs)
{
// here you maintain the actual implementations
// ...
}
// C header
#define V3DECL
void V3DECL v3_add(VECTOR3 *out, VECTOR3 lhs, VECTOR3 rhs);
// C body
#include "V3impl.inl"
// CPP Header
#define V3DECL inline
namespace v3core {
#include "V3impl.inl"
} // namespace
class Vector3D { ... }
这可能仅适用于具有相对简单实体的选定方法。我将这些方法移动到C ++实现的单独命名空间,因为您通常不需要直接使用它们。
(请注意,内联只是一个编译器提示,它不会强制该方法被内联。 但这很好:如果内部循环的代码大小超过指令缓存,内联很容易损害性能)
是否可以解析pass / return-by-reference取决于编译器的强度,我在很多地方看到过 foo(X * out) 强制堆栈变量,而 X foo() 将值保存在寄存器中。
其他提示
如果您只是在C ++类函数中包装C库调用(换句话说,C ++函数除了调用C函数之外什么也不做),那么编译器将优化这些调用,这样就不会降低性能。
与任何有关表现的问题一样,您会被告知要测量以获得答案(这是严格正确的答案)。
但是根据经验,对于实际上可以内联的简单内联方法,您将看不到性能损失。一般来说,内联方法除了将调用传递给另一个函数之外什么都不做,这是内联的一个很好的选择。
但是,即使您的包装器方法没有内联,我怀疑您没有注意到性能损失 - 甚至不是可测量的 - 除非在某个关键循环中调用包装器方法。即使这样,如果包装函数本身没有做太多工作,它也可能只是可测量的。
这类事情是关注的最后一件事。首先担心使代码正确,可维护,并且您正在使用适当的算法。
与所有与优化相关的事情一样,答案是您必须先衡量性能本身,然后才能知道优化是否值得。
- 对两个不同的函数进行基准测试,一个直接调用 C 风格函数,另一个通过包装器调用。查看哪一个运行得更快,或者差异是否在测量的误差范围内(这意味着您无法测量差异)。
- 看一下上一步中两个函数生成的汇编代码(在gcc上,使用
-S
或者-save-temps
)。看看编译器是否做了一些愚蠢的事情,或者你的包装器是否有任何性能错误。
除非性能差异太大而不利于不使用包装器,否则重新实现不是一个好主意,因为您可能会引入错误(这甚至可能导致看起来正常但实际上是错误的结果)。即使差异很大,记住 C++ 与 C 非常兼容并且即使在 C++ 代码中也以 C 风格使用您的库会更简单且风险更小。
我认为你不会注意到很多差异。假设您的目标平台支持所有数据类型,
我正在编写DS和其他一些ARM设备并且浮点数是邪恶的......我不得不将typedef浮动到FixedPoint&lt; 16,8&gt;
如果您担心调用函数的开销会减慢您的速度,为什么不测试内联 C 代码或将其转换为宏呢?
另外,为什么不在 C 代码中提高 const 的正确性 - const_cast 确实应该谨慎使用,尤其是在您控制的接口上。