【问题标题】:Pointer vs Variable speed in C++C ++中的指针与变速
【发布时间】:2011-10-18 15:54:06
【问题描述】:

在一次求职面试中,有人问我“在 C++ 中,如何通过普通变量标识符或指针更快地访问变量”。我必须说我对这个问题没有一个好的技术答案,所以我做了一个疯狂的猜测。

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

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

【问题讨论】:

  • 我看不出面试官的问题的重点。
  • @Mike:虽然我同意实际上知道答案并没有那么有用,但如果你有很多经验,答案是你最终会接触到的东西,以及大量琐事,比如案例您可能在正常生活中可能永远不会遇到的未定义行为等。然后,这个问题可以说是有用的,以了解 1) 申请人有很多经验 2) 申请人与“酷人”挂在一起,例如这里堆栈溢出或在 freenode 上的 #c++ 上,这种“琐事”一直出现
  • 这个问题有助于了解候选人是否了解幕后情况。这不需要很多经验,只需了解底层硬件的工作原理即可。请参阅下面的评论,了解为什么我认为这很重要。安德烈亚斯:我目前非常高兴能在一些不关心未定义行为的程序员之后进行清理。这就是为什么当您进行不相关的更改时,他们留下的代码会突然崩溃,例如更改他们意外覆盖的内存中的内容。如果你认为这是琐事......
  • @dewtell:我说的是更奇特的未定义行为案例 =)

标签: c++ performance variables pointers


【解决方案1】:

当您访问“变量”时,您会查找地址,然后获取值。

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

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

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

...然后...

c) 获取指向地址的值。

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

无论是指针变量(C 或 C++)还是引用变量(仅限 C++),都会发生完全相同的事情。

但差别非常小。

【讨论】:

  • 除非它很大,例如在加载命中存储危险期间。
  • 我不明白你为什么说差异很小。如果它是两倍的内存访问,那么它可能需要两倍的时间。
  • 我认为我们不能假设“变量”具有可直接访问的内存地址,尤其是非局部变量。
  • 它的长度可能是原来的两倍以上 - 请参阅下面关于内存层次效应的答案。和 Rig,你是对的,单个变量访问的差异通常是微不足道的,但是把足够的沙粒放在一起,你就可以得到一个海滩。例如,如果您创建一个经常访问的数据结构,由于您不了解问题而需要几个额外的间接级别,那么突然之间差异可能看起来不那么小。重点是“候选人是否了解潜在问题”,而不是“在这种特殊情况下我们应该做什么。”
  • 指针并不总是变量。例如,&varthis 是不是变量的指针。
【解决方案2】:

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

【讨论】:

  • 如果您想声称变量“更快”,那么提及编译器优化并不是正确的理由。
  • @littleadv:不同意,寄存器分配的速度优势对于常规目的而言非常重要(前 c++0x auto 分配)局部变量。
  • 分配变量并不是真正的优化,它是编译的必要部分。如果您在函数中定义“int a”,那么您并不是说“RAM 中有一个名为 'a' 的位置”,然后编译器会通过将其放入寄存器来优化它。你说的是“有一个叫做 a 的整数变量”,编译器必须决定把它放在哪里。
  • @littleadv,重读我的回答:这是一个例子,而不是假设。问题是 Jerry 认为 标识符是指向存储值的内存地址的指针,而事实并非如此。事实上,可能根本就没有内存地址。
  • 根据我的信息,指针永远不会存储在寄存器中,只有当变量被大量使用时才会被保存,如果指针将被保存在寄存器中,那么它的值不是指向的变量的值,我正在学习编译器课程,这就是正在做的事情。
【解决方案3】:

让我们暂时忽略优化,想想抽象机器必须做什么才能通过(本地)指针引用局部变量与变量。如果我们将局部变量声明为:

int i;
int *p;

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

我认为,这就是面试问题的重点——看看你是否理解这两种访问方式之间的根本区别。您认为局部变量访问涉及一种查找,并且确实如此-但是指针访问涉及相同类型的查找以获取指针的值,然后我们才能开始追踪它所指向的东西.简单来说,由于这个额外的步骤,指针访问会变慢。

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

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

