c++性能:局部变量vs数据成员

C++ performance: Local variable vs data member

本文关键字:vs 数据成员 局部变量 性能 c++      更新时间:2023-10-16

假设代码blow是我的类。它是简化的,不完整的。让我们关注operator()的实现。

class Delta{
public:
    long long operator()() {
        auto now = steady_clock::now(); 
        auto delta = (now - last).count();
        last = now;
        return delta;
    }
private:
    steady_clock::time_point last;
};

operator()可能每秒被调用数千次。我只是想知道频繁地分配和释放变量nowdelta可能会损害operator()的性能。那么,如果我想最大限度地提高速度,让nowdelta成为class Delta的数据成员是否更好?但我也听说局部变量在编译时甚至可能不存在。所以开销也不存在

实际上,这个运算符的速度对我的应用程序的速度没有任何影响。我只是不想知道一个与编译器无关的答案。当这种情况发生时。我应该让它成为数据成员还是局部变量?

在x86-64上,我希望这段代码最终在RAX中分配nowdelta。在汇编语言中,代码看起来是这样的顺序:

assume RSI:ptr _Delta
call steady_clock::now()
sub rax, [rsi].last
mov [rsi].last, rax
ret

当然,在真正的汇编语言中,您会看到steady_clock::now()的混乱名称(例如),但您可以了解总体思想。在进入任何非静态成员函数时,它将在某个寄存器中具有this。返回值总是在rax中。我看不出编译器需要(甚至想要)为任何其他变量分配空间的任何特别好的理由。

在32位x86上,这将最终使用一些堆栈空间的可能性要高得多,尽管它可能会在EDX:EAX中返回64位值,在这种情况下,事情最终将与上面的情况非常相似,只是使用了一个寄存器。

大多数其他处理器开始使用比x86更多的寄存器,因此寄存器压力更低。例如,在SPARC上,一个例程通常在启动时有8个空闲的本地寄存器可供使用,因此在寄存器中分配now几乎是肯定的。

底线:你不太可能看到显著的速度差异,但如果你确实看到了差异,我猜更可能倾向于使用局部变量而不是成员变量。

这不会有太大(如果有的话)区别。操作系统按页面分配内存(包括堆栈)。因此,堆栈可能不会完成一个页面,因此进程不需要上下文切换来获得另一个页面。

对于编译器中立的回答,速度将归结为上下文切换,处理器上运行的其他事情,....

此外,像你这样的人似乎只关注微观的性能改进,而回避更大的图景。最好先找出瓶颈在哪里,然后集中精力解决这些问题。记住80/20法则。

优化通常取决于编译器。但是假设您使用的是某种不错的编译器,那么就不会有性能损失,所以不用担心。为了证明这一点,我用gcc 4.7编译了你的代码,优化级别3:

call   400770 <std::chrono::system_clock::now()@plt> ;; Call.
mov    rdx,rax             ;; Remembe temporary value in %rdx.
sub    rax,QWORD PTR [rbx] ;; Divide
mov    QWORD PTR [rbx],rdx ;; Wrie Back.

根据上下文,可以进一步优化。或者情况会变得更糟。只是给你一个临时变量可以在堆栈上创建的例子——你在nowlast之间放了很多代码,寄存器分配算法不能把所有的变量都放在寄存器中,它将求助于使用堆栈。因此,对于实际结果,您必须检查生成的机器代码。但坦白说,这里没有太多需要优化的,除了一件显而易见的事情。如果你关心性能,你需要担心的是通过PLT的大量调用。换句话说,不要使用std::chrono::system_clock::now() .

你的代码中有一个性能问题。

每当操作符()每次在堆栈上被调用时,将有两个变量将被创建和销毁(实际上它会发生)。

短期内的性能你不会注意到,因为系统总是为堆栈保留一些内存,并且每次访问相同的内存。

但是在长期运行(性能运行)中,您将能够看到差异。

我不反对任何其他答案,但是让我试着用简单的术语来解释这个问题。我将忽略一些现实生活中很重要的细节,但不要教你所问的概念。

假设你有一个函数有这些变量

 int a;
 int b;
 int c;
 int d;

在编译期间,编译器将所有局部变量的大小加起来,当调用函数时,运行时代码为所有变量分配足够的堆栈空间。因此,如果sizeof(int)为4,则上述变量需要16字节的堆栈空间。大多数编译器使用机器寄存器来保存堆栈指针(sp),所以当函数被调用时,运行时代码会做类似

的事情。
sp = sp + 16

为我们的4个变量保留空间。注意,如果函数有1个或1000个局部变量,分配局部变量的运行时代码花费的时间是相同的。每个变量没有成本(除非它们有要调用的变量)。如果我们有一个像

这样的C语句
d = b;

伪机器码看起来像

*(sp+12) = *(sp+4)

,其中12是变量d在堆栈上的偏移量,4是变量b的偏移量。(偏移量不会这么简单,堆栈上还分配了其他东西。)

当您定义具有成员变量的结构/类时,如

class X {
 int a;
 int b;
 int c;
 int d;
 void foo() { d = b; }
};

编译器还将所有变量的大小加起来,并为每个变量分配偏移量。但是现在foo()里面的代码变成了

*(this+12) = *(this + 4)

虽然sp几乎总是保存在机器寄存器中,但this指针只极有可能在机器寄存器中。现代编译器查看使用最多的变量,并将这些变量存储在寄存器中。由于"this"通常被引用很多(通常隐式),它通常被分配给一个寄存器。当'this'在寄存器中时,性能应该是相同的