编译器简介

2017-09-17  本文已影响75人  守拙圆

—— 在 Siri 出现以前如何与计算机对话

译者:penghuster
作者:Nicole Orchard
原文:An Intro to Compilers


简单来说,编译器是一个翻译其他程序的程序。传统的编译器把源码翻译为电脑可识别的机器码。(一些编译器也可将源码翻译为另外一种编程语言。这类编译器被称为源码到源码的翻译器或转译器。)LLVM 是一个使用非常广泛的编译器项目, 由许多模块化的编译工具组成。

传统的编译器设计包括以下三部分:

你好,编译器👋

下面是一个简单的 c 程序,打印 “Hello, Compiler!”到标准输出。C 语法是容易阅读的,但是电脑却不知道如何处理它。我将通过编译的 3 个阶段,将该程序翻译为可执行的机器码。

// compile_me.c
// Wave to the compiler. The world can wait.

#include <stdio.h>

int main() {
  printf("Hello, Compiler!\n");
  return 0;
}

The Frontend

如上文所述,clang 是 LLVM 提供的 C 系列语言的前端工具。clang 由 c 前处理程序、词法分析程序、语法分析程序、语义分析程序和 IR 生成程序。

; llvm_ir.ll

@.str = private unnamed_addr constant [18 x i8] c"Hello, Compiler!\0A\00", align 1

define i32 @main() {
  %1 = alloca i32, align 4 ; <- memory allocated on the stack
  store i32 0, i32* %1, align 4
  %2 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([18 x i8], [18 x i8]* @.str, i32 0, i32 0))
  ret i32 0
}

declare i32 @printf(i8*, ...)

优化器

优化器的工作是根据它对于程序运行时行为的理解,提高代码执行效率。优化器输入 IR 输出优化后的 IR。LLVM 的优化工具—opt—的选项 -O2 将优化处理器执行该程序速度,选项 -Os 将优化改程序的大小。

比较 LLVM 前端工具生成的 LLVM IR 与执行如下指令生成的结果:
opt -O2 -S llvm_ir.ll -o optimized.ll
optimized.ll 中的主函数:

; optimized.ll

@str = private unnamed_addr constant [17 x i8] c"Hello, Compiler!\00"

define i32 @main() {
  %puts = tail call i32 @puts(i8* getelementptr inbounds ([17 x i8], [17 x i8]* @str, i64 0, i64 0))
  ret i32 0
}

declare i32 @puts(i8* nocapture readonly) 

在这个优化版本中,main 函数没有在栈上分配内存空间,因此
main 函数不占用任何内存。由于 printf 中没有用到格式化字符串,故优化代码中调用 puts 代替 printf

当然,优化器不仅仅知道何时用 printf 替换 puts,也知道适时展开循环和内联化简单计算的结果。分析如下程序,该程序两个数相加并打印结果。

// add.c
#include <stdio.h>

int main() {
  int a = 5, b = 10, c = a + b;
  printf("%i + %i = %i\n", a, b, c);
}

这是未经优化的 LLVM IR:

@.str = private unnamed_addr constant [14 x i8] c"%i + %i = %i\0A\00", align 1

define i32 @main() {
  %1 = alloca i32, align 4 ; <- allocate stack space for var a
  %2 = alloca i32, align 4 ; <- allocate stack space for var b
  %3 = alloca i32, align 4 ; <- allocate stack space for var c
  store i32 5, i32* %1, align 4  ; <- store 5 at memory location %1
  store i32 10, i32* %2, align 4 ; <- store 10 at memory location %2
  %4 = load i32, i32* %1, align 4 ; <- load the value at memory address %1 into register %4
  %5 = load i32, i32* %2, align 4 ; <- load the value at memory address %2 into register %5
  %6 = add nsw i32 %4, %5 ; <- add the values in registers %4 and %5. put the result in register %6
  store i32 %6, i32* %3, align 4 ; <- put the value of register %6 into memory address %3
  %7 = load i32, i32* %1, align 4 ; <- load the value at memory address %1 into register %7
  %8 = load i32, i32* %2, align 4 ; <- load the value at memory address %2 into register %8
  %9 = load i32, i32* %3, align 4 ; <- load the value at memory address %3 into register %9
  %10 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([14 x i8], [14 x i8]* @.str, i32 0, i32 0), i32 %7, i32 %8, i32 %9)
  ret i32 0
}

declare i32 @printf(i8*, ...)

这是优化后的 LLVM IR:

@.str = private unnamed_addr constant [14 x i8] c"%i + %i = %i\0A\00", align 1

define i32 @main() {
  %1 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([14 x i8], [14 x i8]* @.str, i64 0, i64 0), i32 5, i32 10, i32 15)
  ret i32 0
}

declare i32 @printf(i8* nocapture readonly, ...)

随着变量值的内联化,优化后的 main 函数本质上减少了 17 到 18 行代码。由于变量为常量,opt直接计算了该和。 是否很酷?

后端

LLVM 的后端工具是 llc。它输入 LLVM IR 输出机器码的过程分为三个阶段:

运行如下命令将产生机器码。
llc -o compiled-assembly.s optimized.ll

_main:
    pushq   %rbp
    movq    %rsp, %rbp
    leaq    L_str(%rip), %rdi
    callq   _puts
    xorl    %eax, %eax
    popq    %rbp
    retq
L_str:
    .asciz  "Hello, Compiler!"

该程序是 x86 汇编语言,它是计算机说出的人可阅读的语言。某些人最终也许理解了我。 🙌


版权声明:自由转载-非商用-非衍生-保持署名创意共享3.0许可证

上一篇下一篇

猜你喜欢

热点阅读