编译器,优化,及目标代码生成.

本文介绍从源文件开始到目标代码生成的过程.

  • 首先,是我们每天都要接触的源文件.源文件是由纯ASCII或者其他字符集组成的文本,由程序员使用文本编辑器创建.它有以下的几种形式
    • 纯文本.好处是易于维护.并且可以使用处理文本文件的程序来处理源文件.
      • 这个就是我们最常见的源代码形式了.甚至可以使用notepad来处理源文件!

    • 记号化的源文件.使用专门的单字节"记号"值来表示源文件中的保留字等语句元素.
      • 好处1:尺寸小,由于使用单字节的符号来"压缩"多字符的保留字,所以比纯文本源文件小.

      • 好处2:由于识别单字节比识别多字符高效,所以使得解释器效率更高.

      • 好处3:基于记号化的源文件,也很容易的构建出原先(或者类似的)文本源文件.

      • 缺陷1:会丢弃空白区域.

      • 缺陷2:引入了专有的格式.不方便使用普通的文本文件处理程序来进行处理.

    • 专门的源文件格式.
      • 使用图形元素来表示程序要完成的指令.如Delphi等.

  • 源文件编写完成后,要在计算机上运行,需要使用计算机处理程序来进行处理.主要有以下几种的处理程序.
    • 纯解释器.
      • 工作方式:直接工作于文本源文件.持续扫描源文件,将其作为字符串进行处理.

      • 问题:没有效率.在识别"词素(lexeme)"时的耗时,甚至会大于程序实际的执行时间.

      • 适用场景:1)期望语言处理程序非常紧凑时.2)脚本语言和超高级语言(在程序执行期间将源代码当做字符串操作).

    • 解释器
      • 工作方式:运行时,执行源文件的替身.对符号化的源文件(对文本源文件需要进行符号化的转换)进行操作,省去了执行时的词素分析.

      • 问题:无法将字符串当做程序语句进行执行.

    • 编译器
      • 工作方式:在运行之前,先将源文件转换为可执行的机器码.

      • 特性:生成的机器指令可以由CPU直接执行.所以运行时,所有的资源都可以用来执行机器码,而不必浪费时间解析源文件.

      • 问题:源文件到机器码的转换是单向的,很难进行逆向的操作.

    • 增量编译器
      • 工作方式:编译器和解释器的交集.将源代码转化为某种中间形式.中间形式与原始文件的联系不紧密,而通常是"虚拟(假想)机器"的机器码,而不是可以执行在物理CPU上执行的机器码.然后对虚拟机编写解释器,然后它来实际执行代码.

      • 优势:虚拟机具有可移植性.而真正的机器码只能在特定的CPU上执行.这样是JAVA宣称的"一次编译,到处运行".

      • 可以使用"即时编译"来提升性能.
        • 在运行期间,大部分的事件都花在获得并解析虚拟机代码的操作上了.程序执行期间,解释过程会反复进行.

        • 在首次遇到虚拟指令时,就将虚拟代码转换为实际的机器码,然后在后续过程需要同一语句时省去解释过程.

        • 这样就是很多.NET程序首次启动很慢,而之后速度会变得很快的原因了.

  • 语言处理程序在转化源文件到目标代码时,需要经过几个阶段.


    • 词法分析
      • 扫描程序负责读取从源文件中找到的字符和字符串数据,并将这些数据分类为表示源文件词素项的记号.词素项就是源文件中的字符序列(程序的原子级组件).对每个词素创建一个小的数据包,即”记号”并将此数据包发往语法分析程序.

      • 此过程可选,语法分析可直接工作于源文件,但是这样的话,语法分析程序在处理源文件时就得多次引用某记号.通过预处理源文件,将其分类为一系列的记号后,编译过程就更高效.

      • 将字符串词素转化成较小的记号包,扫描程序就能让语法分析程序将记号按整数值对待,而无需依照字符串进行操作.CPU处理小值整数比字符串高效,而且语法分析要多次引用记号数据,这样省去很多时间.

      • 在分析阶段多次扫描每个记号的语言系统只有纯解释器.所以其很慢.

    • 语法分析

      • 编译器的一部分.负责检查源程序的语法语义是否正确,且将记号流(即源代码)重组为更复杂的数据结构,使之表示程序的意思即语义.

      • 扫描程序和语法分析程序一般以线行方式从头到尾加工源文件,编译器通常只读源文件一次,随后通过构建表示源代码的数据结构(AST:抽象语法树)来随即访问引用源文件体.

      • 使得方便地引用程序的不同部分.减轻代码生成和优化阶段的负担.

    • 中间代码生成.
      • 不直接转化为本机机器码的原因.
        • 1)编译器的优化阶段可以进行某些类型的优化(对中间代码形式操作较容易).

        • 2)许多编译器是跨平台的,能生成工作于不同CPU架构的机器码.然后将所有不依赖于特定CPU的动作放到中间代码生成阶段,只需生成这些代码一次,而跟特定CPU相关的放到各个执行PC上来进行.

    • 优化阶段
      • 通常是消除AST中不必要的项目.

      • 问题:效率的定义是程序对某些资源的最小占用.主要的资源是内存(体积)和CPU周期(速度).问题在于:朝着某个目标优化,可能与朝着其他目标的优化措施发生冲突.所以优化是个折中过程,需要牺牲某些次要目标来换取某个合理的结果.

      • 优化对编译时间的影响:编译器还得在合理的时间内产生可执行的结构,这是个”NP-完全问题(NP-complete problem):正确解显然是存在的,但必须经过海量的计算.计算量之大,使得求出正确解没有可行性.所以我们只能找出可行的近似解”.解决NP-问题所需的时间与输入量呈指数关系.编译器使用启发式和案例性算法来确定生成应采取的转换.

      • 基本块,可归约代码和优化.
        • 优化时会随着贯穿程序的控制流跟踪变量值,该过程称为”数据流分析”.以确定变量:何处尚未初始化,何时包含某值,何时不再被使用,何时对变量值一无所知.优化是固有的缓慢过程

        • 在时间上的让步:在进行下步前,对一段代码找寻较多可能的优化办法.因此编程风格很容易搞糊涂编译器,使得无法产生最优化.

        • 将源代码划分为”基本块”的序列.基本块就是顺序执行的机器指令序列,除了块的开始和结束位置外没有任何分支.确定基本块的起止:只要有地方时条件分支/跳转,无条件跳转,或调用指令就表明该基本块到头了.基本块包含将控制发往别处的指令,该指令后是另一基本块的开始处.

        • 基本块使得跟踪基本块内对变量及其他数据的操作变得方便.

        • 当两个基本块的路径汇集到同一代码流时,某变量可能的值的数目就会随着if语句数量呈指数增长.

        • 即使来自于基本块的路径汇集到一起,程序也会经常对变量赋新值,因此编译器不必跟踪旧信息.编译器假设程序不会在每个路径内都对变量赋予不同值,其内部数据结构也据此构建.

        • 合理的程序会产生”可归约的流程图”:程序控制流的图形化表示.图内,各基本块的结束处与其传送控制的基本块开始处用箭头连接.

        • 不用Goto的程序是可归约的.可归约的程序内的基本块可缩写成提纲方式,后者的块继承了基本块内在的特性.提纲方式使得优化程序只需应对数目较少的基本块,而不是大量的语句.

      • 编译器常见的优化措施.
        • 常量折叠:在编译时期就计算出常量或子表达式的值,而不是运行时才发送代码去计算结果.

        • 常量传播:如果能确定在前面的代码中某变量被赋值以常量,那么将要访问该变量的地方替换成常量值.

        • 死代码消除:删除那些与特定源代码语句(其结果从未被使用过,或者条件块从不为True)关联的目标码.

        • 公共子表达式消除:如果子表达式中变量值并未改变,就将其缓存,而无需在下次出现时重复计算该表达式的值.

        • 强度消弱:采用与源代码不同的运算符(开销小)来直接计算出结果.

        • 归纳:许多表达式中某个变量值完全依赖于其他某个变量,这时省去对其新值的计算,或者在循环内将变量值计算与表达式计算合并.

        • 循环不变量:不会随着每轮循环改变的表达式,只要在循环外一次计算出其结果,然后”代码移动”将其移出循环体.

      • 控制编译器的优化.
        • 默认时,编译器不采取任何优化措施,必须明确地告诉编译器执行某些优化.理由:
          • 1)优化是漫长的过程;

          • 2)优化后许多调试器不能很好地工作;

          • 3)编译器的大部分缺陷都在优化程序中.”优化”对不同的人有不同的意义,所以有各种方面的优化.

  • 编译器的输出.也就是语言处理程序最终将源文件转换的结果.大部分的输出都不是特定CPU能够执行的.
    • 1)输出高级语言代码(可读,易验证,跨平台,可利用其他高级语言编译器的优化程序;但耗费更多的处理时间,高级语言很难高效地映射到底层机器码).

    • 2)输出汇编语言代码(允许嵌入”内联的汇编语言”语句来满足时间严格要求的场景);

    • 3)输出目标文件(需要链接器再加工);

    • 4)输出可执行文件.

