我一直读到,在C语言中,使用指针算法通常比订阅数组访问更快.即使使用现代(据说是优化的)编译器,这也是真的吗?
如果是这样的话,当我开始从学习C转向学习Objective-C和Mac上的Cocoa时,情况仍然如此吗?
在C和Objective-C中,哪种是数组访问的首选编码样式?(由各自语言的专业人士)认为哪个更易读、更"正确"(因为没有更好的术语)?
我一直读到,在C语言中,使用指针算法通常比订阅数组访问更快.即使使用现代(据说是优化的)编译器,这也是真的吗?
如果是这样的话,当我开始从学习C转向学习Objective-C和Mac上的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]);
}
它们都是零,真是令人惊讶:-问题是,在低级机器代码中,a[i]
到底意味着什么?这意味着
以内存中a
的地址为例.
向该地址添加i
倍于a
的单个项目的大小(int通常是四个字节).
从该地址获取值.
因此,每次从a
中提取一个值时,a
的基址就会被加到i
乘以4的结果中.如果只是取消引用指针,请执行步骤1.二,.不需要执行,只需执行步骤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++"与第一步不同.第二步."b++"将使指针增加4.
(important for newbies: run
++
分)
好吧,但为什么会更快呢?因为给指针加四比把i
乘以四再加上指针要快.这两种情况都有加法运算,但在第二种情况下,没有乘法运算(避免了一次乘法所需的CPU时间).考虑到现代CPU的速度,即使数组是1百万个元素,我想知道您是否真的可以对差异进行基准测试.
通过查看编译器生成的程序集输出,可以判断现代编译器是否可以将其中任何一个优化为同样快的速度.您可以通过将"-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),这也使他们能够更好地优化程序流以提高内部速度.事实上,第一条单指令和下面的另外两条指令可能会产生same set of sub-instructions,在这种情况下,没有任何可测量的速度差.
关于Objective-C,它只是带有扩展的C.因此,在指针和数组方面,对C适用的一切对Objective-C也适用.如果另一方面使用对象(例如,NSArray
或NSMutableArray
),则这是一种完全不同的野兽.但是,在这种情况下,无论如何都必须使用方法访问这些数组,没有指针/数组访问可供 Select .