chenjigeng / blog

个人博客
278 stars 25 forks source link

编译小知识 #22

Open chenjigeng opened 3 years ago

chenjigeng commented 3 years ago

机器码

计算机直接使用的程序语言,是电脑CPU直接读取运行的机器指令,运行速度最快,但是晦涩难懂。机器指令码是用于指挥计算机应做的操作和操作数地址的一组二进制数。

缺点:与硬件设备相关,针对不同的系统架构,需要不同的机器码。

字节码

字节码是一种中间码,它比机器码更抽象,需要直译器转译后才能成为机器码的中间代码。

实现方式:通过编译器和虚拟机。编译器将源码编译成字节码,特定平台上的虚拟机将字节码转译为可以直接执行的指令。字节码的典型应用为Java bytecode。

优点:是通过高级语言编译得来的,与特定机器码无关,因此可以跨平台执行。

缺点:依赖虚拟机/解释器执行。

高级语言

高级语言(High-level programming language)是一种独立于机器,面向过程或对象的语言,如C、C++、Python、Java。

高级语言主要是相对于汇编语言、机器码等低级语言来说的,与计算机的硬件结构及指令系统无关,因此它有更强的表达能力。而且是可移植的。仅需稍作修改甚至不用修改,就可将一段代码运行在不同类型的计算机上。

高级语言如何执行

我们编程用的基本都是高级语言,计算机不能直接理解高级语言,只能理解和运行机器码,所以必须要把高级语言翻译成机器语言,计算机才能运行高级语言所编写的程序。

高级语言划分

编译型语言

编译型语言的首先将源代码编译生成机器语言,再由机器运行机器码。

比如C/C++,在运行前都需要有一个专门的编译过程,把程序转换为机器码,之后每次运行的时候,都不需要重新编译,可以直接执行编译后的机器码。

优点

程序执行效率高,一次编译,多次执行。

缺点

依赖于编译器,跨平台性能比较差:一般编译结果都会转为机器码(平台相关的代码),因此跨平台能力比较差。

过程

解释性语言

解释型语言(英语:Interpreted language)是一种编程语言类型。这种类型的程式语言,会将源码一句一句直接执行,不需要像编译语言(Compiled language)一样,经过编译器先行编译为机器码,之后再执行。这种程式语言需要利用解释器,在执行期,动态将源码逐句直译(interpret)为机器码,之后再执行。

优点

跨平台能力强

缺点

由于是在运行的时候,动态去解释源码并执行,因此效率会比较低。而且由于编译的时候,会追求快,所以对代码优化做的不是很极致。

Java 属于解释性还是编译型?

编译型

Java代码首先得先经过 java 编译器 编译为 .class(字节码)文件,如果不经过编译的话,是无法执行的。

解释性

编译后的 .class(字节码)并不能独立运行,它必须依赖于 JVM,由 JVM 来解释运行它。

所以说,java 它既具备了编译型语言的特点,又具备了解释性语言的特点。

为什么需要依赖 JVM?

主要是为了跨平台执行。

简单实战

假设我们自己实现了一门语言,叫 QuipScript。语法如下

# Welcome to this beautiful QuipScript program!

# QScript is a cool new language for building and printing a string.

# Blank lines are ignored.

# Any line beginning with `#` is a comment.

# One statement per line, three kinds of statements:

# `+` adds any following characters to the string.

# `-N` removes the last N characters from the string.

# `print` (or any line beginning with `p`) prints the current string state.

+hello

+ you

print

-3

+them

-4

+world!

-1

print

比如我们写了上面这一段程序,看起来不错,可是他没办法执行,我们需要把它转化为机器码或者转化为其他高级语言来执行它。

尝试写一个解释器

上面这段代码还没办法执行,不过好在我们的 QuipScript 的语法很简单,作为一个高端的 JS 程序员,对我们来说,手写一个解释器应该就是 有手就行。我们可以分为以下几步来做

  1. 读取源文件,将其整体作为一个字符串。
  2. 逐行去读取这个字符串
  3. 根据每一行的标识符去做执行对应的操作。由于我们语法很简单,标识符只有 "#" "+" "-" "print"
