【问题标题】:In C, accessing my array index is faster or accessing by pointer is faster?在 C 中,访问我的数组索引更快还是通过指针访问更快?
【发布时间】:2011-02-08 23:45:35
【问题描述】:

在 C 中,访问数组索引更快还是通过指针访问更快? 我的意思是更快,哪个需要更少的时钟周期。 该数组不是常量数组。

【问题讨论】:

  • 如果你有一个指向元素确切位置的指针,那么指针可能会更快,因为索引必须根据基地址计算元素的地址。

标签: c optimization assembly


【解决方案1】:

哪一个更快完全取决于系统,但两者在功能上是等价的,如果一个真的更快,我会感到非常惊讶。也就是代码

myArr[index]

完全等价于

*(&myArr[0] + index)

同样,写作

*ptr

相当于写

ptr[0]

大多数编译器都足够聪明,可以解决这个问题,所以如果一个编译器比另一个更快,我会感到惊讶。

不过,更重要的是,您可能不应该太担心这一点。在一切正常之后再担心优化。如果您发现数组访问确实让您感到沮丧,那么请考虑寻找更快的替代方案。否则,不要担心;除非您迫切需要优化,否则拥有干净、可读、可维护的代码比拥有优化的代码更有价值。

【讨论】:

  • “除非你迫切需要优化,否则拥有干净、可读、可维护的代码比拥有优化的代码更有价值”,然后优化是 - 什么?一个非常笼统的陈述,比那些拥有多年经验的人更适合那些相信他的教授在大学里所说的一切的人。一切都是利弊的平衡,我们所有的新同事越早了解这一点,我们的客户就会越早从中受益。
  • “过早的优化是万恶之源”
【解决方案2】:

templatetypedef 总结了它。为他的回应添加一些支持。以这些示例函数为例:

