通过指针算法访问数组值与使用C中的下标访问数组值

时间:2008-10-24 11:25:06

标签: objective-c c arrays pointers pointer-arithmetic

我继续读到,在C中,使用指针算法通常比下标数组访问更快。即使是现代(据称是优化的)编译器,这是真的吗?

如果是这样,当我开始在Mac上学习C到Objective-C和Cocoa时,情况仍然如此吗?

在C和Objective-C中,哪种数组访问的首选编码风格?被认为(由各自语言的专业人士)更清晰,更“正确”(缺乏更好的术语)?

11 个答案:

答案 0 :(得分:74)

您需要了解此声明背后的原因。你有没有问过自己为什么它更快?让我们比较一些代码:

int i;
int a[20];

// Init all values to zero
memset(a, 0, sizeof(a));
for (i = 0; i < 20; i++) {
    printf("Value of %d is %d\n", i, a[i]);
}

它们都是零,这真是一个惊喜:-P问题是,a[i]实际上在低级机器代码中意味着什么?这意味着

  1. 在内存中取a的地址。

  2. i的单个a项的大小a倍添加到该地址(int通常为4个字节)。

  3. 从该地址获取值。

  4. 因此,每次从a获取值时,i的基地址都会加到int i; int a[20]; int * b; memset(a, 0, sizeof(a)); b = a; for (i = 0; i < 20; i++) { printf("Value of %d is %d\n", i, *b); b++; } 乘以4的结果中。如果您只是取消引用指针,则不需要执行步骤1.和2.仅执行步骤3.

    请考虑以下代码。

    ++

    此代码可能更快......但即使是这样,差异也很小。为什么它会更快? “* b”与上述步骤3相同。但是,“b ++”与步骤1和步骤2不同。“b ++”将指针增加4。

      

    对新手很重要:正在运行int   在指针上不会增加   指针在内存中的一个字节!它会   将指针增加多少字节   在内存中它指向的数据是   在尺寸方面。它指向int和。{   i在我的机器上是四个字节,所以b ++   将b增加4倍!)

    好的,但为什么它会更快?因为向指针添加四个比将-Os乘以四并将其添加到指针要快。在任何一种情况下都有一个加法,但在第二种情况下,你没有乘法(你避免了一次乘法所需的CPU时间)。考虑到现代CPU的速度,即使阵列是1 mio元素,我想知道你是否真的能够对差异进行基准测试。

    现代编译器可以优化任何一个同样快速的程序,您可以通过查看它生成的程序集输出来检查。您可以通过将“-S”选项(大写S)传递给GCC来实现。

    这是第一个C代码的代码(优化级别-O2已经被使用,这意味着优化代码大小和速度,但不会做速度优化,这将显着增加代码大小,不像{{1}和-O3完全不同的是:

    _main:
        pushl   %ebp
        movl    %esp, %ebp
        pushl   %edi
        pushl   %esi
        pushl   %ebx
        subl    $108, %esp
        call    ___i686.get_pc_thunk.bx
    "L00000000001$pb":
        leal    -104(%ebp), %eax
        movl    $80, 8(%esp)
        movl    $0, 4(%esp)
        movl    %eax, (%esp)
        call    L_memset$stub
        xorl    %esi, %esi
        leal    LC0-"L00000000001$pb"(%ebx), %edi
    L2:
        movl    -104(%ebp,%esi,4), %eax
        movl    %eax, 8(%esp)
        movl    %esi, 4(%esp)
        movl    %edi, (%esp)
        call    L_printf$stub
        addl    $1, %esi
        cmpl    $20, %esi
        jne L2
        addl    $108, %esp
        popl    %ebx
        popl    %esi
        popl    %edi
        popl    %ebp
        ret
    

    与第二个代码相同:

    _main:
        pushl   %ebp
        movl    %esp, %ebp
        pushl   %edi
        pushl   %esi
        pushl   %ebx
        subl    $124, %esp
        call    ___i686.get_pc_thunk.bx
    "L00000000001$pb":
        leal    -104(%ebp), %eax
        movl    %eax, -108(%ebp)
        movl    $80, 8(%esp)
        movl    $0, 4(%esp)
        movl    %eax, (%esp)
        call    L_memset$stub
        xorl    %esi, %esi
        leal    LC0-"L00000000001$pb"(%ebx), %edi
    L2:
        movl    -108(%ebp), %edx
        movl    (%edx,%esi,4), %eax
        movl    %eax, 8(%esp)
        movl    %esi, 4(%esp)
        movl    %edi, (%esp)
        call    L_printf$stub
        addl    $1, %esi
        cmpl    $20, %esi
        jne L2
        addl    $124, %esp
        popl    %ebx
        popl    %esi
        popl    %edi
        popl    %ebp
        ret
    

    嗯,这是不同的,这是肯定的。 104和108的数字差异来自变量b(在第一个代码中,堆栈上有一个变量较少,现在我们还有一个变量堆栈地址)。 for循环中的实际代码差异为

    movl    -104(%ebp,%esi,4), %eax
    

    相比
    movl    -108(%ebp), %edx
    movl    (%edx,%esi,4), %eax
    

    实际上对我而言,第一种方法看起来更快(!),因为它发出一个CPU机器代码来执行所有工作(CPU为我们完成所有工作),而不是拥有两个机器代码。另一方面,下面的两个汇编命令可能比上面的汇编命令运行时间更短。

    作为结束语,我要说取决于你的编译器和CPU功能(CPU以什么方式访问内存所提供的命令),结果可能是两种方式。任何一个可能更快/更慢。除非您将自己完全限制在一个编译器(也就是一个版本)和一个特定CPU上,否则您无法确定。由于CPU可以在单个汇编命令中执行越来越多的操作(很久以前,编译器实际上必须手动获取地址,将i乘以4并在获取值之前将它们加在一起),以前的语句是如今的绝对真理现在越来越值得怀疑。谁也知道CPU如何在内部工作?上面我将一个装配说明与另外两个装配说明进行比较。

    我可以看到指令的数量不同,这样的指令所需的时间也可能不同。此外,这些指令在其机器演示中需要多少内存(它们需要从内存传输到CPU缓存)是不同的。但是,现代CPU不会像您提供的那样执行指令。将大的指令(通常称为CISC)分成小的子指令(通常称为RISC),这也允许它们更好地优化程序流程以提高内部速度。实际上,下面的第一条单指令和另外两条指令可能会导致同一组子指令,在这种情况下,没有任何可测量的速度差异。

    关于Objective-C,它只是带扩展名的C语言。所以对于C来说,所有适用的东西对于Objective-C以及指针和数组都是正确的。如果你另一方面使用对象(例如,NSArrayNSMutableArray),这是一个完全不同的野兽。但是,在这种情况下,您必须使用方法访问这些数组,没有可供选择的指针/数组访问。

答案 1 :(得分:10)

  

“通常使用指针算术   比订阅数组更快   访问“

罗。无论哪种方式都是相同的操作。 Subscripting是用于将(元素大小*索引)添加到数组的起始地址的语法糖。

也就是说,当迭代数组中的元素时,每次通过循环获取指向第一个元素并增加它的指针通常比每次从循环变量计算当前元素的位置要快一些。 (虽然在现实生活中应用这个问题并不常见。首先检查你的算法,过早优化是所有邪恶的根源等等)

答案 2 :(得分:5)

这可能有点偏离主题(对不起),因为它没有回答你关于执行速度的问题,但是你应该考虑过早优化是所有邪恶(Knuth)的根源。在我看来,特别是当仍然(​​重新)学习语言时,一定要以最容易阅读的方式编写。 然后,如果您的程序运行正确,请考虑优化速度。 无论如何,大多数时候你的代码都会足够快。

答案 3 :(得分:4)

Mecki有一个很好的解释。根据我的经验,索引与指针通常很重要的事情之一就是其他代码在循环中的作用。例如:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <iostream>

using namespace std;

typedef int64_t int64;
static int64 nsTime() {
  struct timespec tp;
  clock_gettime(CLOCK_REALTIME, &tp);
  return tp.tv_sec*(int64)1000000000 + tp.tv_nsec;
}

typedef int T;
size_t const N = 1024*1024*128;
T data[N];

int main(int, char**) {
  cout << "starting\n";

  {
    int64 const a = nsTime();
    int sum = 0;
    for (size_t i=0; i<N; i++) {
      sum += data[i];
    }
    int64 const b = nsTime();
    cout << "Simple loop (indexed): " << (b-a)/1e9 << "\n";
  }

  {
    int64 const a = nsTime();
    int sum = 0;
    T *d = data;
    for (size_t i=0; i<N; i++) {
      sum += *d++;
    }
    int64 const b = nsTime();
    cout << "Simple loop (pointer): " << (b-a)/1e9 << "\n";
  }

  {
    int64 const a = nsTime();
    int sum = 0;
    for (size_t i=0; i<N; i++) {
      int a = sum+3;
      int b = 4-sum;
      int c = sum+5;
      sum += data[i] + a - b + c;
    }
    int64 const b = nsTime();
    cout << "Loop that uses more ALUs (indexed): " << (b-a)/1e9 << "\n";
  }

  {
    int64 const a = nsTime();
    int sum = 0;
    T *d = data;
    for (size_t i=0; i<N; i++) {
      int a = sum+3;
      int b = 4-sum;
      int c = sum+5;
      sum += *d++ + a - b + c;
    }
    int64 const b = nsTime();
    cout << "Loop that uses more ALUs (pointer): " << (b-a)/1e9 << "\n";
  }
}

在基于Core 2的快速系统(g ++ 4.1.2,x64)上,这是时间:

    Simple loop (indexed): 0.400842
    Simple loop (pointer): 0.380633
    Loop that uses more ALUs (indexed): 0.768398
    Loop that uses more ALUs (pointer): 0.777886

有时索引更快,有时指针算术也是如此。这取决于CPU和编译器如何管理循环执行。

答案 4 :(得分:3)

如果您正在处理数组类型数据,我会说使用下标会使代码更具可读性。在今天的机器上(特别是对于这样的简单机器),可读代码更重要。

现在,如果你明确地处理了一大块数据,你可以使用malloc(),并且你想在数据中得到一个指针,比如音频文件头中的20个字节,那么我认为地址算法更清楚地表达了什么你正试图这样做。

我不确定这方面的编译器优化,但即使下标速度较慢,也只能在最多的几个时钟周期内放慢速度。当你能从清晰的思路中获得更多的东西时,这几乎不算什么。

编辑:根据其他一些回复,下标只是一个合成元素,并没有像我想的那样影响性能。在这种情况下,一定要使用指针所指向的块内的访问数据来表达您想要表达的任何上下文。

答案 5 :(得分:3)

请记住,即使用超标量cpus等查看机器代码,也很难预测执行速度

  • 乱序执行
  • 流水线
  • 分支预测
  • 超线程
  • ...

这不只是计算机器指令,甚至不仅仅计算时钟周期。 在真正需要的情况下,似乎更容易衡量。即使计算给定程序的正确循环次数并不是不可能的(我们必须在大学里进行),但这并不是很有趣也很难做到。 旁注:在多线程/多处理器环境中,正确测量也很困难。

答案 6 :(得分:1)

char p1[ ] = "12345";
char* p2 = "12345";

char *ch = p1[ 3 ]; /* 4 */
ch = *(p2 + 3); /* 4 */

C标准没有说哪个更快。在可观察的行为上是相同的,并且由编译器以它想要的任何方式实现它。通常它根本不会读取内存。

通常,除非指定编译器,版本,体系结构和编译选项,否则无法说哪个“更快”。即使这样,优化也将取决于周围环境。

所以一般建议是使用任何更清晰,更简单的代码。使用array [i]为一些工具提供了尝试查找索引越界条件的能力,所以如果你使用数组,最好只对它们进行处理。

如果它很关键 - 查看编译器生成的汇编程序。但请记住,当您更改围绕它的代码时,它可能会发生变化。

答案 7 :(得分:1)

不,使用指针算法并不快,而且可能更慢,因为优化编译器可能会使用英特尔处理器上的LEA(加载有效地址)等指令或其他处理器上类似的指针算法,这比指令运算速度快于添加或添加/ mul 。它的优点是可以同时执行多项操作而不会影响标志,并且还需要一个周期来计算。顺便说一句,以下内容来自GCC手册。所以-Os并不主要针对速度进行优化。

我也完全同意themarko。首先尝试编写干净,可读和可重用的代码,然后考虑优化并使用一些分析工具来找到瓶颈。大多数情况下,性能问题是与I / O相关的,或者是一些不好的算法或者你必须要追捕的一些bug。 Knuth是男人; - )

我刚刚想到你会用结构数组做什么。如果你想做指针运算,那么你肯定应该为结构的每个成员做。这听起来有点矫枉过正吗?是的,当然它是矫枉过正的,它也打开了一道大门来掩盖错误。

  

-Os优化尺寸。 Os启用所有通常不会增加代码大小的O2优化。它还执行旨在减少代码大小的进一步优化。

答案 8 :(得分:0)

这不是真的。它与下标运算符一样快。在Objective-C中,您可以使用C中的数组和面向对象的样式,其中面向对象的样式要慢很多,因为它会因为调用的动态特性而在每个调用中进行一些操作。

答案 9 :(得分:0)

速度不太可能存在差异。

使用数组运算符[]可能是首选,因为在C ++中,您可以使用与其他容器相同的语法(例如向量)。

答案 10 :(得分:0)

我已经为几个AAA游戏的c ++ /程序集优化工作了10年,我可以说在特定平台/编译器上我已经工作了,指针运算已经完成了一个非常可衡量的差异。

作为一个透视的例子,我能够通过指针算法替换所有数组访问,以完全不相信我的同事,使我们的粒子生成器中的一个非常紧密的循环快40%。我从一位老师那里听说这是当天的一个很好的伎俩,但我认为它不会对我们今天的编译器/ cpu产生影响。我错了;)

必须指出的是,许多控制台臂处理器都没有现代cisc cpu的所有可爱功能,编译器有时会有些不稳定。