C 操作员过载样式,这更适合优化

C++ Operator overloading styles, which is better for optimisation?

本文关键字:优化 操作员 样式      更新时间:2023-10-16

有两种方法可以在C 中超载二进制运算符:作为成员函数或作为非成员函数。

由于成员函数样式可以包含在.h文件中,并且非成员样式将在.cpp文件中,因此我认为成员函数样式将使编译器更容易优化。

  • 成员和非成员运营商之间的性能有差异吗?
  • 会员和非成员运营商是否会产生相同的代码(在任何编译器中)?

声明为成员是否与优化无关。编译器通常足够聪明,可以在这两种情况下都会产生同样优化的代码。而且,无论如何,对于精确编译器,版本和配置而言,这是有意义的,并且可以更改另一个。

对于可读性和可固定性问题,操作员应为成员...除非不能。如果可以的话,它允许在与OOP相干的类定义中声明它:它在该类的对象上运行,让我们尝试使其成为该类的方法。

只是在某些情况下根本不可能。例如,输出流注射器(<<运算符)可以接受任何类的第二操作数,因此不能将其定义为成员。这就是C 允许运算符的非成员定义的原因。

如果您打算声明超载二进制运算符的成员和非成员版本,那么它们应该大多是等效的:

struct A {
    A operator+(const A& other) const;     // 1
};
A operator+(const A& x, const A& y) const; // 2

一个很小的差异可能是某些编译器可能会对成员版本使用更有效的此通话式约定(前提是它们通过this指针通过寄存器而不是将其推在堆栈上,并且他们不利用该寄存器默认情况下,快速计数呼叫约定)。

二进制操作员的另一个可能的签名是按值进行参数:

A operator+(A x, A y) const;

但是,此声明仅适用于非会员版本。会员版本的最接近类似物是:

struct A {
    A operator+(A other) const;
};

等效于

A operator+(const A& x, A y) const;

现在,考虑到通过值或(const)引用之间的传递不是您可以忽略的。如果您认为通过参考可以更有效地进行参考,那么您会误会。带有微不足道的构造函数/破坏者的小型类更好地按价值传递。在某些情况下,即使逐个值逐一通过较大的类也可能导致更好的优化,因为它允许编译器忘记 Aliasing

在进行一些研究之后,我可以说答案是肯定的,选择成员函数(在.h文件中使用代码)或非成员函数(在.cpp文件中使用代码)对编译器优化代码的能力有一定影响。

这是一个简单的示例。我在这里有两个操作员,一个是成员函数,并在.h文件中定义。另一个是一个非成员函数,并在.cpp文件中定义。

首先,班级的.h文件:

// test_class.h
// ------------
class Vector3D
{
public:
    Vector3D(double xx, double yy, double zz)
    : x(xx),
      y(yy),
      z(z)
    {}
    Vector3D operator +(Vector3D v)
    {
        return Vector3D(x+v.x, y+v.y, z+v.z);
    }
    friend Vector3D operator -(Vector3D v1, Vector3D v2);
    double  x,y,z;
};

现在班级的.cpp文件

// test_class.cpp
#include "test_class.h"
Vector3D operator -(Vector3D v1, Vector3D v2)
{
    return Vector3D(v1.x-v2.x, v1.y-v2.y, v1.y-v2.y);
}

最后,主CPP文件:

// ConsoleApplication1.cpp : Defines the entry point for the console application.
//
#include "test_class.h"
#include <iostream>
using namespace std;
int main(int argc, char* argv[])
{
    Vector3D a(1,2,3);
    Vector3D b(4,5,6);
    Vector3D c(0,0,0);
    Vector3D d(0,0,0);
    c = a+b;
    cout << c.x << c.y << c.z << endl;
    d = a-b;
    cout << d.x << d.y << d.z << endl;
    return c.x + d.x;
}

查看汇编器输出,我可以看到成员函数产生的说明少于成员函数:

; 20   :     c = a+b;
; 21   :     cout << c.x << c.y << c.z << endl;
push    DWORD PTR __imp_?endl@std@@YAAAV?$basic_ostream@....
movsd   xmm0, QWORD PTR $T2[ebp+16]
movsd   xmm1, QWORD PTR __real@4014000000000000
mov ecx, DWORD PTR __imp_?cout@std@@3V?$basic_ostream@....
sub esp, 24                 ; 00000018H
movsd   QWORD PTR [esp+16], xmm0
movsd   xmm0, QWORD PTR __real@401c000000000000
movsd   QWORD PTR [esp+8], xmm0
movsd   QWORD PTR [esp], xmm1
call    DWORD PTR __imp_??6?$basic_ostream@....
mov ecx, eax
call    DWORD PTR __imp_??6?$basic_ostream@....
mov ecx, eax
call    DWORD PTR __imp_??6?$basic_ostream@....
mov ecx, eax
call    DWORD PTR __imp_??6?$basic_ostream@....
; 22   : 
; 23   :     d = a-b;
movsd   xmm0, QWORD PTR __real@4010000000000000
sub esp, 24                 ; 00000018H
mov eax, esp
sub esp, 24                 ; 00000018H
movq    QWORD PTR [eax], xmm0
movsd   xmm0, QWORD PTR __real@4014000000000000
movq    QWORD PTR [eax+8], xmm0
movq    xmm0, QWORD PTR _b$[ebp+16]
movq    QWORD PTR [eax+16], xmm0
movsd   xmm0, QWORD PTR __real@3ff0000000000000
mov eax, esp
movq    QWORD PTR [eax], xmm0
movsd   xmm0, QWORD PTR __real@4000000000000000
movq    QWORD PTR [eax+8], xmm0
movq    xmm0, QWORD PTR _a$[ebp+16]
movq    QWORD PTR [eax+16], xmm0
lea eax, DWORD PTR $T1[ebp]
push    eax
call    ??G@YA?AVVector3D@@V0@0@Z       ; operator-
movq    xmm0, QWORD PTR [eax+16]
movq    xmm2, QWORD PTR [eax]
movq    xmm1, QWORD PTR [eax+8]
; 24   :     cout << d.x << d.y << d.z << endl;
mov ecx, DWORD PTR __imp_?cout@std@@3V?$basic_ostream@....
add esp, 52                 ; 00000034H
movsd   QWORD PTR _d$3$[ebp], xmm2
push    DWORD PTR __imp_?endl@std@@YAAAV?$basic_ostream@....
sub esp, 24                 ; 00000018H
movsd   QWORD PTR [esp+16], xmm0
movsd   QWORD PTR [esp+8], xmm1
movsd   QWORD PTR [esp], xmm2
call    DWORD PTR __imp_??6?$basic_ostream@....
mov ecx, eax
call    DWORD PTR __imp_??6?$basic_ostream@....
mov ecx, eax
call    DWORD PTR __imp_??6?$basic_ostream@....
mov ecx, eax
call    DWORD PTR __imp_??6?$basic_ostream@....

成员功能及其代表产生16个说明,而非会员功能及其代表产生37个说明!

为什么?这是因为在编译时,编译器只能从类的标题文件中提供信息。它没有类的.CPP文件可用,并且必须将成员功能视为黑匣子。

在成员函数示例中,编译器嵌入了所有功能代码,并设置为优化它。在这种情况下,它可以做得很好,因为它可以在编译时执行算术。

在非成员函数示例中,编译器必须将参数引用到堆栈上,并实际执行函数调用。结果它得到的结果是一个完全惊喜。

但是,如果您的工具链支持整个程序优化,则很可能能够在链条稍后执行此优化。如果我打开这种类型的优化,则两个功能会产生相同数量的指令。