从汇编语言看C语言多种初始化的效率

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

C语言的结果和数组多种初始化的效果,以下内容默认是64位CPU指令集,除非有特殊说明。


提示:以下是本篇文章正文内容,下面案例可供参考

一、结构体初始化

#include <string.h>
#include <stdio.h>

typedef struct
{
    char  a;
    int   b;
    short c;
}UnAlignment_T;

int main(void)
{
    UnAlignment_T   test = {0};

    printf(" test\n .a address = %#08x\n .b address = %#08x\n .c address = %#08x \n", &test.a, &test.b, &test.c);

    UnAlignment_T   mem;

    printf(" mem\n .a address = %#08x\n .b address = %#08x\n .c address = %#08x \n", &mem.a, &mem.b, &mem.c);

    memset((void*)&mem, 0xff, sizeof(UnAlignment_T));
    printf(".a = %d\n.b=%d\n.c=%d\n", mem.a, mem.b, mem.c);

    return 0;
}
我们经常有疑问:test = {0}的初始化效果是什么,是只给第一个成员变量a初始化为0,还是全部都初始化为0?从实验的答案来看,是把所有内存都初始化为0

查看汇编语言,就能看到初始化的效果:

        .file   "init.c"
        .section        .rodata
        .align 8
.LC0:
        .string " test\n .a address = %#08x\n .b address = %#08x\n .c address = %#08x \n"
        .align 8
.LC1:
        .string " mem\n .a address = %#08x\n .b address = %#08x\n .c address = %#08x \n"
.LC2:
        .string ".a = %d\n.b=%d\n.c=%d\n"
        .text
        .globl  main
        .type   main, @function
main:
.LFB0:
        .cfi_startproc
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        subq    $32, %rsp
        movq    $0, -16(%rbp)
        movl    $0, -8(%rbp)
        leaq    -16(%rbp), %rax
        leaq    8(%rax), %rcx
        leaq    -16(%rbp), %rax
        leaq    4(%rax), %rdx
        leaq    -16(%rbp), %rax
        movq    %rax, %rsi
        movl    $.LC0, %edi
        movl    $0, %eax
        call    printf
        leaq    -32(%rbp), %rax
        leaq    8(%rax), %rcx
        leaq    -32(%rbp), %rax
        leaq    4(%rax), %rdx
        leaq    -32(%rbp), %rax
        movq    %rax, %rsi
        movl    $.LC1, %edi
        movl    $0, %eax
        call    printf
        leaq    -32(%rbp), %rax
        movl    $12, %edx
        movl    $255, %esi
        movq    %rax, %rdi
        call    memset
        movzwl  -24(%rbp), %eax
        movswl  %ax, %ecx
        movl    -28(%rbp), %edx
        movzbl  -32(%rbp), %eax
        movsbl  %al, %eax
        movl    %eax, %esi
        movl    $.LC2, %edi
        movl    $0, %eax
        call    printf
        movl    $0, %eax
        leave
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc
.LFE0:
        .size   main, .-main
        .ident  "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-28)"
        .section        .note.GNU-stack,"",@progbits

调试标签:
.cfi_def_cfa_offset:
cfi:Call Frame Information
cfa:canonical Frame Address
.cfi_def_cfa_offset modifies a rule for computing CFA. Register remains the same, but offset is new. Note that it is the absolute offset that will be added to a defined register to compute CFA address.

以下语句进入main函数

pushq   %rbp          # 将栈底入栈
movq    %rsp, %rbp    # 栈底移动到当前栈顶
subq    $32, %rsp     # 栈顶向低地址移动32个字节,test + mem

以下是初始化语句,可以看到实际上只初始化了12个字节,但是却占了16个字节。

movq    $0, -16(%rbp) # 为了不移动栈顶,所以使用rbp?
movl    $0, -8(%rbp)

打印test的成员变量a、b、c的地址如下:
test
.a address = 0xf98b0e90
.b address = 0xf98b0e94
.c address = 0xf98b0e98
mem
.a address = 0xf98b0e80
.b address = 0xf98b0e84
.c address = 0xf98b0e88

从以上地址可以得出以下结论:
结构体入栈的顺序是test的c、b、a,mem的c、b、a

test结构体初始化示意图如下:
在这里插入图片描述
灰色部分未初始化,c更靠近栈底,a更靠近栈顶。按照int宽度,操作系统对所有成员进行默认对齐。

作为对比,我们可以看一下memset是如何初始化的:

leaq    -32(%rbp), %rax   # %rax指向mem的首地址
movl    $12, %edx         # %edx等于12,为结构体字节数,作为memset的第3个参数
movl    $255, %esi        # %esi等于0xff,作为memset的第2个参数
movq    %rax, %rdi        # %rdi指向mem的首地址,作为memset的第1个参数
call    memset            # 执行C LIB函数调用
从上述操作来看,memset的性能远不如,= {0}。

有关打印等相关说明见第三章

二、数组初始化

2.1 定义初始化

#include <string.h>

int main(void)
{
    int array_equalZero[1000] = {0};
    int array_memset[1000];

    memset((void*)&array_memset[0], 0, sizeof(array_memset));

    return 0;

}

对应汇编语言:

        .file   "arrayInit.c"
        .text
        .globl  main
        .type   main, @function
main:
.LFB0:
        .cfi_startproc
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        subq    $8000, %rsp
        leaq    -4000(%rbp), %rsi
        movl    $0, %eax
        movl    $500, %edx
        movq    %rsi, %rdi
        movq    %rdx, %rcx
        rep stosq
        leaq    -8000(%rbp), %rax
        movl    $4000, %edx
        movl    $0, %esi
        movq    %rax, %rdi
        call    memset
        movl    $0, %eax
        leave
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc
.LFE0:
        .size   main, .-main
        .ident  "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-28)"
        .section        .note.GNU-stack,"",@progbits

