代码之家  ›  专栏  ›  技术社区  ›  Judge Maygarden

如何将C&C++库与最小性能惩罚同步?

  •  1
  • Judge Maygarden  · 技术社区  · 17 年前

    我有一个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...
    };
    
    6 回复  |  直到 17 年前
        1
  •  4
  •   Bill the Lizard    17 年前

    如果你只是包装C库调用在C++类函数(换句话说,C++函数什么也不做,但调用C函数),那么编译器将优化这些调用,从而不是一个性能损失。

        2
  •  3
  •   Michael Burr    17 年前

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

    但根据经验,对于实际上可以内联的简单内联方法,您不会看到性能损失。通常,只将调用传递给另一个函数的内联方法是内联的最佳候选方法。

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

    这类事情是最不值得关注的。首先要考虑的是如何使代码正确、可维护,以及是否使用了适当的算法。

        3
  •  2
  •   peterchen    17 年前

    一般来说,函数调用本身并不昂贵。在过去的几年中,周期成本已经大大降低,并且可以很容易地预测,因此呼叫惩罚本身可以忽略不计。

    但是,内联打开了更多优化的大门:如果您有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() 在寄存器中保留值。

        4
  •  2
  •   CesarB    17 年前

    与往常一样,与优化相关的一切问题的答案是,在你知道优化是否值得之前,你必须衡量性能本身。

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

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

        5
  •  1
  •   CVertex    17 年前

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

    我正在为DS和其他一些ARM设备编码,浮点是邪恶的……我必须将def float键入FixedPoint<16,8>

        6
  •  1
  •   Greg Rogers    17 年前

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

    另外,为什么不在您使用C代码时提高C代码的常量正确性呢?常量转换应该少用,尤其是在您控制的接口上。