📜  c++ 到汇编 - C++ (1)

📅  最后修改于: 2023-12-03 15:13:59.555000             🧑  作者: Mango

C++ 到汇编

前言

C++ 是一门高级编程语言,它提供了很多丰富的语法和功能,使得编写程序更方便快捷。然而,在一些场景中,我们需要掌握汇编语言,以实现更高效的代码。本文将介绍如何将 C++ 代码转换成汇编代码,并探讨一些汇编优化的技巧。

编译 C++ 代码成汇编代码

为了将 C++ 代码转成汇编,需要使用工具来进行编译。GCC 是一个常用的开源编译器,可以在 Linux 和 Mac 上使用。在 Windows 上,可以使用 MinGW 或者 Cygwin 来安装 GCC。

首先,我们需要编写一个简单的 C++ 程序来测试。下面是一个求斐波那契数列的程序:

#include <iostream>

int fib(int n) {
    if (n <= 1) {
        return n;
    }
    return fib(n - 1) + fib(n - 2);
}

int main() {
    std::cout << fib(10) << std::endl;
    return 0;
}

然后,我们可以使用下面的命令来将其编译成汇编:

$ g++ -S fib.cpp

这里的 -S 参数表示输出汇编代码而不是可执行文件。执行该命令后,会生成一个名为 fib.s 的文件,其中包含了编译后的汇编代码。

分析汇编代码

接下来,我们来看一下编译后的汇编代码。以下是 fib.s 文件的部分内容:

_Z3fibi:
.LFB0:
    .cfi_startproc
    cmpl    $1, %edi
    jg  .L2
    movl    %edi, %eax
    ret
.L2:
    subl    $1, %edi
    call    _Z3fibi
    movl    %eax, %edx
    subl    $1, %edi
    call    _Z3fibi
    addl    %edx, %eax
    ret
    .cfi_endproc

.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
    subq    $16, %rsp
    movl    $10, %edi
    call    _Z3fibi
    movl    %eax, %esi
    leaq    .LC0(%rip), %rdi
    movl    $0, %eax
    call    printf@PLT
    movl    $0, %eax
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc

可以发现,汇编代码中的各个指令都是直接操作 CPU 寄存器的,例如 movl 就是将数据从内存中移动到 CPU 寄存器中。除了寄存器以外,还有其他汇编代码中的重要结构,例如 .LFB0.cfi_startproc.cfi_endproc 等等,这些结构可以帮助汇编代码和高级语言代码的对应关系。

在深入分析汇编代码时,还需要了解一些特殊的寄存器,例如:

  • eax 通常用于返回值。
  • ebxecxedxesiedi 用于一般用途寄存器。
  • ebp 用于形参、局部变量和临时值的堆栈指针。
  • esp 用于栈顶指针。
汇编优化技巧

在编写汇编代码时,我们需要考虑一些优化技巧,以提高代码的性能。以下是一些常用的技巧:

减少内存访问

内存访问是计算机中最慢的操作之一,因此我们需要尽可能减少内存访问的次数。例如,在 C++ 中,我们可以使用局部变量缓存数组的长度,以避免多次计算数组的长度:

// 普通 C++ 代码
void func(int* arr, int len) {
    for (int i = 0; i < len; i++) {
        // ...
    }
}

// 汇编优化后的代码
void func(int* arr, int len) {
    int loop_limit = len;
    for (int i = 0; i < loop_limit; i++) {
        // ...
    }
}
循环展开

循环展开是一种常用的优化技巧,它可以避免 CPU 频繁地跳转,从而提高代码的性能。循环展开的思路是将多次迭代的循环体展开成多个对应的汇编指令。

以下是实现斐波那契数列的 C++ 代码:

int fib(int n) {
    if (n <= 1) {
        return n;
    }
    return fib(n - 1) + fib(n - 2);
}

展开成汇编代码后的样子:

_Z3fibi:
.L2:
    cmpl    $1, %edi
    jle .L3
    pushq   %rbx
    movl    %edi, %eax
    movl    $1, %ebx
.L4:
    subl    $1, %eax
    cmpl    $1, %eax
    jle .L9
    pushq   %rax
    call    _Z3fibi
    addq    $8, %rsp
    addl    %eax, %ebx
    subl    $1, %eax
    jmp .L4
.L9:
    movl    $1, %eax
    addl    %ebx, %eax
    popq    %rbx
    ret
.L3:
    movl    %edi, %eax
    ret

展开后的代码使用了循环展开技术,减少了跳转和其他负载的开销。

去除多余指令

在 C++ 中,我们通常会使用一些高级函数或语法糖来简化代码,但这些语法糖和函数调用都有一些额外的开销。例如,下面是一个使用 std::string 类型的程序:

#include <string>
#include <iostream>

int main() {
    std::string str = "hello world";
    std::cout << str << std::endl;
    return 0;
}

编译后的汇编代码会有很多额外指令:

main:
.LFB9:
    pushq   %rbp
.LCFI0:
    movq    %rsp, %rbp
.LCFI1:
    subq    $32, %rsp
.LCFI2:
    movl    $offset(_ZStL8__ioinit), %eax
    call    *__tls_get_addr@NTDLL_TSB_CALL_ONCE (421296)
    leaq    -24(%rbp), %rax
    movq    %rax, %rdi
    movq    $128849333258, %rsi
    movq    %rsi, (%rax)
    call    _ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEC1EPKcRKS3_@PLT
    leaq    -24(%rbp), %rax
    movq    %rax, %rdi
    call    _ZNSsD2Ev@PLT
    leaq    -24(%rbp), %rax
    movq    %rax, %rsi
    leaq    .LC0(%rip), %rdi
    movl    $0, %eax
    call    _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@PLT
    movl    $0, %eax
    leave
    ret

这些额外指令可以通过手动编写更底层的代码来避免。例如,我们可以手动实现一个输出字符串的库函数,从而减少不必要的代码:

#include <iostream>

void print(const char* str) {
    while (*str) {
        putc(*str, stdout);
        str++;
    }
}

int main() {
    const char* str = "hello world";
    print(str);
    return 0;
}

编译后的汇编代码如下:

main:
.LFB0:
    pushq   %rbp
    movq    %rsp, %rbp
    subq    $16, %rsp
    movl    $.LC0, %edi
    call    _Z5printPKc
    xorl    %eax, %eax
    leave
    ret
print(char const*):
.LFB1:
    pushq   %rbp
    movq    %rsp, %rbp
    subq    $16, %rsp
    movq    %rdi, -8(%rbp)
    jmp .L2
.L5:
    movq    -8(%rbp), %rdi
    addq    %rax, %rdi
    movzbl  (%rax), %ecx
    movl    %eax, %edx
    addl    $1, %edx
    movq    %rdi, %rax
    movb    %cl, (%rax)
    movq    %rdx, %rax
.L2:
    movq    -8(%rbp), %rax
    movzbl  (%rax), %ecx
    testb   %cl, %cl
    jne .L5
    leave
    ret

可以看到,手动实现的代码要比使用 std::string 类型的代码更加高效。

总结

C++ 到汇编是程序员需要了解的重要领域之一。本文介绍了如何编译 C++ 代码成汇编代码,并探讨了一些汇编优化的技巧。在编写代码的过程中,需要考虑到内存访问、循环展开和多余指令等因素,以提高代码的性能。