我有一个 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 确实应该谨慎使用,尤其是在您控制的接口上。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top