'use strict';

const SOURCE_CODE_FILENAME = './hello-world.qs'

/**

 * QuipScript Interpreter!

 * This amazing program reads in a QS file and interprets it.

 * Interpretation executes the desired behavior "live" as source code is read.

 */

const { readFileSync } = require('fs')

const sourceCode = readFileSync(SOURCE_CODE_FILENAME, 'utf8')

let string = ''

sourceCode

.split('\n') // split into lines

.forEach(line => {

        switch (line[0]) {

                case undefined: break; // empty line, do nothing

                case '#': break; // comment line, do nothing

                case '+': string += line.slice(1); break; // concat, add line to string

                case '-': string = string.slice(0, +line); break; // remove letters

                case 'p': console.log(string); break; // print string state now

                default: throw Error('unexpected token: ' + line);

        }

})

源码如上图,然后我们只需要将源码交给我们的解释器执行,就可以逐行执行,得到我们想要的结果.

如果我们在执行到一半,报错了会怎么样呢?

# Welcome to this beautiful QuipScript program!

# QScript is a cool new language for building and printing a string.

# Blank lines are ignored.

# Any line beginning with `#` is a comment.

# One statement per line, three kinds of statements:

# `+` adds any following characters to the string.

# `-N` removes the last N characters from the string.

# `print` (or any line beginning with `p`) prints the current string state.

+hello

+ you

print

-3

+them

-4

+world!

-1

print

something

执行结果如下

从执行结果可以看到,由于我们解释器是变解释边执行的,因此如果中途报错的话,也只有执行到了后才能发现。

解释器的目标

Just-in-Time (JIT) Compiler

我们上面实现的解释器,是边解释边执行的。但是,我们也可以考虑先将其编译为 JS 代码,之后在去执行这段JS代码。我们把上面的代码修改下:

"use strict";

const SOURCE_CODE_FILENAME = "./hello-world.qs";

/**

 * QuipScript Just-In-Time (JIT) Compiler!

 * This amazing program reads in a QS file, compiles it & then runs it.

 * Compilation translates source code to object code, in this case JavaScript.

 */

const { readFileSync } = require("fs");

const sourceCode = readFileSync(SOURCE_CODE_FILENAME, "utf8");

let program = `let string = '';\n`;

sourceCode

    .split("\n") // split into lines

    .forEach((line) => {

        switch (line[0]) {

            case undefined:

                break; // empty line, do nothing

            case "#":

                break; // comment line, do nothing

            case "+": // concat, add line to string

                program += `string += '${line.slice(1)}';\n`;

                break;

            case "-": // remove letters from string

                program += `string = string.slice(0, ${line});\n`;

                break;

            case "p": // print string

                program += "console.log(string);\n";

                break;

            default:

                throw Error("unexpected token: " + line);

        }

    });

// and now that we've compiled our source code (QS) to object code (JS),

// we run it (just in time!):

eval(program); // eslint-disable-line no-eval

// for debugging:

console.log("\n\nFYI, here is the compiled program:\n\n" + program);

跟上面的解释器不同的是,我们是先将源代码编译为 JS 代码。编译完成后,再去统一执行这段代码。这样做的好处就是,如果我使用了一些错误的语法的话,在编译的时候就会报错,并不会执行。

JIT 的目标

  1. 将输入的源文件尽快的转化为目标文件
  2. 尽可能是在完成转换后,执行目标文件

Ahead-of-Time (AOT) Compiler

AOT 和 JIT 的区别,其实就是编译后是否马上执行。一般我们将C/C++ 的编译器称之为 AOT,因为他们只会进行编译,而不会马上执行。

我们上文实现的解释器和 JIT编译器,每一次执行源代码的时候,都需要重新跑一遍流程,这样就会导致很多重复的工作量。那么如果我们可以简单的编译一次,之后就直接执行编译后的结果呢?这就是 AOT 编译器干的事情。