无符号整数 fun1 ( 无符号整数 *x ) { 无符号整数 ra,rb; rb=0; for(ra=0;ra

现在 gcc 产生了这个:

00000000 乐趣1: 0: e52d4004 推 {r4} ; (str r4,[sp,#-4]!) 4: e1a03000 移动 r3, r0 8: e2804efa 添加 r4, r0, #4000 ; 0xfa0 c: e3a00000 mov r0, #0 10: e1a02003 移动 r2, r3 14:e492c004 ldr ip,[r2],#4 18:e5931004 ldr r1,[r3,#4] 1c: e2823004 添加 r3, r2, #4 20: e080000c 添加 r0, r0, ip 24: e1530004 cmp r3, r4 28: e0800001 加 r0, r0, r1 2c:1afffff7 bne 10 30: e49d4004 弹出 {r4} ; (ldr r4, [sp], #4) 34:e12fff1e bx lr 00000038 乐趣2: 38:e3a03000 移动 r3,#0 3c: e1a02003 移动 r2, r3 40: e790c003 ldr ip, [r0, r3] 44: e2833004 添加 r3, r3, #4 48:e7901003 ldr r1,[r0,r3] 4c: e2833004 添加 r3, r3, #4 50: e082200c 添加 r2, r2, ip 54:e3530efa cmp r3,#4000; 0xfa0 58: e0822001 添加 r2, r2, r1 5c:1afffff7 bne 40 60: e1a00002 移动 r0, r2 64:e12fff1e bx lr

代码不同,但我对错失优化机会感到惊讶。

Clang/llvm 产生了这个:

00000000 乐趣1: 0:e3a01000 移动 r1,#0 4: e3a02ffa 移动 r2, #1000 ; 0x3e8 8: e1a03001 移动 r3, r1 c: e2522001 subs r2, r2, #1 10:e490c004 ldr ip,[r0],#4 14: e08c3003 添加 r3, ip, r3 18: e2c11000 sbc r1, r1, #0 1c: e182c001 orr ip, r2, r1 20:e35c0000 cmp ip,#0 24:1afffff8 bne c 28: e1a00003 移动 r0, r3 2c: e12fff1e bx lr 00000030 乐趣2: 30:e3a01000 移动 r1,#0 34: e3a02ffa 移动 r2, #1000 ; 0x3e8 38: e1a03001 移动 r3, r1 3c: e2522001 潜艇 r2, r2, #1 40:e490c004 ldr ip,[r0],#4 44: e08c3003 添加 r3, ip, r3 48: e2c11000 sbc r1, r1, #0 4c: e182c001 orr ip, r2, r1 50:e35c0000 cmp ip,#0 54:1afffff8 bne 3c 58: e1a00003 移动 r0, r3 5c: e12fff1e bx lr

您可能会注意到编译器生成了完全相同的代码、指针或偏移量。通过更改编译器,我比更改指针与数组索引更好。我认为 llvm 可以做得更好一些,我需要进一步研究这一点以了解我的代码做了什么导致这种情况。

编辑:

我希望编译器至少使用支持指针的 ldr rd,[rs],#4 指令,并希望编译器会看到它可以破坏数组地址,从而将其视为指针而不是而不是一个数组的偏移量(并使用上面的指令,这基本上是 clang/llvm 所做的)。或者,如果它做了数组的事情,它将使用 ldr rd,[rm,rn] 指令。基本上是希望其中一个编译器能够生成以下解决方案之一:

乐趣: 移动 r1,#0 mov r2,#1000 funa_loop: ldr r3,[r0],#4 添加 r1,r1,r3 潜艇 r2,r2,#1 bne funa_loop 移动 r0,r1 bx lr 乐趣: 移动 r1,#0 移动 r2,#0 funb_loop: ldr r3,[r0,r2] 添加 r1,r1,r3 添加 r2,r2,#4 cmp r2,#0x4000 bne funb_loop 移动 r0,r1 bx lr 功能: 移动 r1,#0 mov r2,#4000 潜艇 r2,r2,#4 函数循环: beq func_done ldr r3,[r0,r2] 添加 r1,r1,r3 潜艇 r2,r2,#4 b func_loop func_done: 移动 r0,r1 bx lr

没有到达那里,但已经很接近了。这是一个有趣的练习。注意以上都是 ARM 汇编程序。

一般来说,(不是我特定的 C 代码示例,也不一定是 ARM),一些流行的架构会从基于寄存器的地址 (ldr r0,[r1]) 加载和使用寄存器加载index/offset (ldr r0,[r1,r2]) 其中地址是两个寄存器的总和。理想情况下,一个寄存器是数组的基地址,第二个是索引/偏移量。前者从寄存器加载适合指针,后者适合数组。如果您的 C 程序不打算更改或移动指针或索引,那么在这两种情况下,这意味着计算静态地址,然后使用正常加载,数组和指针都应该产生相同的指令。对于更改指针/索引的更有趣的情况。

指针 ldr r0,[r1] ... 添加 r1,r1,一些数字 数组索引 ldr r0,[r1,r2] ... 添加 r2,r2,一些数字

(根据需要将 load 替换为 store,将 add 替换为 sub)

某些架构没有三寄存器寄存器索引指令,因此您必须执行类似的操作

数组索引: 移动 r2,r1 ... ldr r0,[r2] ... 添加 r2,r2,一些数字

或者取决于编译器,它可能会变得非常糟糕,尤其是如果您编译进行调试或没有优化,并且假设您没有添加三个寄存器

数组索引: 移动 r2,#0 ... 移动 r3,r1 添加 r3,r2 ldr r4,[r3] ... 添加 r2,一些数字

所以这两种方法很可能是相等的。正如在 ARM 上看到的那样,它可以将两个(在立即数限制内)指针指令合并为一个,从而加快速度。数组索引解决方案会消耗更多的寄存器,并且取决于架构中可用寄存器的数量,这会促使您更快、更频繁地将寄存器交换到堆栈中(比使用指针时更频繁),从而使您的速度更慢。如果您不介意破坏基地址,那么最重要的是指针解决方案可能从性能角度为您提供优势。它与您的代码和编译器有很大关系。对我来说,它的可读性开始发挥作用,我觉得数组更容易阅读和遵循,其次我是否需要保留该指针以释放 malloc 或再次遍历该内存等。如果是这样,我可能会使用一个数组一个索引,如果它是一次性传递并且我不关心破坏基地址,我将使用一个指针。正如您在上面看到的编译器生成的代码,如果性能很关键,那么无论如何都要在汇编器中手动编写解决方案(基于建议的方法,让编译器先尝试)。

【讨论】:

  • 一个非常好的答案,其中不同的方法相互测试。 A- 对于一个雄心勃勃的方法,但您没有按照指定的问题对数组执行此操作。我也错过了相对执行时间的比较。不过我会给你一个 1!
  • 时钟周期与其他任何问题一样加载。带有内存周期的指令可能会花费更多,所以如果一个循环有 8 条指令,另一个有 7 条指令,但有 8 条指令的指令也有额外的一两个内存周期,这可能会真正改变结果。缓存差异等它是一团糟。即使在单个处理器系列中,甚至在同一个处理器内核中,速度也会因芯片之外的因素而有很大差异。
  • 阅读汇编语言的禅宗 Michael Abrash 或其他人。即使您认为代码看起来更快,您也需要对其进行计时和测试。一个泛化的更快或更慢的问题,没有关于它运行的大量细节,一个泛化的答案,这通常会花费你更多这通常在编译器上更容易实现,这大约是你能做的最好的。除非您有详细的原因并且通常是封闭的平台,否则您不需要的只是通用的性能解决方案。
  • 如果您查看 templatetypedef 的(优秀)答案,我确实回应了这个问题以及原始问题,数组与指针。 *ptr vs ptr[0] 不值得编码我们知道答案,ptr[index] vs *(&ptr + index) 或 ptr[index++] vs *ptr++ 是可以在编译器中看到性能问题的用例输出很有趣。我没有涵盖 *(&ptr +index) vs ptr[index] 因为就像 *ptr vs ptr[0] 它们是相同的。因此,如果您有问题或 templatetypedef 未涵盖的另一个用例,我可以与之交谈。
  • 正如我在这里的评论中所写的那样:就像编写糟糕的 C 代码并获得性能不佳的机器代码一样,编写良好的 C 代码并获得快速和高效的机器码。许多“不优化器”(或应称为的任何东西)似乎认为编写什么源代码并不重要,因为编译器无论如何都会产生相同的机器代码。这是纯粹的垃圾。至于手写汇编代码,我认为通常最好留给编译器。但是,我仍然可以帮助编译器做得更好。
【解决方案3】:

在我接触过的每个编译器上,简单的索引操作都会编译成相同的机器代码。为了可读性,通常建议按索引。

涉及指针访问与数组索引的不同逻辑的更复杂的情况需要逐个检查。如果您有疑问,请像往常一样分析您的代码。

【讨论】:

    【解决方案4】:

    您的问题没有有意义的答案。语言级别的操作没有与之相关的特定“速度”。它们本身不能“更快”或“更慢”。

    只有 CPU 指令可以更快或更慢,并且只有 CPU 指令可以消耗 CPU 周期。为了以某种方式将这种“速度”概念从 CPU 指令带回到语言级别的操作 [这些 CPU 指令是从中生成的],在一般情况下,您需要了解上下文。之所以如此,是因为相同的语言级操作可以在不同的上下文中生成完全不同的 CPU 指令(更不用说它还可能取决于编译器设置等)

    换句话说,发布实际代码。作为一个抽象的无上下文问题,它根本没有意义。

    【讨论】:

    • 这个答案太无知了,以至于让我们的职业蒙羞。严厉的话,但我100%支持他们。编译器不是您似乎暗示的随机指令生成器,而是通常对我们的工作至关重要的优质工具。但是,如果你给它提供臃肿的垃圾代码源代码,它会忠实地生成性能不佳的臃肿垃圾机器代码。从您的回答来看,这似乎是您的一般经验。如果您向它提供高质量的源代码,它将忠实地生成快速的高质量机器代码。也许您不知道这种关系?
    • @Olof Forshell:你是在拖钓还是什么?您在评论中发布的是某种无关紧要的废话(谈论耻辱)。我只是说实际的机器代码将极大地取决于上下文。就像在一个循环中顺序访问所有数组元素一样,索引访问和“滑动指针”访问通常会在许多现代编译器中产生相同的代码。在任何大规模访问上下文之外访问单个数组元素时,代码可能很容易导致指针和索引访问不同。
    • 我完全不清楚“臃肿的垃圾源代码”是在哪里以及如何出现的,以及它与问题和/或我的回答有何关系。我怀疑某人早上忘记喝一杯果汁可能是这里问题的根源。
    • 您说“语言级别的操作没有特定的“速度””,我说是的。一段构造不佳的 C 代码将导致编译器生成糟糕且缓慢的机器代码。因此,相反的情况也必须是正确的。至于“相同的语言级操作可以在不同的上下文中生成完全不同的 CPU 指令”,我说一般来说它们会产生或多或少相同的代码,但是上下文可能会添加指令而不是寻址本身。然而,优化级别可以产生非常不同的序列,但这不是“上下文”。
    • 让我澄清一下我是否不清楚:语言级操作与执行速度有关,因为编译器使用它们来生成可执行代码。我知道没有商业或免费的编译器可以从糟糕的源代码中生成好的机器代码。这并不意味着不存在这样的编译器,也许您遇到过它们?速度优化级别通常会产生比调试(低)优化更快的代码。这个问题肯定有非常有意义的答案。
    【解决方案5】:

    在最低级别,这些操作大多倾向于编译成相同的东西。如果您真的感兴趣,您应该让您的 C 编译器生成汇编输出(例如使用 gcc -S),以便您可以检查,尤其是因为它至少取决于:

    • 您的目标平台。
    • 你的编译器。
    • 您的优化级别。

    您会发现,即使存在 差异(这是值得怀疑的),这种程度的微优化也大多不值得您为此付出努力。您最好进行宏观优化,例如改进算法,因为这样可以提供更多的投资回报。

    在这些影响可能很小的情况下,我总是针对可读性进行优化。

    【讨论】:

    • “你会发现,即使存在差异(这是值得怀疑的),那种程度的微优化也不值得你付出努力”是一个非常绝对的陈述,我如果受到挑战,它肯定会像一副纸牌一样掉下来。你真的应该在这里提供你的(几乎不知情的)意见吗?我说正确的答案是“在某些(尽管不是全部)情况下,付出努力是值得的,在此过程中,您将学到一些对未来项目有用的东西。”
    • @Olot,你说得对,我通常鄙视绝对,所以我改变了它。但是,我的意见是不知情。如果没有脑死编译器,指针和数组索引将具有完全相同相同的底层汇编,并且宏优化的好处远远超过几个时钟周期。这是长期的经验。
    • “完全正确” ...嗯。一个不完全假设的情况:假设您的函数应该返回第一个结构成员的索引,其中 active 为 FALSE。该结构有 100 万个成员,随机加载到 2/3。这是一个相当简单的算法,其中循环内的局部优化意味着平均搜索时间减半或更好。是否应该不进行这种优化,因为应该执行宏优化,但由于算法太简单而没有进行优化?
    • @Olot:这取决于优化(你还没有说明它是什么,所以有点难以评估)。如果它是一些简单的东西,比如常见的子表达式消除或常量折叠,那最好留给编译器(除非,正如我所说,你有一个无脑编译器)。如果您的优化是根据它的 2/3 加载的事实来更改算法,或者将第一个 FALSE 索引单独维护到列表中,以便仅在某些更新时更改(在该点之前伪造一个或验证那个特定的更新),那么 宏优化。
    • 公共子表达式消除相对经常由具有更多“空闲”寄存器要分配的 RISC 编译器执行。对于 x86-32(不知道 -64),它执行的频率相对较低,因为优化必须与 RISC(8 对 32)一样多的寄存器进行处理。在我看来,说编译器“脑死亡”似乎有点过于简单化了:我猜它已经尽其所能地处理了它必须使用的东西。我在页面下方的回答说明了使用显式公共子表达式消除的优化以及如何进一步发展。
    【解决方案6】:

    明确消除常见的子表达式可能对您有用。如果您使用的是 x86 或 RISC 架构和优化器质量,可能会有所不同。

    当我编写一个必须遍历数组或索引结构的例程时,我会计算一个指向数组/结构成员基址的指针并使用它来寻址。基本情况

    struct SOMETHING list[100];
    
    int find_something (...)
    {
      int i;
    
      i=0;
      while (i<(sizeof(list)/sizeof(struct SOMETHING)))
      {
        if (list[i].active && list[i].last_access+60<current_time) return i;
    
        ++i;
      }
      return -1;
    }
    

    可以细化为(即帮助编译器生成更好的代码):

    int find_something (...)
    {
      int i;
      struct SOMETHING *pList;
    
      i=0;
      while (i<(sizeof(list)/sizeof(struct SOMETHING)))
      {
        pList=&list[i];
        if (pList->active && pList->last_access+60<current_time) return i;
    
        ++i;
      }
      return -1;
    }
    

    这只是为了说明,代码的简单性可能会隐式生成指针,但如果例程更复杂,情况可能并非如此。使用“列表[i]”。在第一个示例中,您将运行(在 x86 上)编译器没有足够的寄存器来生成和存储地址一次的风险(RISC haha​​),而是为每个引用生成它。对于 x86 情况,需要一个局部变量来存储指针,除非明确指示,否则很少有编译器会创建堆栈变量。在 RISC 上,编译器有很多可供使用的寄存器,并且通常会决定每次迭代都创建(并保留)一次指针是值得的。

    循环可以进一步细化:

      pList=list;
      i=0;
      while (i<(sizeof(list)/sizeof(struct SOMETHING)))
      {
        if (pList->active && pList->last_access+60<current_time) return i;
    
        pList+=1;    
        ++i;
      }
    

    这种构造没有任何地址计算开销。 “pList+=1”(其他人可能更喜欢“++pList”)会导致将一个常量值(等于单个行/成员的大小)添加到 pList。

    还有:

      pList=list;
      pEndList=&list[sizeof(list)/sizeof(struct SOMETHING)];
      while (pList!=pEndList)
      {
        if (pList->active && pList->last_access+60<current_time) return pList-list;
    
        pList+=1;    
      }
    

    这消除了索引增量,并将其替换为循环外的一乘和循环内的一除(在返回构造中只执行一次)。

    现在,在所有你不优化的人开始尖叫血腥谋杀之前,我的观点是,可接受的构造取决于它们所在函数的大小和复杂性。我可能不会在一个 300 行的函数中考虑这个结构,它足够复杂,但在上述情况下?如果搜索是整体处理的重要组成部分?如果加速足够大?

    那为什么不呢?优点和缺点。它总是有利有弊。充分利用它们。绝对的?很少(如果有的话)。

    【讨论】:

      【解决方案7】:

      一样。都是 O(1),时钟时间可以忽略不计。你基本上是在访问内存地址。

      【讨论】:

      • 两个操作为 O(1) 并没有说明它们的相对速度; 1 和 1000000 都是 O(1)。
      • 鉴于 OPs 问题的抽象性质,这个(和 AndreyT 的)答案是唯一正确的答案。如果不进行组装,您将无法回答这个问题。你说你知道的,它是 O(1)。这就是理论计算机科学的魅力所在。
      • 你怎么知道它是 O(1)?如果我的编译器将a[i] 编译为一个将指针增加1 i 倍的循环会怎样?这将使它成为 O(n)。如果你想学究气...
      • @Oyster:啊,是的,内存被实现为链表的著名案例...... :)
      • “唯一正确的答案” - 这是一个非常广泛的陈述,我不确定任何人都可以为它辩护,甚至是你。
      【解决方案8】:

      通过索引访问数组时,实际上是在执行两个操作:加法(将索引添加到数组基址),然后是内存访问(实际读取或写入结果地址的内容)。我想当您谈论“通过指针访问”时,您的意思是您已经拥有指向目标元素的指针。因此,从逻辑上讲,使用指针可以节省“加法”部分,因此应该更快,或者至少不会变慢。

      不过……

      作为一个粗略的估计,在现代计算机中,内存访问比添加要昂贵得多(尤其是如果它从缓存中掉出来),因此差异(如果有的话)将是微小的。在某些架构(例如 x86 或 PowerPC)上,加法和内存访问可以组合成一个操作码。情况也会有所不同,具体取决于数组地址是否为编译时常量(即数组不是常量数据,而是声明为全局变量,vs 使用malloc() 获得的块)。使用数组可以帮助编译器找到更好的代码,关于泛型指针(特别是在使用 restrict 关键字时)。上下文有很大的影响(例如,当时有多少空闲寄存器?)。

      所以:

      • 您的问题没有绝对的答案。您必须尝试并采取措施。
      • 如果存在可检测到的差异(可能没有),则很难预测哪个方向,这取决于大量外部因素,包括特定的编译器版本和优化标志、处理器架构以及模型、内存布局等。
      • 如果没有一些相当深入的汇编知识和一点编译理论,您将无法获得任何可靠的优化收益。
      • 你应该先专注于制作一个正确的代码,然后才担心优化;在实际条件下进行适当测量之前,不会出现性能问题。

      【讨论】:

      • 鉴于调查方法,完全有可能了解编译器将为不同的 C 构造生成什么样的机器代码,如果不是确切的序列(至少我不能)。因此,也可以选择性能最高的构造,并确保性能将在执行时出现。不调查这些问题的开发人员将没有资格说这是不可能的,他或她也不会理解他们应该避免提供建议,除非“我不知道。有没有办法可以调查这个?”
      猜你喜欢
      • 2018-07-04
      • 2023-03-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多