在C中通过指针算术与下标访问数组值
我继续阅读,在C中,使用指针算术通常比数组访问下标更快。 即使是现代的(据称是最优化的)编译器,这是否也是如此?
如果是这样,当我开始从Mac上学习C到Objective-C和Cocoa时 ,情况还是如此吗?
在C和Objective-C中,数组访问的首选编码风格是什么? (由各自语言的专业人士)认为更清晰,更“正确”(缺less更好的术语)?
你需要了解这个说法背后的原因。 你有没有质疑自己为什么更快? 我们来比较一些代码:
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问题是,实际上在低级机器码中是什么意思? 它的意思是
-
把内存中的地址。
-
将
i
的单个项目的大小添加到该地址(int通常是四个字节)。 -
从该地址获取值。
所以每次你从a
取一个值, a
的基地址就加上i
乘以4的结果。 如果你只是取消引用一个指针,步骤1和2.不需要执行,只有第3步。
考虑下面的代码。
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++; }
这个代码可能会更快……但即使是这样,差别也很小。 为什么它会更快? “* b”与上述步骤3相同。 但是,“b ++”与步骤1和步骤2不同。“b ++”会将指针增加4。
( 对于新手来说很重要 :在指针上运行
++
不会增加指针在内存中的一个字节!它将增加指针的内存中所有字节的大小,指向一个int
,int
在我的机器上是四个字节,所以b ++增加了四个!)
好吧,但为什么它会更快? 因为将四个指针添加到一个指针比将i
乘以四并将其添加到一个指针要快。 在这两种情况下都有一个补充,但是在第二个中没有乘法(避免了一次乘法所需的CPU时间)。 考虑到现代CPU的速度,即使这个数组是100万个元素,我也想知道你是否真的能够找出差异的基准。
一个现代编译器可以优化任何一个同样快的东西,你可以通过查看它产生的汇编输出来检查。 您通过将“-S”选项(大写S)传递给GCC来做到这一点。
下面是第一个C代码的代码(优化级别-Os
已被使用,这意味着优化代码大小和速度,但不会进行速度优化,这会显着增加代码大小,与-O2
不同,与-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号码的差别来自于variablesb
(在第一个代码中,堆栈中有一个variables较less,现在我们还有一个变化的堆栈地址)。 for
循环中真正的代码差异是
movl -104(%ebp,%esi,4), %eax
相比
movl -108(%ebp), %edx movl (%edx,%esi,4), %eax
其实对我来说,看起来第一种方法更快(!),因为它发出一个CPU机器代码来执行所有的工作(CPU完成这一切),而不是有两个机器代码。 另一方面,下面的两个汇编命令的运行时间可能会比上面的时间less。
作为一个结束语,我要说取决于你的编译器和CPU能力(CPU以什么方式提供访问内存的命令),结果可能是两种方式之一。 任何一个可能会更快/更慢。 你不能肯定地说,除非你完全限制到一个编译器(也意味着一个版本)和一个特定的CPU。 由于CPU可以在一个单独的汇编命令中做得越来越多(几年前,编译器实际上必须手动获取地址,将i
乘以4并在获取值之前将它们相加),过去绝对真实性的陈述现在越来越有问题了。 还有谁知道CPU如何在内部工作? 上面我比较了一个汇编指令和另外两个汇编指令。
我可以看到,指令的数量是不同的,这样的指令需要的时间也可以不同。 另外这些指令在机器表示中需要多less内存(毕竟它们需要从内存传输到CPU高速caching)是不同的。 然而现代的CPU并不像你提供的那样执行指令。 拆分大指令(通常称为CISC)分成小的子指令(通常称为RISC),这也允许他们更好地优化内部速度的程序stream程。 实际上,下面的第一条单指令和另外两条指令可能会导致相同的一组子指令 ,在这种情况下,不存在任何可测量的速度差异。
关于Objective-C,它只是C的扩展。 所以对于C来说,所有对于Objective-C来说都是正确的,就指针和数组而言也是如此。 如果你使用对象(例如, NSArray
或NSMutableArray
),这是一个完全不同的野兽。 但是在这种情况下,你必须用方法访问这些数组,没有指针/数组的访问权限可供select。
“使用指针算术通常比数组访问下标更快”
罗。 两种方法都是一样的。 下标是添加(元素大小*索引)到数组的起始地址的语法糖。
也就是说,当迭代数组中的元素时,每次通过循环获取指向第一个元素的指针并增加它通常会比从循环variables中计算当前元素的位置稍快。 (虽然这对于现实生活中的应用来说很重要,但首先检查一下你的algorithm,过早的优化是万恶之源等等)
这可能有点偏离主题(对不起),因为它不能回答你关于执行速度的问题,但你应该考虑过早的优化是万恶之源 (Knuth)。 在我看来,特别是在还是(重新)学习语言的时候,一定要把它写成最先阅读的方式。 那么,如果你的程序运行正确 ,考虑优化速度。 大多数情况下,你的代码总是足够快。
麦基有一个很好的解释。 从我的经验来看,索引与指针通常有关的事情之一是其他代码在循环中的位置。 例:
#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): " << (ba)/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): " << (ba)/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): " << (ba)/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): " << (ba)/1e9 << "\n"; } }
在一个基于Core 2的快速系统(g ++ 4.1.2,x64)上,下面是时间:
简单循环(索引):0.400842 简单循环(指针):0.380633 循环使用更多的ALU(索引):0.768398 使用更多ALU(指针)的循环:0.777886
有时索引是更快,有时指针算术是。 这取决于CPU和编译器如何能够循环执行循环。
如果你正在处理数组types的数据,我会说使用下标使代码更具可读性。 在今天的机器上(特别是对于像这样简单的东西),可读代码更重要。
现在,如果你明确地处理malloc()的数据块,并且你想获得一个指针在数据中,比如在一个audio文件头部中有20个字节,那么我认为地址算术更清楚地expression了你试图做。
在这方面,我不确定编译器的优化,但即使下标速度较慢,也只能在最多几个时钟周期内减慢速度。 当你从思路的清晰度中获得更多的东西时,这几乎是不可能的。
编辑:根据其他一些反应,下标只是一个syntacitic元素,并没有影响性能,像我想的那样。 在这种情况下,通过指针所指向的块内的访问数据,一定要使用任何你想expression的上下文。
请记住,即使用超标量cpu等查看机器码,执行速度也很难预测
- 不合格的处罚
- stream水线
- 分支预测
- 超线程
- …
这不仅仅是计算机器指令,甚至不仅仅是计数时钟。 在真正需要的情况下,看起来更容易。 即使计算一个给定程序的正确循环次数并不是不可能的(我们必须在大学里这样做),但是这样做并不是很有趣也很难做到。 旁注:正确测量在multithreading/多处理器环境中也很难。
char p1[ ] = "12345"; char* p2 = "12345"; char *ch = p1[ 3 ]; /* 4 */ ch = *(p2 + 3); /* 4 */
C标准没有说哪个更快。 在可观察的行为是相同的,它是由编译器来实现它以任何想要的方式。 往往它甚至不会读内存。
一般来说,除非指定编译器,版本,体系结构和编译选项,否则无法说哪个更“快”。 即便如此,优化将取决于周围的环境。
所以一般的build议是使用任何给出更明确,更简单的代码。 使用array [i]给出了一些工具能够试图find索引超出边界的条件,所以如果你使用的是数组,最好把它们当作对待。
如果非常重要,请查看编译器生成的汇编程序。 但请记住,它可能会随着您更改周围的代码而发生变化。
不,使用指针算术不是更快,也可能更慢,因为优化编译器可能使用Intel处理器上的LEA(加载有效地址)或其他处理器上的类似指令运算的指令,这比加法或加/ mul更快。 它具有一次做多件事而不影响标志的优点,而且还需要一个周期来计算。 顺便说一句,下面是从GCC手册。 所以-Os
不主要为了速度而优化。
我也完全同意themarko。 首先尝试编写干净,可读和可重用的代码,然后考虑优化并使用一些分析工具来查找瓶颈。 大多数情况下,性能问题是I / O相关的,或者是一些错误的algorithm或者一些你必须追查的错误。 克努特是男人;-)
我刚想到,你会用一个结构数组来做什么。 如果你想做指针算术,那么你绝对应该为结构的每个成员做这件事。 这听起来像是矫枉过正吗? 是的,当然这是矫枉过正,也为掩盖错误打开了一扇大门。
-Os
优化大小。Os
使所有的O2优化通常不增加代码大小。 它还执行进一步的优化,旨在减less代码大小。
速度不太可能会有任何差异。
使用数组运算符[]可能是首选,因为在C ++中,可以使用与其他容器(例如vector)相同的语法。
这不是真的。 与下标操作符一样快。 在Objective-C中,可以使用类似于C语言的数组和面向对象的风格,因为面向对象的风格要慢得多,因为在调用的dynamic性质上它会在每个调用中进行一些操作。
我已经为几个AAA游戏的C ++ /程序集优化工作了10年,而且我可以说在我工作的特定平台/编译器上,指针算术做出了相当大的差别。
作为一个例子,我可以通过用指针algorithmreplace所有的数组访问,使我们的工作人员完全不相信,从而使得我们的粒子生成器能够实现一个非常紧密的循环。 我从老师那里听说过这是一个很好的伎俩,但是我认为这对我们今天的编译器/ CPU没有任何影响。 我错了 ;)
必须指出的是,许多控制台处理器并不具备现代cisc cpu的所有可爱特性,有时编译器有点晃动。