我们来看一下2组例子:
第一组例子:
下标表达式:
a = get_value();
array[a] = 0;
指针表达式:
a = get_value();
*(array + a) = 0;
第二组例子:
下标表达式:
int array[10], a;
for(a=0; a<10; a+=1) {
array[a] = 0;
}
指针表达式:
int array[10], *ap;
for(a=array; a < array + 10; ap++) {
*ap = 0;
}
这2组例子都是一个使用下标,一个使用指针,每组例子的2中方式都实现相同的效果,但这2组例子使用下标和指针访问数组元素效率是一样的吗?
第一组例子效率完全一致,第二组例子循环体所有区别,计数器1和整形长度相乘,然后和指针相加,每次执行乘法迅运算都是相同的1和4。而指针每次都是相同的1和4相乘,而且只是在编译时执行一次,后面就是把指针和4相加。
它们到底怎么工作的呢?接下来,我们就来研究一下:
一、先看一个下标版本
#include <stdio.h>
#define SIZE 50
int x[SIZE];
int y[SIZE];
int i;
int *p1, *p2;
void try1() {
for(i = 0; i < SIZE; i++) {
x[i] = y[i];
}
}
int main () {
try1();
return 0;
}
编译获得汇编代码:
.file "main.c"
.text
.globl x
.bss
.align 32
.type x, @object
.size x, 200
x:
.zero 200
.globl y
.align 32
.type y, @object
.size y, 200
y:
.zero 200
.globl i
.align 4
.type i, @object
.size i, 4
i:
.zero 4
.globl p1
.align 8
.type p1, @object
.size p1, 8
p1:
.zero 8
.globl p2
.align 8
.type p2, @object
.size p2, 8
p2:
.zero 8
.text
.globl try1
.type try1, @function
try1:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $0, i(%rip)
jmp .L2
.L3:
movl i(%rip), %eax
movl i(%rip), %ecx
cltq
movl y(,%rax,4), %edx
movslq %ecx, %rax
movl %edx, x(,%rax,4)
movl i(%rip), %eax
addl $1, %eax
movl %eax, i(%rip)
.L2:
movl i(%rip), %eax
cmpl $49, %eax
jle .L3
nop
nop
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size try1, .-try1
.globl main
.type main, @function
main:
.LFB1:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $0, %eax
call try1
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1:
.size main, .-main
.ident "GCC: (GNU) 10.2.1 20201016 (Red Hat 10.2.1-6)"
.section .note.GNU-stack,"",@progbits
我们只看try1这个函数的汇编实现:
编译器做了一些优化,这里.LFB0对try1函数做了初始化,然后跳到循环.L2,L2里面判断for循环条件,如果没超过49就执行循环体L3,所以这里的循环体就是我们要看的主要部分:
movl i(%rip), %eax
movl i(%rip), %ecx
cltq
movl y(,%rax,4), %edx
movslq %ecx, %rax
movl %edx, x(,%rax,4)
movl i(%rip), %eax
addl $1, %eax
movl %eax, i(%rip)
循环体就一个x[i] = y[i]的赋值语句,对应的汇编代码:
# main.c:11: x[i] = y[i];
movl i(%rip), %eax # i, i.0_1
# main.c:11: x[i] = y[i];
movl i(%rip), %ecx # i, i.1_2
# main.c:11: x[i] = y[i];
cltq
movl y(,%rax,4), %edx # y[i.0_1], _3
# main.c:11: x[i] = y[i];
movslq %ecx, %rax # i.1_2, tmp89
movl %edx, x(,%rax,4) # _3, x[i.1_2]
将i读入eax寄存器(累积暂存器),将i读入ecx寄存器(计数寄存器), cltq 等效于 movslq%eax,%rax,就是将eax的地址扩展后读入rax寄存器,然后将y的新地址读入到edx寄存器,接下来扩展ecx的地址,读入到rax寄存器,然后将edx寄存器读入到x的新地址。 这里我们看到有2次乘以4的操作,因为这里是整形,地址占4个字节。
二、指针版本
void try2() {
for(p1 = x, p2 = y; p1 - x < SIZE; ) {
*p1++ = *p2++;
}
}
编译得到汇编:
try2:
.LFB1:
.cfi_startproc
pushq %rbp #
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp #,
.cfi_def_cfa_register 6
# main.c:16: for(p1 = x, p2 = y; p1 - x < SIZE; ) {
movq $x, p1(%rip) #, p1
# main.c:16: for(p1 = x, p2 = y; p1 - x < SIZE; ) {
movq $y, p2(%rip) #, p2
# main.c:16: for(p1 = x, p2 = y; p1 - x < SIZE; ) {
jmp .L5 #
.L6:
# main.c:17: *p1++ = *p2++;
movq p2(%rip), %rdx # p2, p2.4_1
leaq 4(%rdx), %rax #, _3
movq %rax, p2(%rip) # _3, p2
# main.c:17: *p1++ = *p2++;
movq p1(%rip), %rax # p1, p1.6_4
leaq 4(%rax), %rcx #, _6
movq %rcx, p1(%rip) # _6, p1
# main.c:17: *p1++ = *p2++;
movl (%rdx), %edx # *p2.5_2, _7
# main.c:17: *p1++ = *p2++;
movl %edx, (%rax) # _7, *p1.7_5
.L5:
# main.c:16: for(p1 = x, p2 = y; p1 - x < SIZE; ) {
movq p1(%rip), %rax # p1, p1.8_8
subq $x, %rax #, _9
# main.c:16: for(p1 = x, p2 = y; p1 - x < SIZE; ) {
cmpq $196, %rax #, _9
jle .L6 #,
# main.c:19: }
nop
nop
popq %rbp #
.cfi_def_cfa 7, 8
ret
.cfi_endproc
这里L5是循环结束控制,使用了地址的方式,整形地址占4个字节,50个整数,就是200个字节,所以0-196个字节表示循环体范围。
L6就是循环体代码中分别读取p2和p1的地址,读取p2的值给p1,这里同样有2次乘以4的地址转换,效率看起来没啥变化。而且L5里面的循环条件更复杂一点。
三、指针版本+计数器版本
void try3() {
for(i = 0, p1 = x, p2 = y; i < SIZE; i++) {
*p1++ = *p2++;
}
}
编译得到汇编代码:
try3:
.LFB2:
.cfi_startproc
pushq %rbp #
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp #,
.cfi_def_cfa_register 6
# main.c:22: for(i = 0, p1 = x, p2 = y; i < SIZE; i++) {
movl $0, i(%rip) #, i
# main.c:22: for(i = 0, p1 = x, p2 = y; i < SIZE; i++) {
movq $x, p1(%rip) #, p1
# main.c:22: for(i = 0, p1 = x, p2 = y; i < SIZE; i++) {
movq $y, p2(%rip) #, p2
# main.c:22: for(i = 0, p1 = x, p2 = y; i < SIZE; i++) {
jmp .L8 #
.L9:
# main.c:23: *p1++ = *p2++;
movq p2(%rip), %rdx # p2, p2.9_1
leaq 4(%rdx), %rax #, _3
movq %rax, p2(%rip) # _3, p2
# main.c:23: *p1++ = *p2++;
movq p1(%rip), %rax # p1, p1.11_4
leaq 4(%rax), %rcx #, _6
movq %rcx, p1(%rip) # _6, p1
# main.c:23: *p1++ = *p2++;
movl (%rdx), %edx # *p2.10_2, _7
# main.c:23: *p1++ = *p2++;
movl %edx, (%rax) # _7, *p1.12_5
# main.c:22: for(i = 0, p1 = x, p2 = y; i < SIZE; i++) {
movl i(%rip), %eax # i, i.13_8
addl $1, %eax #, _9
movl %eax, i(%rip) # _9, i
.L8:
# main.c:22: for(i = 0, p1 = x, p2 = y; i < SIZE; i++) {
movl i(%rip), %eax # i, i.14_10
# main.c:22: for(i = 0, p1 = x, p2 = y; i < SIZE; i++) {
cmpl $49, %eax #, i.14_10
jle .L9 #,
# main.c:25: }
nop
nop
popq %rbp #
.cfi_def_cfa 7, 8
ret
.cfi_endproc
使用计数器的方式,简化了循环条件的判断处理,虽然C代码看起来冗余一些,但是汇编效率可能好一丢丢......
但总体差别并不大,都会有复制指针值的
四、寄存器指针版本
void try4() {
register int *p1, *p2;
register int i;
for(i = 0, p1 = x, p2 = y; i < SIZE; i++) {
*p1++ = *p2++;
}
}
编译得到汇编代码:
try4:
.LFB3:
.cfi_startproc
pushq %rbp #
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp #,
.cfi_def_cfa_register 6
pushq %r13 #
pushq %r12 #
pushq %rbx #
.cfi_offset 13, -24
.cfi_offset 12, -32
.cfi_offset 3, -40
# main.c:31: for(i = 0, p1 = x, p2 = y; i < SIZE; i++) {
movl $0, %ebx #, i
# main.c:31: for(i = 0, p1 = x, p2 = y; i < SIZE; i++) {
movl $x, %r12d #, p1
# main.c:31: for(i = 0, p1 = x, p2 = y; i < SIZE; i++) {
movl $y, %r13d #, p2
# main.c:31: for(i = 0, p1 = x, p2 = y; i < SIZE; i++) {
jmp .L11 #
.L12:
# main.c:32: *p1++ = *p2++;
movq %r13, %rdx # p2, p2.15_1
leaq 4(%rdx), %r13 #, p2
# main.c:32: *p1++ = *p2++;
movq %r12, %rax # p1, p1.16_2
leaq 4(%rax), %r12 #, p1
# main.c:32: *p1++ = *p2++;
movl (%rdx), %edx # *p2.15_1, _3
# main.c:32: *p1++ = *p2++;
movl %edx, (%rax) # _3, *p1.16_2
# main.c:31: for(i = 0, p1 = x, p2 = y; i < SIZE; i++) {
addl $1, %ebx #, i
.L11:
# main.c:31: for(i = 0, p1 = x, p2 = y; i < SIZE; i++) {
cmpl $49, %ebx #, i
jle .L12 #,
# main.c:34: }
nop
nop
popq %rbx #
popq %r12 #
popq %r13 #
popq %rbp #
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE3:
.size try4, .-try4
.globl main
.type main, @function
这里的循环体L12就高效一些了,因为指针变量直接保存在寄存器中的,直接使用硬件的地址自增模型直接增加值。
四、去掉计数器的纯寄存器指针版本
void try5() {
register int *p1, *p2;
for(p1 = x, p2 = y; p1 < &x[SIZE];) {
*p1++ = *p2++;
}
}
编译得到汇编代码:
try5:
.LFB4:
.cfi_startproc
pushq %rbp #
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp #,
.cfi_def_cfa_register 6
pushq %r12 #
pushq %rbx #
.cfi_offset 12, -24
.cfi_offset 3, -32
# main.c:38: for(p1 = x, p2 = y; p1 < &x[SIZE];) {
movl $x, %ebx #, p1
# main.c:38: for(p1 = x, p2 = y; p1 < &x[SIZE];) {
movl $y, %r12d #, p2
# main.c:38: for(p1 = x, p2 = y; p1 < &x[SIZE];) {
jmp .L14 #
.L15:
# main.c:39: *p1++ = *p2++;
movq %r12, %rdx # p2, p2.17_1
leaq 4(%rdx), %r12 #, p2
# main.c:39: *p1++ = *p2++;
movq %rbx, %rax # p1, p1.18_2
leaq 4(%rax), %rbx #, p1
# main.c:39: *p1++ = *p2++;
movl (%rdx), %edx # *p2.17_1, _3
# main.c:39: *p1++ = *p2++;
movl %edx, (%rax) # _3, *p1.18_2
.L14:
# main.c:38: for(p1 = x, p2 = y; p1 < &x[SIZE];) {
cmpq $x+200, %rbx #, p1
jb .L15 #,
# main.c:41: }
nop
nop
popq %rbx #
popq %r12 #
popq %rbp #
.cfi_def_cfa 7, 8
ret
.cfi_endproc
这个版本C代码就很简洁,汇编代码也很高效,$x+200就是&x[SIZE], 由于SIZE是个常量,编译过程就会完成,这就是最好的实现。
结论:
1、当根据某个固定数目的增量在一个数组中移动时,使用指针变量将比使用下标产生效率更高的代码。
2、声明寄存器变量的指针通常比位于静态内存和堆栈中的指针效率更高。
3、如果可以通过测试一些已经初始化并经过调整的内容来判断循环是否应该终止,就不需要计数器帮助。
4、在运行时求值比&array[SIZE]这种常量表达式代价高。
但面向C程序员的,可读性很重要,所以在效率提高不很明显的情况下,以简洁可读性高为原则,越复杂风险越高。
这篇博客探讨了在C语言中,使用数组下标和指针遍历数组时的效率差异。通过两组例子和对应的汇编代码分析,得出结论:在固定增量遍历时,指针可能更高效;寄存器指针比静态或堆栈指针效率更高;避免在运行时计算常量表达式。然而,考虑到可读性,通常以简洁和易懂为主,除非性能提升显著。

1940

被折叠的 条评论
为什么被折叠?



