指针与c++中的可变速度

Pointer vs Variable speed in C++

本文关键字:速度 c++ 指针      更新时间:2023-10-16

在一次求职面试中,有人问我:"在c++中,如何通过普通的变量标识符或指针更快地访问变量?"我必须说我对这个问题没有一个好的技术答案,所以我胡乱猜了一下。

我说过访问时间可能与正常变量/标识符相同,它是指向存储值的内存地址的指针,就像指针一样。换句话说,就速度而言,它们具有相同的性能,指针的不同只是因为我们可以指定它们所指向的内存地址。

面试官似乎对我的回答不太信服/满意(虽然他什么也没说,只是继续问别的问题),所以我想过来问他们我的回答是否准确,如果不准确,为什么(从理论和技术角度)。

当你访问一个"变量"时,你先查找地址,然后获取值。

记住——指针是一个变量。所以实际上,你:

a)查找(指针变量的)地址,

b)获取值(存储在该变量中的地址)

…然后…

c)获取指向的地址的值。

所以,是的,通过"指针"(而不是直接)访问确实涉及(一点)额外的工作和(稍微)更长的时间。

无论它是指针变量(C或c++)还是引用变量(仅c++),结果都是一样的。

但是差别非常小。

变量不必存在于主存中。根据具体情况,编译器可以将其全部或部分存储在寄存器中,并且访问寄存器比访问RAM要快得多。

让我们暂时忽略优化,只考虑抽象机器在引用局部变量和通过(局部)指针引用变量时必须做些什么。如果我们将局部变量声明为:

int i;
int *p;

当我们引用i的值时,未优化的代码必须获取(比如说)在当前堆栈指针12之后的值,并将其加载到寄存器中,以便我们可以使用它。然而,当我们引用*p时,相同的未优化代码必须从16经过当前堆栈指针获取p的值,将其加载到寄存器中,然后获取寄存器指向的值并将其加载到另一个寄存器中,以便我们可以像以前一样使用它。第一部分工作是相同的,但是从概念上讲,指针访问涉及到在处理值之前需要完成的额外步骤。

我认为,这就是面试问题的重点——看看你是否理解这两种访问方式之间的根本区别。你可能在想,局部变量访问涉及到一种查找,确实是这样,但是指针访问涉及到一种相同类型的查找,在我们开始追踪指针指向的东西之前,先找到指针的值。简单地说,未优化的术语,指针访问将会变慢,因为额外的步骤。

现在通过优化,可能会发生两个时间非常接近或相同的情况。确实,如果其他最近的代码已经使用了p的值来引用另一个值,那么您可能已经在寄存器中找到了p,因此通过p查找*p所花费的时间与通过堆栈指针查找i所花费的时间相同。出于同样的原因,如果您最近使用了i的值,那么您可能已经在寄存器中找到。虽然*p的值可能也是如此,但优化器只能在确定p在同一时间内没有改变的情况下重用寄存器中的值。重用i的值没有这样的问题。简而言之,虽然在优化中访问两个值可能花费相同的时间,但访问局部变量几乎永远不会变慢(除非在真正病态的情况下),而且可能会更快。这就是面试官问题的正确答案。

在存在内存层次结构的情况下,时间上的差异可能会变得更加明显。局部变量将在堆栈上彼此靠近,这意味着您很可能在第一次访问它时就已经在主存和缓存中找到所需的地址(除非它是您在此例程中访问的第一个局部变量)。对于指针指向的地址没有这样的保证。除非最近访问过它,否则您可能需要等待缓存丢失,甚至是页面错误,才能访问指向地址,这可能会使它比本地变量慢几个数量级。不,这种情况不会一直发生,但在某些情况下,这是一个可能产生影响的潜在因素,这也是候选人在回答这类问题时可能会提到的。

那么其他评论者提出的问题呢:这有多重要?这是真的,对于一个单一的访问,这种差异在绝对值上是很小的,就像一粒沙子。但如果你把足够多的沙粒放在一起,就会形成海滩。虽然(继续这个比喻)如果你正在寻找一个能在沙滩路上快速奔跑的人,你不会想要一个在他或她开始跑步之前就痴迷于清扫路上的每一粒沙子的人,你确实想要一个在他或她不必要地跑过膝盖深的沙丘时能意识到的人。在这里,侧写员并不总是能拯救你——用这些比喻的术语来说,他们更善于识别一块你需要四处奔跑的大石头,而不是注意到许多让你陷入困境的小沙粒。因此,我希望我的团队成员能够从基本层面上理解这些问题,即使他们很少费心去使用这些知识。不要为了追求微优化而停止编写清晰的代码,但要意识到可能会降低性能的东西,尤其是在设计数据结构时,并要意识到你所付出的代价是否物有所值。这就是为什么我认为这是一个合理的面试问题,可以探讨候选人对这些问题的理解。