现在其他评论者提出的问题如何:这有多大关系?确实,对于单次访问,​​绝对值的差异会很小,就像一粒沙子。但是你把足够多的沙子放在一起,你就得到了一个海滩。虽然(继续这个比喻)如果你正在寻找一个可以在海滩路上快速奔跑的人,你不希望有人会在他或她开始跑步之前痴迷于扫除路上的每一粒沙子,你确实希望有人在他或她不必要地穿过齐膝深的沙丘时会意识到这一点。剖析器不会总是在这里救你——用这些比喻的术语来说,它们更擅长识别一块你需要四处奔跑的大石头,而不是注意到很多让你陷入困境的小沙粒。所以我希望我的团队中的人能够从根本上理解这些问题,即使他们很少不遗余力地使用这些知识。不要停止在追求微优化的过程中编写清晰的代码,但要注意可能会降低性能的各种事情,尤其是在设计数据结构时,并且要知道你所付出的代价是否物有所值.这就是为什么我认为这是一个合理的面试问题,以探索候选人对这些问题的理解。

【讨论】:

    【解决方案4】:

    paulsm4 和 LaC 所说的 + 一点 asm:

    整数 y = 0; mov dword ptr [y],0 y = x; mov eax,dword ptr [x] ;获取 x 进行注册 mov dword ptr [y],eax ;将其存储到 y y = *px; mov eax,dword ptr [px] ;获取 x 的地址 mov ecx,dword ptr [eax] ;获取 x mov dword ptr [y],ecx ;将其存储到 y

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

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

    【讨论】:

    • +1 用于显示操作码。我们需要更多懂汇编的程序员。
    【解决方案5】:

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

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

    【讨论】:

    • 我希望现代编译器会忽略您,并自行决定哪些变量应随时在寄存器中(好吧,除非您获取此类变量的地址,否则它将忽略您)。
    【解决方案6】:

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

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

    【讨论】:

    • 没有人对寻呼相关的问题感兴趣吗?
    • 当然 - 我已经在上面的回答中解决了这个问题(从“存在内存层次结构......”开始)。它不仅仅是分页,它也会影响与缓存相关的访问。但我认为这是面试官寻找的重点之外的另一个问题。
    【解决方案7】:

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

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

    我希望平均而言,通过指针访问会比通过变量访问值慢。

    【讨论】:

      【解决方案8】:

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

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

      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 寄存器作为指向值的指针。

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

      【讨论】:

      • 但是,面试问题似乎很可疑。如果您可以直接访问变量,为什么还要使用指针?我认为代码的清晰度将决定你的选择——而不是性能。
      • 按照同样的思路,通过指针访问一个值可以确保该值不会被缓存在寄存器中(正如@LaC 所暗示的那样),尽管它仍然可以在缓存中进行优化。在一般情况下,出于多种原因,您希望避免使用指针。
      【解决方案9】:

      我认为问题的关键部分是“访问变量”。对我来说,如果变量在范围内,为什么要创建指向它的指针(或引用)来访问它?仅当变量本身是某种数据结构或者您以某种非标准方式访问它(例如将 int 解释为 float)时,使用指针或引用才有意义。

      仅在非常特定的情况下使用指针或引用会更快。在一般情况下,在我看来,就优化而言,您会尝试再次猜测编译器,而我的经验告诉我,除非您知道自己在做什么,否则这是一个坏主意。

      它甚至取决于关键字。 const 关键字很可能意味着该变量在编译时已完全优化。这比指针快。 register 关键字does not guarantee 表示变量存储在寄存器中。那么你怎么知道它是否更快呢?我认为答案是视情况而定,因为没有一刀切的答案。

      【讨论】:

      • 如果您有一个数组,并且在一个循环中迭代该数组,您需要多次访问每个项目的内容。在循环的早期创建指向该项目的指针会影响性能吗?查找指针引用比访问数组条目快还是慢?
      • @thomthom:只有一种方法可以找出答案,那就是在您选择的编译器和平台上进行尝试。你可能会对结果感到惊讶:)
      【解决方案10】:

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

      【讨论】:

        猜你喜欢
        • 2014-06-26
        • 1970-01-01
        • 2021-05-06
        • 1970-01-01
        • 2021-11-26
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-10-02
        相关资源
        最近更新 更多