进入main函数:

pushq   %rbp
movq    %rsp, %rbp

将rsp移动至栈顶,2个1000成员的int数组。8000字节

subq    $8000, %rsp

执行array_equalZero[1000]= {0}初始化。可以看到执行了500次循环完成初始化。

leaq    -4000(%rbp), %rsi  # %rsi指向array_equalZero首地址,更靠近栈底的4000字节
movl    $0, %eax           # %eax等于0
movl    $500, %edx         # %edx等于500
movq    %rsi, %rdi         # %rdi指向array_equalZero首地址
movq    %rdx, %rcx         # %rcx保存循环次数,赋值为500,因为要执行stosq,所以要用64位寄存器
rep stosq                  # 循环执行500次,将%eax中的值拷贝到%rdi指向的内存中

调用memset对数组进行初始化

leaq    -8000(%rbp), %rax  # %rax 指向array_memset首地址
movl    $4000, %edx        # %edx等于4000
movl    $0, %esi           # %esi等于0
movq    %rax, %rdi         # %rdi指向array_memset首地址
call    memset             # 执行C LIB库函数
movl    $0, %eax
leave

memset函数源码如下:

void * __cdecl memset (
        void *dst,
        int val,
        size_t count
        )
{
        void *start = dst;

#if defined (_M_IA64) || defined (_M_AMD64)

        {
        __declspec(dllimport)

        void RtlFillMemory( void *, size_t count, char );

        RtlFillMemory( dst, count, (char)val );

        }

#else  /* defined (_M_IA64) || defined (_M_AMD64) */
        while (count--) {
                *(char *)dst = (char)val;
                dst = (char *)dst + 1;
        }
#endif  /* defined (_M_IA64) || defined (_M_AMD64) */

        return(start);
}

现在大家普遍使用的CPU都不是安腾系列,所以进入#else分支。
汇编实现的memset
我们还是来看一下arch/x86/boot/copy.s中的实现:
以下命令低32位CPU指令集,函数的参数寄存器依次:EBX、ECX、EDX、ESI、EDI
返回值放在EAX中。

GLOBAL(memset)
    pushw   %di
    movw    %ax, %di
    movzbl  %dl, %eax
    imull   $0x01010101,%eax
    pushw   %cx
    shrw    $2, %cx
    rep; stosl
    popw    %cx
    andw    $3, %cx
    rep; stosb
    popw    %di
    retl
ENDPROC(memset)

2.2 执行过程中初始化

如果复杂的结构(包含多个结构、数组)在运行过程中需要重新初始化,就不能用={0},此时用memset会比较简单。但是对于大块的内存,memset会消耗性能。不建议进行大内存块的memset。那此时需要制定内存的初始化机制:
1、结构定义一个标志位:useFlag,表明本段内存是否使用。如果需要初始化,则useFlag=0
2、保存字符串的数组,定义一个count字段,指示当前有效字符个数,如果需要初始化,则count=0,同时将arrayString[0] = ‘\0’;
3、对于整数等普通数据数组,同样设置count字段,指示当前有效数据个数,如果需要初始化,则count=0
4、对结构体中强相关的数据进行封装,对需要初始化的内容,进行赋值初始化。因为赋值初始化可以通过movw、movl等指令进行,不需要单字节循环,也比memset效率高

三、打印说明

疑问:为什么%rdi的值要用%rax来中转一下,不能直接赋值呢?

test各成员地址的打印操作如下(C库入参寄存器顺序:RDI,RSI,RDX,RCX,R8,R9):

leaq    -16(%rbp), %rax    # test先入栈,所以在更靠近栈底的16个字节,此时%rax指向a
leaq    8(%rax), %rcx      # rcx等于c的地址
leaq    -16(%rbp), %rax    # rax等于a的地址
leaq    4(%rax), %rdx      # rdx等于b的地址
leaq    -16(%rbp), %rax    # rax等于a的地址
movq    %rax, %rsi         # rsi等于a的地址
movl    $.LC0, %edi        # edi保存常量字符串.LC0的地址
movl    $0, %eax           # printf参数结束
call    printf             # 执行库调用

mem的成员地址打印如下,具体就不详细解释了。

leaq    -32(%rbp), %rax
leaq    8(%rax), %rcx
leaq    -32(%rbp), %rax
leaq    4(%rax), %rdx
leaq    -32(%rbp), %rax
movq    %rax, %rsi
movl    $.LC1, %edi
movl    $0, %eax
call    printf

以下为mem成员取值打印

movzwl  -24(%rbp), %eax   # -24(%rbp)指向c,取出16bits数存入%eax,高位补0
movswl  %ax, %ecx         # 从%ax取出16bits数,将其存取%ecx,高位补符号位
movl    -28(%rbp), %edx   # %edx等于b
movzbl  -32(%rbp), %eax   # -32(%rbp)指向a,取出8bits数存入%eax,高位补0
movsbl  %al, %eax         # 从%al取出8bits数,将其存入%eax,高位补1
movl    %eax, %esi        # %esi等于a
movl    $.LC2, %edi       # %edi等于常量字符串.LC2
movl    $0, %eax          # 变长参数结束
call    printf            # 执行C LIB调用
疑问:是否可以直接:movswl -24(%rbp), %ecx ?

恢复栈顶,并出栈

movl    $0, %eax
leave

leave等价于(AT&T汇编leave指令.)

movl %ebp, %esp
popl %ebp
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值