C++中的内联成员运算符和内联运算符之间的区别

3
如果我有两个结构体:
struct A
{
    float x, y;
    inline A operator*(A b) 
    {
        A out;
        out.x = x * b.x;
        out.y = y * b.y;
        return out;
    } 
}

还有一个等价的结构体

struct B
{
    float x, y;
}

inline B operator*(B a, B b) 
{
    B out;
    out.x = a.x * b.x;
    out.y = a.y * b.y;
    return out;
} 

你知道B的operator*和A的operator*在编译时是否会有任何不同,或者运行速度是否会更快或更慢吗?(函数内部实际执行的操作应该是无关紧要的)
我的意思是...将内联运算符声明为成员与不作为成员是否会对实际函数的速度产生任何通用影响?
我有许多不同的结构体目前都遵循内联成员运算符的样式...但我想修改它成为有效的C代码,因此在这样做之前,我想知道是否会对性能/编译产生任何变化。

你应该阅读https://dev59.com/62855IYBdhLWcg3wUCWC,其中展示了比这两种方法更快的技术。 - Ben Voigt
3个回答

11
你的代码写法让我觉得 B::operator* 的运行速度会稍微慢一些。这是因为 A::operator* 的实现方式在底层是这样的:
inline A A::operator*(A* this, A b) 
{ 
    A out;
    out.x = this->x * b.x;
    out.y = this->y * b.y;
    return out;
}

因此,A将其左侧参数的指针传递给函数,而在调用函数之前,B必须复制该参数。两者都必须复制其右侧参数。

如果您使用引用并使其正确使用const,则AB实现相同的代码将更好:

struct A
{
    float x, y;
    inline A operator*(const A& b) const 
    {
        A out;
        out.x = x * b.x;
        out.y = y * b.y;
        return out;
    } 
}

struct B
{
    float x, y;
}

inline B operator*(const B& a, const B& b) 
{
    B out;
    out.x = a.x * b.x;
    out.y = a.y * b.y;
    return out;
}

由于结果是临时的(您没有返回修改后的现有对象),因此仍然需要返回对象而不是引用。


补充

然而,在B中使用const传递引用作为两个参数,由于解引用,它比A更有效吗?

首先,当您拼写出所有代码时,两者都涉及相同的解引用操作。(请记住,访问this的成员意味着进行指针解引用。)

但即使如此,它取决于编译器的智能程度。在这种情况下,假设编译器查看您的结构并决定无法将其放入寄存器中,因为它是两个浮点数,因此将使用指针来访问它们。因此,解引用指针的情况(这是引用实现的内容)是您可以得到的最好的结果。汇编代码将类似于以下内容(这是伪汇编代码):

// Setup for the function. Usually already done by the inlining.
r1 <- this
r2 <- &result
r3 <- &b

// Actual function.
r4 <- r1[0]
r4 <- r4 * r3[0]
r2[0] <- r4
r4 <- r1[4]
r4 <- r4 * r3[4]
r2[4] <- r4

假设采用类似RISC(例如ARM)的架构。x86可能使用更少的步骤,但指令解码器会将其扩展到大致相同的详细级别。关键是它们都是在寄存器中进行固定偏移地址指针解引用,这就是速度最快的地方了。优化器可以尝试更聪明地实现对象跨越多个寄存器,但这种优化器要难得多。 (虽然我有一个小小的怀疑,如果 result 只是一个临时对象而不是保留的对象,则LLVM类型的编译器/优化器可以轻松执行该优化)。

所以,由于您正在使用this,因此具有隐式指针解引用。 但是,如果对象在堆栈上呢?没有帮助;堆栈变量会转换为对堆栈指针(或帧指针,如果使用)进行固定偏移地址解引用。 因此,在最后某个地方,您将解引用指针,除非您的编译器足够聪明,可以将对象分散到多个寄存器中。

可以随意传递-S选项给gcc以获取最终代码的反汇编,以查看在您的情况下实际发生了什么。


谢谢,迈克...我总是忘记使用const或者传引用。 我的错误...然而,在B中,如果两个参数都使用了const传引用,会不会因为解引用而使其比A更有效率呢? - Serge
1
@Stefan:不,因为两者都涉及解引用(一个解引用this指针,另一个解引用显式引用参数,但成本是相同的)。 - Ben Voigt
如果编译器可以证明传递的引用参数仅为加载参数,则可以将指针/引用优化为传值方式。 - dirkgently

4

你真的应该让编译器来处理内联。

话虽如此,类定义中定义的函数(如A)默认情况下是inline的。对于A::operator *inline说明符是无用的。

更有趣的情况是,当您将成员函数定义放在类定义之外时。在这种情况下,如果您想向编译器提供提示(它可以随意忽略),表明这是常用的,并且指令应在调用者内联编译,则需要使用内联。

请阅读C++ FAQ 9


还要记住编译器有时会无视 inline,这可能会让你在链接时出现多次定义符号错误。因此,如果编译器这样做了,你可能还想将非成员函数声明为 static,以避免这种情况发生。我之所以说“令人恼火”,是因为这会导致我遇到过一个问题,即 #define 可以强制内联,而 inline 函数却不能,结果使用 inline 的代码实际上更大,需要更多的堆栈。没有人会考虑嵌入式设备的情况。 - Mike DeSimone
请注意,我说的是“将inline非成员函数声明为static”。不幸的是,static关键字在这两个上下文中有着截然不同的含义。我的观点仅仅是为了避免因为编译器决定不内联一个非成员函数而导致的多次定义符号错误。此外,我在实践中也遇到过这个问题,所以并不是所有的编译器都符合您所描述的标准。 - Mike DeSimone

2

这是我会编写的结构体:

struct A
{
    float x, y;
    A(float ax, float ay) : x(ax), y(ay) { }
    A operator*(const A& b) const { return b(x * b.x, y * b.y); } 
}

回答这个问题,是的,在某些情况下将运算符作为成员函数编写可能会稍微快一点,但不足以在您的代码中产生明显的差异。
一些注意事项:
  1. 不要担心使用inline关键字。优化编译器会自行决定何时进行内联。
  2. 使用初始化构造函数。这样做可以提高代码可读性。它们还可以带来小的性能优势,让人更加放心。
  3. 尽可能经常通过const引用传递结构体。
  4. 注重编写具有良好风格的代码而不是快速代码。大多数代码都足够快,如果不够快,则可能是算法或IO处理中的某些愚蠢问题导致的。

1
你能否解释一下什么情况会导致潜在的(微小)速度增加吗? 如果不能,没关系。 - Serge
在某些情况下,一些编译器会更喜欢通过寄存器传递“this”指针,但这只意味着函数调用附近的其他汇编代码会稍微慢一些。所以:不要担心它。 - cdiggins

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接