时间: 2024-12-07 02:27:24

编译器,优化,及目标代码生成.的相关文章

深入理解JVM读书笔记四: (早期)编译器优化

10.1概述 Java 语言的 "编译期" 其实是一段 "不确定" 的操作过程,因为它可能是指一个前端编译器(其实叫 "编译器的前端" 更准确一些)把 .java 文件转变成 .class 文件的过程:也可能是指虚拟机的后端运行期编译器(JIT 编译器,Just In Time Compiler)把字节码转变成机器码的过程:还可能是指使用静态提前编译器(AOT 编译器,Ahead Of Time Compiler)直接把 *.java 文件编译成

C语言 之编译器优化

C语言的编译器会对变量和代码进行一定的优化,我们看下面这个例子. int a,b,c; a=1; b=a; c=b; 这个程序正常运行的时候会这样子:先把a指向的内存空间内放入1 再把a指向的内存空间里的数读出来放到b指向的内存空间, 最后再把b指向的内存空间里的数读出来放到c指向的内存空间里. 然而编译器要对这个程序进行一定的优化,编译的时候,直接把1放入三个内存空间中. 这样子在正常运行的时候确实是好事,但是总会有一些特殊的情况发生,比如,一个中断程序突然改变了a的值,那么会发生这样的情况:

