Linux C存取效率对比——堆、栈、常量区

本文主要探讨堆和栈在使用中的存取效率,利用宏汇编指令分析访存情况来进行简单判断。

实验环境及使用工具:i686,32位Ubuntu Linux,gcc (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3,gdb

首先,引用一道题的代码和“答案”,这是“比较堆和栈存取效率”的,但是事实上,他给的两种方式都用的栈,个人试出来的占用堆空间的情况,只能是malloc()和new()等系统调用产生的。

#include<stdio.h>  

main(){  

        char a = 1;  

        char c[] = "1234567890";  

        char *p = "1234567890";  

        a = c[1];  

        a = p[1];  


“答案”:存取效率的比较

chars1[]="aaaaaaaaaaaaaaa";
char *s2="bbbbbbbbbbbbbbbbb";
aaaaaaaaaaa是在运行时刻赋值的;
而bbbbbbbbbbb是在编译时就确定的;

主要疑问是,两者都在栈中存储,确定"aaa...."是运行时刻赋值的么?栈中是运行时“赋值”么。


那么,既然看到了这段代码,还是对比一下吧,可以先比较一下以“数组”和"指针"(后边会解释具体含义)形式初始化的两段字符串的存取效率。

宏汇编指令执行过程:

Breakpoint 1, main () at efficiencyOfStorage.c:4  

4       char a = 1;  

1: x/i $pc  

=> 0x8048419 <main+21>:    movb   $0x1,0x10(%esp)  

 

5        char c[] = "1234567890";  

0x804841e <main+26>:   movl   $0x34333231,0x11(%esp)  

0x8048426 <main+34>:   movl   $0x38373635,0x15(%esp)  

0x804842e <main+42>:   movw   $0x3039,0x19(%esp)  

0x8048435 <main+49>:   movb   $0x0,0x1b(%esp)  

6        char *p = "1234567890";  

0x804843a <main+54>:  movl   $0x8048540,0xc(%esp)  

7        a = c[1];  

0x8048442 <main+62>:  movzbl 0x12(%esp),%eax  

0x8048447 <main+67>:   mov    %al,0x10(%esp)  

8        a = p[1];  

0x804844b <main+71>: mov 0xc(%esp),%eax  

0x804844f <main+75>: movzbl 0x1(%eax),%eax 

0x8048453 <main+79>: mov %al,0x10(%esp)

10    }

0x8048457 <main+83>: mov 0x1c(%esp),%edx 

0x804845b <main+87>: xor %gs:0x14,%edx 

0x8048462 <main+94>: je 

0x8048469 <main+101> 

0x8048464 <main+96>: call 

0x8048320 <__stack_chk_fail@plt> 

0x8048469 <main+101>: leave 

0x804846a <main+102>: ret  

(根据变量声明的先后顺序可以看到,在linux栈偏移地址是增长的)

首先,它是字符数组,数字字符0-9转换成ascii码是0x30-0x39。 


char c[] = "1234567890";  

0x804841e <main+26>:   movl   $0x34333231,0x11(%esp)  

0x8048426 <main+34>:   movl   $0x38373635,0x15(%esp)  

0x804842e <main+42>:   movw   $0x3039,0x19(%esp)  

0x8048435 <main+49>:   movb   $0x0,0x1b(%esp)

整个数组c包括结束符应该占用11个地址空间(可以用sizeof验证),为0x11至0x1b。

小端模式,字符数组“01234567890” 从低地址0x11开始排列,到0x1b结束(结束符ascii值0x00):  

栈中偏移地址:0x11 0x12 0x13 0x14 0x15 0x16 0x17 0x18 0x19 0x1a 0x1b 

相应内存内容:0x31 0x32 0x33 0x34 0x35 0x36 0x37 0x38 0x39 0x30 0x00

PS:尽管第三次只压入word长的数据(两字节),但还是单独用了一行指令压大小为byte的结束符。

 

6        char *p = "1234567890";  

0x804843a <main+54>:  movl   $0x8048540,0xc(%esp)  

p指针本身肯定在栈,直接让p指针指向字符串常量的地址(0x8048540),“1234567890”被存入该地址的过程被省略了,自动的

 

7           a = c[1];  

0x8048442 <main+62>:  movzbl 0x12(%esp),%eax  

0x8048447 <main+67>:   mov    %al,0x10(%esp)  

从地址0x12取出值0x32,传给eax寄存器。


关于movzbl,文章底部有详解,说通俗点就是把(8位)byte长度的值0x32移到(32位)long长度的某地址存储空间中(此例为eax)寄存器了——此时eax中值0x00000032(前24位应该补0,因为“zero”,可以肯定后八位是0x32,就行了)

mov al把eax的低8位值0x32,即数字2,存到栈偏移地址0x10(即变量a的地址)。赋值完成

如果这些简单汇编看不懂,还感兴趣,请移步我的通俗的汇编贴


8          a = p[1];  

0x804844b <main+71>: mov 0xc(%esp),%eax  

0x804844f <main+75>: movzbl 0x1(%eax),%eax  

0x8048453 <main+79>: mov %al,0x10(%esp)    

将栈偏移地址0xc中储存的指针p(内容为指向的地址)移到eax寄存器中。  

第二句较难:

从eax中取出指针,偏移1,读取字符串中第二个字符’2’,把该(八位)地址对应的值(0x32,即数字2)存到栈偏移地址0x10(即变量a的地址)。

将eax寄存器中低8位,即0x32,传给栈偏移地址0x10中,即为给a赋值。

赋值完成



结论:可以明显看出,前者直接有目的地从栈中读取数据到寄存器eax中,后者则要先把指针值读出来,再通过指针加偏移去找需要的地址的值,根据我们关于计算机组成原理的常识,多了一次访问内存,显然效率低了。


可以看到的是,两个字符串在读取的时候同样是用数组下标的操作形式,所以和操作方式无关?

感觉不够严谨,也测试了用指针的操作形式,如下(多次测试,和上边过程变量地址恐有变化,原理相同即可):

11        a = *(c + 1);
1: x/i $pc
=> 0x8048487 <main+83>:    lea    0x21(%esp),%eax
(gdb)
0x0804848b    11        a = *(c + 1);
1: x/i $pc
=> 0x804848b <main+87>:    movzbl 0x1(%eax),%eax
(gdb)
0x0804848f    11        a = *(c + 1);
1: x/i $pc
=> 0x804848f <main+91>:    mov    %al,0x20(%esp)

取出C的地址

取出——C的地址+1偏移量所指向的——值

将该值传递给变量a

(gdb)
12        a = *(p + 1);
1: x/i $pc
=> 0x8048493 <main+95>:    mov    0x1c(%esp),%eax
(gdb)
0x08048497    12        a = *(p + 1);
1: x/i $pc
=> 0x8048497 <main+99>:    movzbl 0x1(%eax),%eax
(gdb)
0x0804849b    12        a = *(p + 1);
1: x/i $pc
=> 0x804849b <main+103>:    mov    %al,0x20(%esp)

取出p指向的“字符串常量”的首地址

取出——p指向的“字符串常量”的首地址+1偏移量所指向的——值

将该值传递给变量a

两者唯一区别就是指令lea和mov,原因就是p指向的是“常量区”,只需要p的内容(即目标地址)即可,而c,要取自身的地址。


 


PS:没有对比堆空间的存取问题,因为涉及系统调用,指令非常多,过程非常慢。堆比栈存取慢很多是显然的了




附:


文中所谓“栈偏移地址0x10”之类,非绝对地址,皆指偏移地址,%esp是一个固定位置,偏移多少就是固定位置加多少偏移量。

=> 0x8048456 <main+34>:  movl   $0x38373635,0x25(%esp)

(gdb) print $esp

$2 = (void *) 0xbffff230

(gdb) si

0x0804845e 5      char c[] = "1234567890";

=> 0x804845e <main+42>:  movw   $0x3039,0x29(%esp)

(gdb) print $esp

$3 = (void *) 0xbffff230

0x08048465 5      char c[] = "1234567890";

=> 0x8048465 <main+49>:  movb   $0x0,0x2b(%esp)

(gdb) print $esp

$4 = (void *) 0xbffff230

 

 

movzbl:

在AT&T语法中,符号扩展和零扩展指令的格式为,基本部分"movs"和"movz"(对应Intel语法的为movsxmovzxmovzx为零扩展,即高位补零,movsx为符号扩展,即高位补符号位)

后面跟上源操作数长度和目的操作数长度。movsbl意味着movs (from)byte (to)long;movbw意味着movs (from)byte (to)word;movswl意味着movs (from)word (to)long。对于movz指令也一样。比如指令“movsbl   %al, %edx”意味着将al寄存器的内容进行符号扩展后放置到edx寄存器中。

movzx是将源操作数的内容拷贝到目的操作数,并将该值0扩展至16位或者32位。但是它只适用于无符号整数。

 他大致分为下面的三种格式:

movzx 32位通用寄存器,8位通用寄存器/内存单元

movzx 32位通用寄存器,16位通用寄存器/内存单元

movzx 16位通用寄存器, 8位通用寄存器/内存单元 

 


堆空间是程序运行时动态申请的,系统维护一个关于空闲区域的链表,从小到大按容量找,找到第一个符合要求(大于等于所需空间)的结点,分配之。

那么也不一定就全用链表,WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈,而是直接在进程的地址空间中保留一块内存,虽然用起来最不方便。但是速度快,也最灵活。

那么删除怎么删?怎么知道删多少?这个大小是系统记录的,不是问题,只管free()、delete()就成了。如果申请的少,不巧没有很合适的,分配多了的部分,系统还会释放掉,免得浪费。


你可能感兴趣的:(C语言,堆栈,存储效率,栈指针,访存)