我继续读到,在C中,使用指针算法通常比下标数组访问更快.即使使用现代(据称是优化的)编译器,这是真的吗?
如果是这样,当我开始在Mac上学习C到Objective-C和Cocoa时,情况仍然如此吗?
在C和Objective-C中,哪种是数组访问的首选编码风格?被认为(由各自语言的专业人士)更清晰,更"正确"(缺乏更好的术语)?
您需要了解此声明背后的原因.你有没有问过自己为什么它更快?让我们比较一些代码:
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]
实际上在低级机器代码中意味着什么?它的意思是
取a
内存中的地址.
添加i
单个项目大小a
到该地址的时间(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.
(对于新手很重要:
++
在指针上运行不会增加指针在内存中的一个字节!它会将指针增加内存中的字节数与它指向的数据的大小相同.它指向aint
和int
4个字节我的机器,所以b ++增加b四!)
好的,但为什么它会更快?因为向指针添加四个比乘以i
四并将其添加到指针要快.在任何一种情况下都有一个加法,但在第二种情况下,你没有乘法(你避免了一次乘法所需的CPU时间).考虑到现代CPU的速度,即使阵列是1 mio元素,我想知道你是否真的能够对差异进行基准测试.
现代编译器可以优化任何一个同样快速的程序,您可以通过查看它生成的程序集输出来检查.您可以通过将"-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的数字差异来自变量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以及指针和数组都是正确的.如果你另一方面使用对象(例如,NSArray
或者NSMutableArray
),这是一个完全不同的野兽.但是在这种情况下,无论如何都必须使用方法访问这些数组,没有可供选择的指针/数组访问权限.
"使用指针算法通常比下标数组访问更快"
罗.无论哪种方式都是相同的操作.Subscripting是用于将(元素大小*索引)添加到数组的起始地址的语法糖.
也就是说,当迭代数组中的元素时,每次通过循环获取指向第一个元素并增加它的指针通常会比每次从循环变量计算当前元素的位置稍微快一些.(虽然在现实生活中应用这个问题很不寻常.首先检查你的算法,过早优化是所有邪恶的根源等等)
这可能有点偏离主题(抱歉)因为它没有回答你关于执行速度的问题,但是你应该认为过早的优化是所有邪恶的根源(Knuth).在我看来,特别是当仍然(重新)学习语言时,一定要以最容易阅读的方式编写.然后,如果您的程序运行正常,请考虑优化速度.无论如何,大多数时候你的代码都会足够快.