编译器优化陷阱——全局指针多次使用异常

做程序开发一定会和编译器打交道,编译器优化可以给我们代码运行带来一定的提升,但也可能存在一些意想不到的问题.下面就是我在开发时候遇到的一个坑,希望可以给大家一些借鉴 直接上代码说话吧 1 static unsigned char* s_data = NULL; //存储一帧视频数据 2 void DoRendering () 3 { 4 // D3D11 case 5 if (s_DeviceType == kUnityGfxRendererD3D11 && EnsureD3D11Reso

C#编译器优化那点事

使用C#编写程序,给最终用户的程序,是需要使用release配置的,而release配置和debug配置,有一个关键区别,就是release的编译器优化默认是启用的. 优化代码开关即optimize开关,和debug开关一起,有以下几种组合. | 编译器开关设置 |C#IL代码质量 | JIT本地代码质量 | | ------------- |:-------------:| -----:| | /optimize- /debug-(默认) | 未优化 | 有优化 | | /optimize-

对String类型的认识以及编译器优化

Java中String不是基本类型,但是有些时候和基本类型差不多,如String b = "tao" ; 可以对变量直接赋值,而不用 new 一个对象(当然也可以用 new). Java中的变量和基本类型的值存放于栈内存,而new出来的对象本身存放于堆内存,指向对象的引用还是存放在栈内存.例如如下的代码: int  i=1; String s =  new  String( "Hello World" ); 变量i和s以及1存放在栈内存,而s指向的对象"H

一个函数返回临时对象引起的编译器优化问题

我们都知道,如果在一个函数调用另一个函数,假设是 main 函数调用 fun 函数,这个 fun 函数返回一个临时类类型变量,那么这个时候编译器就会在 main 函数申请一个空间并生成一个临时对象,通过拷贝构造函数将 fun 返回的临时变量的值拷贝到这个临时对象.我们看如下的代码: #include <iostream> #include <cstring> using namespace std; class Matrix { public: explicit Matrix(do

HotSpot模板解释器目标代码生成过程源码分析

虽然说解释执行模式是逐字逐句翻译给目标平台运行的,但这样的过程未免太过缓慢,如果能把字节码说的话做成纸条,运行时只要把对应的纸条交给目标平台就可以了,这样,执行速度就会明显提升.JVM的Hotspot虚拟机的模板解释器就是用这种方法来解释执行的.在开始分析之前,先了解一下JVM的执行方式. (1).边解释边运行,即每次解释一条字节码并运行其解释的本地代码,这种执行引擎速度相对很慢  (2).JIT(即时编译)具有更快的运行速度但是需要更多的内存,方法被第一次调用时,字节码编译生成的本地代码将会被

[Inside HotSpot] C1编译器优化:条件表达式消除

1. 条件传送指令 日常编程中有很多根据某个条件对变量赋不同值这样的模式,比如: int cmov(int num) { int result = 10; if(num<10){ result = 1; }else{ result = 0; } return result; } 如果不进行编译优化会产出cmp-jump组合,即根据cmp比较的结果进行跳转.可以使用gcc -O0查看: cmov(int): push rbp mov rbp, rsp mov DWORD PTR [rbp-20],

【工作】Proxy Server的优化 - 检测目标网站URL变化

在工作中,我在组里负责一个Proxy(代理)的Module,这个Module是针对微软的Office 365的邮件门户OWA实现,工作起来后,用户访问Office 365 OWA,无需再输入Office 365的网址,只需输入我们Proxy的地址,然后我们会将请求转送到Office 365 OWA,达到用户访问的目的,并使用户的体验如同实际访问Office 365 OWA一样. 其实我们Proxy的原理是,使用Node.js构建一个http Server,拿到client端(实际是Browser