paulsm4和LaC说的话+一点asm:

<>之前Int y = 0;[y],0Y = x;[x];获取x寄存器Mov双字PTR [y];将其存储到y中Y = *px;[px];获取x的地址Mov [ax];获取xMov双字PTR [y],执行;将其存储到y中之前

另一方面,这并不重要,而且这可能更难优化(fe)。你不能在CPU寄存器中保留值,因为指针只是指向内存中的某个地方)。所以y = x的优化代码;可以像这样:

mov dword ptr [y], ebx -如果我们假设本地变量x存储在ebx

我认为面试官想让你提到寄存器这个词。例如,如果您将变量声明为寄存器变量,编译器将尽最大努力确保它存储在CPU上的寄存器中。

关于总线访问和其他类型变量和指针的协商的一些讨论将有助于构建它。

paulsm4和LaC已经和其他成员一起很好地解释了它。我想强调的是,当指针指向堆中已经分页的东西时,分页的效果。

=>局部变量可以在堆栈或寄存器中使用
=>,而在指针的情况下,指针可能指向不在缓存中的地址,分页肯定会降低速度。

变量保存了某种类型的值,访问该变量意味着从内存或寄存器中获取该值。当从内存中获取值时,我们需要从某处获取它的地址——大多数情况下,它必须被加载到寄存器中(有时它可以是加载命令本身的一部分,但这种情况很少见)。

指针保存一个值的地址;这个值必须在内存中,指针本身可以在内存中,也可以在寄存器中。

我预计通过指针访问的平均速度会比通过变量访问的速度慢。

您的分析忽略了指针本身是必须访问的内存变量的常见场景。

影响软件性能的因素有很多,但是如果你对所涉及的变量做一些简化的假设(特别是它们没有以任何方式缓存),那么每一级间接指针都需要额外的内存访问。

int a = 1234; // luggage combination
int *b = &a;
int **c = &b;
...
int e = a; // one memory access
int e = *b; // two memory accesses
int e = **c; // three memory accesses

因此,"哪个更快"的简短答案是:忽略可能发生的编译器和处理器优化,直接访问变量更快。

在最好的情况下,该代码在紧密循环中重复执行,指针值可能被缓存到CPU寄存器中,或者在最坏的情况下被缓存到处理器的L1缓存中。在这种情况下,一级指针间接访问可能与直接访问变量一样快或更快,因为"直接"可能意味着通过"堆栈指针"寄存器(加上一些偏移量)。在这两种情况下,您都使用CPU寄存器作为指向值的指针。

还有其他可能影响此分析的场景,例如对于全局或静态数据,其中变量的地址被硬编码到指令流中。在这种情况下,答案可能取决于所涉及的处理器的具体情况。

我认为问题的关键部分是"访问变量"。对我来说,如果变量在作用域中,为什么要创建指向它的指针(或引用)来访问它?只有当变量本身是某种类型的数据结构,或者以某种非标准方式访问它(如将int型解释为浮点型)时,使用指针或引用才有意义。

使用指针或引用只有在非常特殊的情况下才会更快。在一般情况下,在我看来,就优化而言,你会试图猜测编译器,我的经验告诉我,除非你知道你在做什么,否则这是一个坏主意。

它甚至取决于关键字。const关键字很可能意味着该变量在编译时被完全优化了。这比指针要快。register关键字不能保证变量存储在寄存器中。那么你怎么知道它是否更快呢?我想答案是,这要看情况,因为没有放之四海而皆准的答案。

我认为一个更好的答案可能是它取决于指针"指向"的位置。注意,变量可能已经在缓存中了。然而,指针可能会招致fetch惩罚。这类似于链表与矢量性能的权衡。Vector是缓存友好的,因为所有的内存都是连续的。然而,由于链表包含指针,因此可能会导致缓存损失,因为内存可能分散在