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 <[email protected]>

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语法的为movsx和movzx,movzx为零扩展,即高位补零。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()就成了。

假设申请的少,不巧没有非常合适的,分配多了的部分,系统还会释放掉,免得浪费。

时间: 2024-08-03 10:02:25

Linux C存取效率对照——堆、栈、常量区的相关文章

Java堆/栈/常量池以及String的详细详解(转)------经典易懂系统

一:在JAVA中,有六个不同的地方可以存储数据: 1. 寄存器(register). 这是最快的存储区,因为它位于不同于其他存储区的地方——处理器内部.但是寄存器的数量极其有限,所以寄存器由编译器根据需求进行分配.你不能直接控制,也不能在程序中感觉到寄存器存在的任何迹象. ------最快的存储区, 由编译器根据需求进行分配,我们在程序中无法控制. 2. 堆栈(stack).位于通用RAM中,但通过它的“堆栈指针”可以从处理器哪里获得支持.堆栈指针若向下移动,则分配新的内存:若向上移动,则释放那

Jvm(27.14.2),理解升级---堆,栈,方法区

看完GC的回收策略之后,我们再来看一下堆,栈,方法区的交互. 首先我们必须牢记一句话,栈是堆和方法区的引用,学的越多对这句话的理解要越深. 1,这里的堆主要是对局部变量表来说的. 2,栈的内存地址是远远小于堆得,因为在栈中只是对象的引用. 3,gc回收只是回收堆内存,不用考虑栈的内存,因为栈的数据结构就是一旦出栈就会释放的. 栈也是JAVA虚拟机自动管理的,(不是由gc)栈类似一个集合(不过是有固定的容量),是由很多元素(专业术语:栈帧)组合起来的,在我们码代码的时候,每调用一个方法,在运行的时

堆 栈 常量池

a.在函数中定义的一些基本类型的变量数据和对象的引用变量都在函数的栈内存中分配.  当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当该变量退出该作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用. b.堆内存用来存放由new创建的对象和数组. 在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理.  在堆中产生了一个数组或对象后,还可以在栈中定义一个特殊的变量,让栈中这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了

java 堆 栈 常量池

java 堆中保存new 出来的对象(每个对象都包含一个与之对应的class的信息,[class信息存放在方法区]),堆中分配的内存,有虚拟机的自动垃圾回收器管理,栈内存只对其所属线程可见. java 栈中保存一些基本数据类型 (int,long,byte,double,float,char,boolean,short)和引用变量,堆内存对所有线程可见. 异常错误 如果栈内存没有可用的空间存储方法调用和局部变量,JVM会抛出java.lang.StackOverFlowError.而如果是堆内存

Java运行时数据区域(堆 栈 方法区 常量池)

运行时数据区域 (1)程序计数器(program counter register) 一块较小的内存空间 当前线程所执行的字节码的行号指示器,字节码解释器在工作的时候就是通过改变程序计数器的值来选取下一跳要执行的指令 多线程环境下,线程轮流切换执行,程序计数器保证线程切换之后能恢复到正确的位置 每个线程都有一个独立的程序计数器 线程私有 没有任何异常 (2)虚拟机栈(stack) 虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的过程中都会创建一个栈帧,用于存储局部变量表.操作数栈.动

运行时数据区域(堆 栈 方法区 常量池)和内存分配策略

内存管理 内存分配和内存释放 内存分配由程序完成,内存释放由GC完成 运行时数据区域 (1)程序计数器(program counter register) 一块较小的内存空间 当前线程所执行的字节码的行号指示器,字节码解释器在工作的时候就是通过改变程序计数器的值来选取下一跳要执行的指令 多线程环境下,线程轮流切换执行,程序计数器保证线程切换之后能恢复到正确的位置 每个线程都有一个独立的程序计数器 线程私有 没有任何异常 java方法,程序计数器的值为当前正在执行的虚拟机字节码指令的地址 nati

c进阶1(堆,栈,静态区,代码区)

一.内存四大区域 1.栈 先进后出 栈的大小固定,默认1M,可以编译的时候设置,超出则溢出 变量离开作用范围后,栈上的数据会自动释放 栈是连续的,向上增长 #include<stdio.h> #include <stdlib.h> void go(); void main() { void *p1 = malloc(10); //p1,p2栈上 void *p2 = malloc(20); //00A0F914, 00A0F908 printf("%p,%p",

java 堆 栈 方法区的简单分析

Java里的堆(heap)栈(stack)和方法区(method) 基础数据类型直接在栈空间分配, 方法的形式参数,直接在栈空间分配,当方法调用完成后从栈空间回收.   引用数据类型,需要用new来创建,既在栈空间分配一个地址空间,又在堆空间分配对象的类变量 . 方法的引用参数,在栈空间分配一个地址空间,并指向堆空间的对象区,当方法调用完成后从栈空间回收.局部变量 new 出来时,在栈空间和堆空间中分配空间,当局部变量生命周期结束后,栈空间立刻被回收,堆空间区域等待GC回收. 方法调用时传入的

JVM堆 栈 方法区详解

一.栈 每当启用一个线程时,JVM就为他分配一个JAVA栈,栈是以帧为单位保存当前线程的运行状态 栈是由栈帧组成,每当线程调用一个java方法时,JVM就会在该线程对应的栈中压入一个帧 只有在调用一个方法时,才为当前栈分配一个帧,然后将该帧压入栈 栈帧帧是由局部变量区.操作数栈和帧数据区组成 java栈上的所有数据都是私有的,任何线程都不能访问另一个线程的栈数据 局部变量区  调用方法时,类型信息确定此方法局部变量区和操作数栈的大小 局部变量区被组织为以一个字长为单位.从0开始计数的数组,类型为