1

我有一个 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...
};
4

6 回答 6

4

如果您只是将 C 库调用包装在 C++ 类函数中(换句话说,C++ 函数除了调用 C 函数什么都不做),那么编译器将优化这些调用,从而不会降低性能。

于 2008-11-13T02:48:54.000 回答
3

与任何有关性能的问题一样,系统会告诉您进行测量以获得答案(这是严格正确的答案)。

但根据经验,对于实际上可以内联的简单内联方法,您不会看到性能损失。一般来说,除了将调用传递给另一个函数之外什么都不做的内联方法是内联的理想选择。

但是,即使您的包装器方法没有内联,我怀疑您不会注意到性能损失 - 甚至不是可测量的损失 - 除非在某个关键循环中调用了包装器方法。即使那样,它也可能只有在包装函数本身没有做太多工作的情况下才能被测量。

这种类型的事情是关于最后需要关注的事情。首先要担心使您的代码正确、可维护以及您正在使用适当的算法。

于 2008-11-13T06:41:28.630 回答
2

像往常一样与优化相关的一切,答案是您必须先衡量性能本身,然后才能知道优化是否值得。

  • 对两个不同的函数进行基准测试,一个直接调用 C 风格的函数,另一个通过包装器调用。查看哪个运行得更快,或者差异是否在您测量的误差范围内(这意味着您没有可以测量的差异)。
  • 查看上一步中两个函数生成的汇编代码(在 gcc 上,使用-Sor -save-temps)。看看编译器是否做了一些愚蠢的事情,或者你的包装器是否有任何性能错误。

除非性能差异太大而有利于不使用包装器,否则重新实现不是一个好主意,因为您可能会引入错误(这甚至可能导致看起来正常但错误的结果)。即使差异很大,只要记住 C++ 与 C 非常兼容,并且即使在 C++ 代码中以 C 风格使用您的库,也会更简单且风险更小。

于 2008-11-13T03:02:47.887 回答
2

您的包装器本身将被内联,但是,您对 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++ 实现的单独命名空间,因为您通常不需要直接使用它们。

(注意,内联只是一个编译器提示,它不会强制内联方法。但这很好:如果内部循环的代码大小超过指令缓存,内联很容易损害性能)

是否可以解决通过/返回引用取决于编译器的强度,我见过很多 foo(X * out) 强制堆栈变量,而 X foo() 确实将值保存在寄存器中。

于 2008-11-13T09:26:02.700 回答
1

我认为您不会注意到太多性能差异。假设您的目标平台支持您的所有数据类型,

我正在为 DS 和其他一些 ARM 设备编码,浮点是邪恶的......我不得不将浮点类型定义为 FixedPoint<16,8>

于 2008-11-13T02:47:54.810 回答
1

如果您担心调用函数的开销会拖慢您的速度,为什么不测试内联 C 代码或将其转换为宏呢?

另外,为什么不改进 C 代码的 const 正确性呢? const_cast 确实应该谨慎使用,尤其是在您控制的接口上。

于 2008-11-13T05:43:08.607 回答