我们再来改下上面的代码

"use strict";

const SOURCE_CODE_FILENAME = "./hello-world.qs";

const OUTPUT_CODE_FILENAME = "./hello-world.js";

/**

 * QuipScript Compiler!

 * This amazing program reads in a QS file, compiles it & then outputs it.

 * Compilation translates source code to object code, in this case JavaScript.

 */

const { readFileSync, writeFileSync } = require("fs");

const sourceCode = readFileSync(SOURCE_CODE_FILENAME, "utf8");

let program = `let string = '';\n`;

sourceCode

    .split("\n") // split into lines

    .forEach((line) => {

        switch (line[0]) {

            case undefined:

                break; // empty line, do nothing

            case "#":

                break; // comment line, do nothing

            case "+": // concat, add line to string

                program += `string += '${line.slice(1)}';\n`;

                break;

            case "-": // remove letters from string

                program += `string = string.slice(0, ${line});\n`;

                break;

            case "p": // print string

                program += "console.log(string);\n";

                break;

            default:

                throw Error("unexpected token: " + line);

        }

    });

// and now that we've compiled our source code (QS) to object code (JS),

// we output it so the user can run it in the future:

writeFileSync(OUTPUT_CODE_FILENAME, program);

// for debugging:

console.log("\n\nCompiled to JS file. For reference:\n\n" + program);

这代码基本上跟 JIT 是一样的,唯一的区别就是在最后,对于 JIT 来说,最后会执行编译后的代码。而对于 AOT 来说,他会将最后生成的代码输出成一个文件。

之后我们只需要直接执行AOT 生成的 js 文件,就可以达到与 解释器/JIT 一样的效果。

优点

Optimizing AOT Compiler

由于 JIT 是在运行时去完成编译,因此它对编译时间的要求就比较高。而 AOT 编译后并不急着运行,因此我们就可以在 AOT 上多花点时间去优化目标代码。

我们上一个 AOT 的输出代码是这样的:

let string = '';

string += 'hello';

string += ' you';

console.log(string);

string = string.slice(0, -3);

string += 'them';

string = string.slice(0, -4);

string += 'world!';

string = string.slice(0, -1);

console.log(string);

然而我们可以发现,这里其实有很多逻辑我们都可以在编译的时候去做优化,因此我们可以改进下 AOT 的代码

"use strict";

const SOURCE_CODE_FILENAME = "./hello-world.qs";

const OUTPUT_CODE_FILENAME = "./hello-world.js";

/**

 * QuipScript Optimizing Compiler!

 * This program reads a QS file, compiles it, optimizes it, & then outputs it.

 * Compilation translates source code to object code, in this case JavaScript.

 */

const { readFileSync, writeFileSync } = require("fs");

const sourceCode = readFileSync(SOURCE_CODE_FILENAME, "utf8");

let program = "";

let string = "";

sourceCode

    .split("\n") // split into lines

    .filter((line) => line && line[0] !== "#") // remove blank lines and comments

    .forEach((line) => {

        switch (line[0]) {

            case "+": // concat, add line to string

                string += line.slice(1);

                break;

            case "-": // remove letters from string

                string = string.slice(0, +line);

                break;

            case "p": // print string

                program += `console.log('${string}');\n`;

                break;

            default:

                throw Error(`unexpected token: ${line}`);

        }

    });

// and now that we've compiled our source code (QS) to object code (JS),

// we output it so the user can run it in the future:

writeFileSync(OUTPUT_CODE_FILENAME, program);

// for debugging:

console.log("\n\nCompiled to JS file. For reference:\n\n" + program);

优化后,我们就可以得到这样的编译结果

console.log('hello you');

console.log('hello world');

当然,要做这个优化的话,肯定就需要付出比 JIT 更多的时间,但最终生成的JS代码在实际运行时甚至更有效。

AOT 的目标

常见的优化