0%

指令级并行的概念

提高处理速度:

  • 提高主频【高到一定程序,发热能耗都会升高】
  • 并行工作:多个部件同时工作

理想流水线的CPI加上各类停顿的时钟周期数:
$$
CPI_{流水线}=CPI_{理想}+停顿_{结构冲突}+停顿_{数据冲突}+停顿_{控制冲突}
$$

需要解决的具体问题

相关和流水线冲突

相关是程序固有的一种属性,它反映了程序中指令之间的相互依赖关系。相关是引起冲突的主要原因(并非唯一?结构冲突无对应相关)。因此消除相关是减少冲突停顿的一种有效方法。

相关是引发冲突的主要原因,其中数据相关与名相关可能导致数据冲突,而控制相关可能导致控制冲突。

相关的两类解决方案

  • 保持相关,但避免发生冲突

    • 指令调度
  • 通过代码变换,消除相关

    • 寄存器重命名

指令调度分类

  • 静态调度

    • 依靠编译器对代码进行静态调度,以减少相关和冲突。
    • 它不是在程序执行的过程中、而是在编译期间进行代码调度和优化。
    • 通过把相关的指令拉开距离来减少可能产生的停顿。
  • 动态调度

    • 在程序的执行过程中,依靠专门硬件对代码进行调度,减少数据相关导致的停顿。
    • 优点:
      • 能够处理一些在编译时情况不明的相关(比如涉及到存储器访问的相关),并简化了编译器;
      • 能够使本来是面向某一流水线优化编译的代码在其它的流水线(动态调度)上也能高效地执行。
      • 以硬件复杂性的显著增加为代价

经典(顺序)流水线的局限性

由于指令是按序流出和按序执行的。

例如:

1
2
3
DIV.D	F4F0F2
ADD.D F10,F4F6
SUB.D F12,F6,F14

假设DIV(除法)指令执行时间(EX)是10个时钟周期,ADD(加法)与SUB(减法)分别为1个,(由于除法与加法之间的数据相关)那么ADD必须等待9个时钟周期才能进入EX阶段。在我们学过的顺序流水线中,由于ID部件被ADD指令占据,SUB指令只有在ADD指令流入EX(执行)阶段后才能进入ID阶段,即其也需要等待9个时钟周期。然而,如果这些指令不用按顺序执行,则SUB(减法)无需等待。

SUB指令需要等待的原因:在ID阶段会检测结构冲突与数据冲突,当发现冲突后,导致冲突的指令中较晚进入流水线的那条(ADD指令),将冻结在流水寄存器中,不在向前流动。由于流水寄存器只能存放一条指令,一旦这条指令受阻,其后的指令(SUB指令)都将停顿。

解决方法:允许指令乱序执行

指令乱序执行

将ID段分为两个阶段:流出(IS)和读操作数(RO)。IS阶段执行指令译码,检查是否存在结构冲突;RO阶段等待数据冲突消失,再进行读操作数的操作。

两个方法:

  • 引入指令缓冲区:不能流出的指令就在缓冲区里排队等待,直到冲突消除
  • 部署更多的执行部件(这里的执行部件是广义的执行,包括:运算器、访存设备等),使得多条指令能都同时执行或访存

乱序执行的问题

WAR冲突和WAW冲突

乱序执行可能会导致WAR冲突和WAW冲突。

可以使用寄存器重命名来消除冲突。值得注意的是,寄存器的数量是有限的,更多的寄存器会导致编译器变慢。

多条指令同时处于执行或访存中

乱序执行将使得多条指令处于执行当中,因此要求,具有多个功能部件、或者功能部件流水化、或者兼而有之。

复杂的异常处理

乱序指令引入的最大问题在于,异常处理比较复杂。异常可以分为精确异常与不精确异常。

  • 精确异常:如果发生异常时,处理机的现场跟严格按程序顺序执行时指令i的现场相同。

  • 不精确异常:当执行指令i导致发生异常时,处理机的现场(状态)与严格按程序顺序执行时指令i的现场不同。

    • 发生不精确异常的原因:因为当发生异常(设为指令i)时

      • 流水线可能已经执行完按程序顺序是位于指令i之后的指令;
      • 流水线可能还没完成按程序顺序是指令i之前的指令。
    • 不精确异常使得在异常处理后难以接着继续执行程序。

显然,动态调度的处理机要保持正确的异常行为。具体而言,对于一条会产生异常的指令来说,只有当处理机确切地知道该指令将被执行时,才允许它产生异常。举个例子,指令i,j顺序流入流水线。j位于条件语句中(选择执行),会引起异常但不会被执行。j被调度到i之前执行,执行过程中发生了异常(实际不会被执行,所以也不会引起异常)。这样是不允许的。

然而,即使保持了正确的异常行为,动态调度处理机仍可能发生不精确异常。 举个例子:指令i,j顺序流入流水线,指令i会导致异常,但其被调度到j之后执行,假设j执行完后直接改变了上下文,那么i产生异常时,上下文与顺序执行的上下文已经不同了。

动态分支预测

ILP(指令级并行)越多,控制相关的制约就越大,分支预测就要有更高的准确度。解决方法就是:动态分支预测。

动态分支预测

  • 在程序运行时,根据分支指令过去的表现来预测其将来的行为。
  • 如果分支行为发生了变化,预测结果也跟着改变。
  • 有更好的预测准确度和适应性。

分支预测的有效性取决于

  • 预测的准确性
  • 预测正确和不正确两种情况下的分支开销
  • 决定分支开销的因素:
    • 流水线的结构
    • 预测的方法
    • 预测错误时的恢复策略等

目标与关键问题

  • 采用动态分支预测技术的目标【只有尽快做到下面两点,才能避免控制相关造成流水线停顿】

    • 预测分支是否成功
    • 尽快找到分支目标地址(或指令)(避免控制相关造成流水线停顿)
  • 需要解决的关键问题

    • 如何记录分支的历史信息,要记录哪些信息?
    • 如何根据这些信息来预测分支的去向,甚至提前取出分支目标处的指令

预测错误时的处理方法

在预测错误时,要作废已经预取和分析的指令,恢复现场,并从另一条分支路径重新取指令。

分支历史表BHT

基本思想是使用一张表(BHT)来记录分支指令最近一次或几次的执行情况(成功还是失败),并据此进行预测。

最简单的分支历史表

只有1个预测位的分支预测表

仅记录分支指令最近一次的历史,只需要1位二进制位。其规则也很简单,一句话,上次是分支成功就认为下次分支成功,上次失败就认为下次失败。

两位预测位的分支历史表

两个位记录分支最近两次的执行情况(成功1或失败0)

研究结果表明:两位分支预测的性能与n位(n>2)分支预测的性能差不多。

操作步骤

  • 分支预测

    • 当分支指令到达译码段(ID)时,根据从BHT读出的信息进行分支预测 。
    • 若预测正确,就继续处理后续的指令,流水线没有断流。否则,就要作废已经预取和分析的指令,恢复现场,并从另一条分支路径重新取指令。
  • 状态修改。【根据状态变迁图修改状态】

BHT的作用范围

关键在于比较判定分支是否成功所需的时间和确定分支目标地址所需的时间哪个更大。

【在5段经典流水线中,判断分支是否成功和计算分支目标地址都是在ID段完成的,故BHT方法不会给该流水线带去什么好处。】

BHT技术只管了预测部份,没管预测后的处理部分。

BHT的实现

BHT可以跟分支指令一起存放在指令Cache中,也可以用一块专门的硬件来实现。

分支目标缓冲器BTB

BTB的目标是将分支的开销降为 0。具体方法是分支目标缓冲,即,将分支成功的分支指令的地址和它的分支目标地址都放到一个缓冲区中保存起来,缓冲区以分支指令的地址作为标识。

BTB可以看成是用专门的硬件实现的一张表格。表格中的每一项至少有两个字段。一是,执行过的成功分支指令的地址,用作该表的匹配标识。二是预测的分支目标地址。只有这两项的就是最简单的BTB。

注:在预测失败的情况下,会承受2个周期的延迟。

与BHT相比,BTB的优点在于IF周期就能找到分支地址,能够将分支成功且预测准确时的分支开销降到0。

改进BTB

1、在分支目标缓冲器中增设一个至少是两位的“分支历史表”字段

2、在表中对于每条分支指令都存放若干条分支目标处的指令,就形成了分支目标指令缓冲器。

这里,BTB中不在保存分支目标地址,而是保存分支目标指令。

为什么存指令,而非指令地址呢?因为IF是通过指令地址取指令,分支成功,程序的空间局部性spatial locality被破坏,取指令的时延很可能会增加

多指令流出技术

多指令流出技术通过降低理想CPI,以改善实际CPI。

多流出处理机的两种基本风格

超标量

  • 在每个时钟周期流出的指令条数不固定,依代码的具体情况而定。(有个上限)
  • 设这个上限为n,就称该处理机为n-流出。
  • 可以通过编译器进行静态调度,也可以基于Tomasulo算法进行动态调度。

超标量结构对程序员是透明的,处理机能自己检测下一条指令能否流出,不需要由编译器或专门的变换程序对程序中的指令进行重新排列

即使是没有经过编译器针对超标量结构进行调度优化的代码或是旧的编译器生成的代码也可以运行,当然运行的效果不会很好【若要想达到很好的效果,方法之一是:使用动态超标量调度技术。】

超长指令字VLIW

  • 在每个时钟周期流出的指令条数是固定的,这些指令构成一条长指令或者一个指令包。
  • 指令包中,指令之间的并行性是通过指令显式地表示出来的。
  • 指令调度是由编译器静态完成的。

基于静态调度的超标量流水线技术

指令按序流出,在流出时进行冲突检测。由硬件检测当前流出的指令之间是否存在冲突以及当前流出的指令与正在执行的指令是否有冲突。

超长指令字技术

超长指令字技术把能并行执行的多条指令组装成一条很长的指令,成为一个指令字。这条指令通常为100多位到几百位。为了支持多条指令的同时执行,需要设置多个功能部件。指令字被分割成一些字段,每个字段称为一个操作槽,直接独立地控制一个功能部件。在超长指令字处理机中,在指令流出时不需要进行复杂的冲突检测,而是依靠编译器全部安排好了。

问题

  • 程序代码长度增加了。原因在于,为提高并行性而进行的大量的循环展开;此外,指令字中的操作槽也并非总能填满。 对于这个问题,可以通过采用指令共享立即数字段的方法,或者采用指令压缩存储、调入Cache或译码时展开的方法予以解决。

  • 采用了锁步机制。任何一个操作部件出现停顿时,整个处理机都要停顿。

  • 机器代码的不兼容性,一旦体系结构发生了变化,代码就必须重新编译。

超流水线处理机

超流水线处理机的基本概念:将每个流水段进一步细分,这样在一个时钟周期内能够分时流出多条指令。这种处理机称为超流水线处理机。

对于一台每个时钟周期能流出n条指令的超流水线计算机来说,这n条指令不是同时流出的,而是每隔1/n个时钟周期流出一条指令。实际上该超流水线计算机的流水线周期为1/n个时钟周期。

指令调度和循环展开

指令调度的基本方法

指令调度一般由编译器完成。通过找出不相关的指令序列,让它们在流水线上重叠并行执行。

编译器做指令调度时,通常会收两个方面的制约。一是程序固有的指令级并行,二是流水线功能部件的延迟。

循环展开的基本概念和方法

循环展开是把循环体的代码复制多次并按顺序排放, 然后相应调整循环的结束条件。它是开发循环级并行的有效方法。

循环展开和指令调度的注意事项

(1)保证正确性

(2)注意有效性

(3)使用不同的寄存器

(4)删除多余的测试指令和分支指令,并对循环结束代码和新的循环体代码进行相应的修正。

(5)注意对存储器数据的相关性分析

(6)注意新的相关性

流水线的基本概念

  • 流水线的段(级):流水线中的每个子过程及其功能部件称为流水线的级或段,段与段相互连接形成流水线
  • 流水线的瓶颈:执行时间最长的段
  • 流水线的深度:流水线的段数称为流水线的深度
  • 流水线的通过时间:第一个任务从进入流水线到流出结果所需的时间。
  • 排空时间:最后一个任务从进入流水线到流出结果所需的时间。

流水线不适用的场景:

  • 只有一个任务需要执行。【任务并没有在更短的时间内完成,反而有可能会使得单个任务的执行时间变长】

  • 多个稀疏任务(来的不够密集)。【使用流水线并不能提升工作效率(执行这些任务的有效时间并没有缩短)】

  • 多个不同任务。【多个密集任务,但各任务的子过程完全无重合(不能使用同一部件处理)】

    但是可以通过其它并行技术加速(增加更多的不同部件,使得不同任务的不同子过程能并行执行)

流水线适用的场景:

  • 多个密集且相同或类似的任务。【流水技术适合于大量重复的时序过程,只有在输入端不断地提供任务,才能充分发挥流水线的效率。】

流水线的分类

按用于计算机系统的等级划分,流水线可分为:部件级、处理机级及处理机间流水线

  • 部件级流水线(运算操作流水线)

    把处理机中的部件分段,再把这些分段相互连接起来,使得各种类型的运算操作能够按流水方式进行。

    eg:浮点加法流水线,包含4个段:求阶差,对阶,尾数相加,规格化

  • 处理机流水线(指令流水线)

    把指令的执行过程按照流水方式处理。把一条指令的执行过程分解为若干个子过程,每个子过程在独立的功能部件中执行。

    eg:经典5段MIPS指令流水线,包含5个段:取址,译码,执行,访存,写回。

  • 处理机间流水线(宏流水线)

    把多台处理机串行连接起来,对同一数据流进行处理,每个处理机完成整个任务中的一部分。

    eg:map reduce

按流水线所完成的功能可划分为:单功能流水线与多功能流水线

  • 单功能流水线:只能完成一种固定功能的流水线
  • 多功能流水线:流水线的各段可以进行不同的连接,以实现不同的功能【某些段可以不执行,直接跳过】。
    • 静态流水线:在同一时间内,多功能流水线中的各段只能按同一种功能的连接方式工作。
    • 动态流水线:在同一时间内,多功能流水线中的各段可以按照不同的方式连接,同时执行多种功能

按流水线中是否有反馈回路,流水线技术可分为线性流水线与非线性流水线

  • 线性流水线:若流水线的各段串行连接,没有反馈回路,则其为线性流水线。再线性流水线中,数据通过流水线中的各段时,每一个段最多只流过一次。
  • 非线性流水线:若流水线中除了有串行的连接外,还有反馈回路,则其为非线性流水线。

按任务流入和流出的顺序是否相同划分:顺序流水线与乱序流水线

  • 顺序流水线:流水线输出端任务流出的顺序与输入端任务流入的顺序完全相同。每一个任务在流水线的各段中是一个跟着一个顺序流动的。

  • 乱序流水线:流水线输出端任务流出的顺序与输入端任务流入的顺序可以不同,允许后进入流水线的任务先完成(从输出端流出)。

流水线的性能指标

吞吐率TP

吞吐率通常使用缩写TP表示,是指在单位时间内流水线所完成的任务数量或输出结果的数量。
$$
TP=\frac{n}{T_k}\
n:任务数\
T_k:处理完成n个任务所用的时间
$$

CPU的吞吐率是指CPU每秒执行的指令数。

分析

假设各段时间均等:

(k段线性)流水线完成n个连续任务所需要的总时间
$$
T_k=(k+n-1)\Delta t\
k\Delta t:流水线通过时间\
(n-1)\Delta t:流水线排出时间\
实际吞吐率:TP=\frac{n}{(k+n-1)\Delta t}\
最大吞吐率:TP_{max}=\lim_{n\to\infty}{\frac{n}{(k+n-1)\Delta t}}=\frac{1}{\Delta t}\
当n趋近于无穷大时,流水线吞吐率趋近于最大值(又称流水线的最大吞吐率)\
TP=\frac{n}{(k+n-1)}TP_{max}
$$
假设各段时间不全相等:

(k段线性)流水线完成n个连续任务所需要的总时间
$$
T_k=\sum\Delta t_i+(n-1)max(\Delta t_i)\quad (i=1,2,3···k)\
\sum\Delta t_i:单个任务的执行时间(通过时间)\
(n-1)max(\Delta t_i):n-1个瓶颈段的执行时间\
实际吞吐率:TP=\frac{n}{\sum\Delta t_i+(n-1)max(\Delta t_1,\Delta t_2,···\Delta t_k)}\
最大吞吐率:TP_{max}=\frac{1}{(n-1)max(\Delta t_1,\Delta t_2,···\Delta t_k)}\
$$

流水线瓶颈问题

流水线中执行时间最长的段称为流水线的瓶颈段

瓶颈段使得流水线性能下降

解决流水线瓶颈的方法

  • 细分瓶颈段(细分流水线)
  • 重复设置瓶颈段

细分瓶颈段

就是降低:$(n-1)max(\Delta t_i)$的值。将数值大的$\Delta t_i$值进行细分,差分成数值更小的$\Delta t_i$

重复设置瓶颈段

就是在采取同时多次进行瓶颈段的操作来降低$\Delta t_i$

加速比S

完成同样一批任务,不使用流水线所用的时间与使用流水线所用的时间之比。
$$
S=\frac{T_s}{T_k}\
T_s:不使用流水线(即顺序执行)所用的时间\
T_k:使用流水线后所用的时间
$$

分析

假设各段时间均等

$$
一条k段流水线完成n个连续任务所需要的时间:T_k=(k+n-1)\Delta t\
顺序执行n个任务所需要的时间:T_s=nk\Delta t\
实际加速比:S=\frac{nk}{n+k-1}\
最大加速比:S_{max}=\lim_{n\to\infty}\frac{nk}{k+n-1}=k
$$
假设各段时间不全相等
$$
实际加速比:S=\frac{n\sum_{i=1}^{k}\Delta t_i}{\sum_{i=1}^{k}\Delta t_i+(n-1)max(\Delta t_1,\Delta t_2,···\Delta t_k)}\
$$
每段执行时间并不完全相等,那么流水线完成n个连续任务所需要的总时间Tk等于单个任务的执行时间(通过时间),加上n-1个瓶颈段的执行时间。而顺序执行这n个任务,所需要的时间为n倍的单个任务的执行时间。

效率E

流水线中的设备实际使用时间与整个运行时间的比值。

【由于流水线有通过时间和排空时间,所以在连续完成n个任务的时间内,各段并不是满负荷地工作】

分析

各段时间均等:

假设各段的效率ei 相等
$$
e_1=e_2=e_3=···=e_k=\frac{n\Delta t}{T_k}=\frac{n}{k+n-1}\
整条流水线的效率:E=\frac{e_1+e_2+e_3+···+e_k}{k}=\frac{ke_1}{k}=\frac{n}{k+n-1}\
$$

效率和其他指标的关系

当流水线各段时间相等时,流水线的效率与吞吐率成正比$E=TP·\Delta t$

流水线的效率是流水线的实际加速比S与它的最大加速比k的比值$E=\frac{S}{K}$

直观的描述:
$$
E=\frac{n个任务实际占用的时空区}{k个段总的时空区}
$$
各段时间不全相等:
$$
E=\frac{n\sum_{i=1}^{k}\Delta t_i}{K[\sum_{i=1}^{k}\Delta t_i+(n-1)max(\Delta t_1,\Delta t_2,···\Delta t_k)]}\
$$

多功能流水线性能指标

影响(多功能)流水线性能的原因

–多功能流水线在做某一种运算时,总有一些段是空闲的;

–静态流水线在进行功能切换时,要等前一种运算全部流出流水线后才能进行后面的运算;

–运算之间存在关联,后面有些运算要用到前面运算的结果;

流水线的工作过程有建立与排空部分。

eg:设在下图所示的静态流水线上计算的吞吐率、效率和加速比

选择适合于流水线工作的算法

先计算A1+B1、A2+B2、A3+B3和A4+B4;再计算(A1+B1)×(A2+B2)和(A3+B3)×(A4+B4);然后求总的乘积结果。

流水线设计中的重要问题

瓶颈问题

  • 理想情况下,流水线在工作时,其中的任务是同步地每一个时钟周期往前流动一段。
  • 当流水线各段不均匀时,机器的时钟周期取决于瓶颈段的延迟时间。
  • 在设计流水线时,要尽可能使各段时间相等。

流水线的额外开销

  • 流水寄存器需要建立时间和传输延迟

    • 建立时间:在触发写操作的时钟信号到达之前,寄存器输入必须保持稳定的时间。
    • 传输延迟:时钟信号到达后到寄存器输出可用的时间。
  • 时钟偏移开销

    • 流水线中,时钟到达各流水寄存器的最大差值时间。(时钟到达各流水寄存器的时间不是完全相同)

由流水线性能与开销得到的重要结论

  • 流水线并不能减少(而且一般是增加)单条指令的执行时间,但却能提高吞吐率。
  • 增加流水线的深度(段数)可以提高流水线的性能。
  • 流水线的深度受限于流水线的额外开销。
  • 当时钟周期小到与额外开销相同时,流水已没意义。因为这时在每一个时钟周期中已没有时间来做有用的工作。【所以,流水段的段数越多不一定就越好】

单功能非线性流水线的调度

基本概念

  • 启动距离:向一条非线性流水线的输入端连续输入两个任务之间的时间间隔称为非线性流水线的启动距离。

  • 禁用启动距离:会引起非线性流水线功能段使用冲突的启动距离则称为禁用启动距离。

启动距离和禁用启动距离一般都用时钟周期数来表示,是一个正整数。

除去禁用启动距离外,其它的启动距离都是可用的,这也意味着,若两个任务进入非线性流水线的时间间隔不是禁用启动距离,那么这两个任务之间不会产生冲突

  • 预约表:时间和执行的功能段之间的二维表

  • 禁止表F:一个由禁用启动距离构成的集合

    对于预约表的每一行的任何一对√,用它们所在的列号相减(大的减小的),列出各种可能的差值,然后删除相同的,剩下的就是禁止表的元素。

冲突向量

冲突向量:一个N位的二进制位串:

初始冲突向量C0 决定了下一个任务需间隔多少个时钟周期才可以流入

设C0=(cNcN-1…ci…c2c1);$C_i=\begin{cases}1,&i\in F\0,&i\notin F\end{cases}$
ci=0 :允许间隔i个时钟周期后送入后续任务
ci=1 :不允许间隔i个时钟周期后送入后续任务

假设第二个任务是在与第一个任务间隔j个时钟周期流入,这时,由于第一个任务已经在流水线中前进了j个时钟周期,其相应的禁止表中各元素的值都应该减去j,并丢弃小于等于0的值。对原始冲突向量C0来说,就是逻辑右移j位(左边补0)

第二个任务也会有着自己的冲突向量C。

由于后续任务既不能与第一个任务冲突,又不能与第二个任务冲突,所以当前(第二个任务进入流水线后的那个时钟周期)冲突向量(禁止表)是第一个任务与第二个任务的当前冲突向量的按位或(并集)

假设: Ck为当前的冲突向量,j: 允许的时间间隔,则,新的冲突向量为:SHR(j)(Ck)∨C0(将当前冲突向量逻辑右移j位,再与新任务的冲突向量C0做按位或),得到,任意一个任务进入后的新冲突向量。

(通过遍历)获得所有可能的冲突向量,构成流水线状态集合:

从初始冲突向量C0出发,对于所有允许的时间间隔(所有i,满足Ci=0)都按上述步骤求出其新的冲突向量,并且把新的冲突向量作为当前冲突向量,反复使用上述步骤,直到不再产生新的冲突向量为止。

流水线状态转移图

得到所有可能的冲突向量后,可以画出其状态转移图。

三要素

  • 流水线状态集合(所有可能的冲突向量)
  • 有向弧:表示状态转移的方向
  • 弧上的数字:表示引入后续任务(从而产生新的冲突向量)所用的时间间隔(时钟周期数)

调度方案与最优调度方案

  • 调度方案:根据流水线状态图,由初始状态出发,任何一个闭合回路即为一种调度方案。

  • 最优调度方案:(任务进入流水线的)平均时间间隔最小的调度方案

列出所有可能的调度方案,计算出每种方案的平均时间间隔,从中找出其最小者即为最优调度方案。

等时间间隔调度方案

不等时间间隔的调度方案,与等间隔的调度方案相比,在控制上要复杂得多。

相关

流水线冲突(例如:在第二条指令需要第一条指令在第五周期的结果,那么第二条指令必须等到第一条指令产生相关结果才能进入流水线)会导致流水线停顿,使其性能下降。

相关是导致流水线冲突的主要原因。

相关是指两条指令之间存在某种依赖关系,如果两条指令相关,则它们就有可能不能在流水线中重叠执行或者只能部分重叠执行。

需要注意的是:相关是程序属性而非流水线属性。

相关可分为三类:数据相关(也称真数据相关),名相关,以及控制相关。

数据相关

定义:对于两条指令i(在前,下同)和j(在后,下同),如果下述条件之一成立,则称指令j与指令i数据相关。

(1)指令j使用指令i产生的结果;

(2)指令j与指令k数据相关,而指令k又与指令i数据相关。【传递性】

数据相关检测

当数据的流动是经过寄存器时,相关的检测比较直观和容易。

当数据的流动是经过存储器时,检测比较复杂。【有可能两条相关指令之间对相关数据进行过修改导致两条指令之间没有相关性】

名相关

名:指令所访问的寄存器或存储器单元的名称

名相关:如果两条指令使用相同的名,但是它们之间并没有数据流动,则称这两条指令存在名相关

反相关

如果指令j写的名与指令i读的名相同,则称指令i和j发生了反相关。

指令j写的名=指令i读的名

这里“反”是指与数据相关的顺序(先写后读)相反(反相关是先读后写)。

输出相关

如果指令j和指令i写相同的名,则称指令i和j发生了输出相关

指令j写的名=指令i写的名

名相关解决方案

名相关特点

  • 名相关的两条指令之间并没有数据的传送。
  • 如果一条指令中的名改变了,并不影响另外一条指令的执行。

消除名相关的方法:换名技术

换名技术:通过改变指令中操作数的名来消除名相关。

  • 对于寄存器操作数进行换名称为寄存器换名
    • 既可以用编译器静态实现,也可以用硬件动态完成。

控制相关

控制相关是指由分支指令引起的相关。传统情况下,为了保证程序应有的执行顺序,必须严格按控制相关确定的顺序执行。

与一条分支指令控制相关的指令不能被移到该分支之前。

流水线冲突

流水线冲突是指对于具体的流水线来说,由于相关等原因的存在使得指令流中的下一条指令不能在指定的时钟周期执行。

流水线冲突分类

  • 结构冲突:因硬件资源满足不了指令重叠执行的要求而发生的冲突。
  • 数据冲突:当指令在流水线中重叠执行时,因需要用到前面指令的执行结果而发生的冲突。
  • 控制冲突:流水线遇到分支指令和其它会改变PC值的指令所引起的冲突。

问题与基本解决方法

问题

  • 导致错误的执行结果。
  • 流水线可能会出现停顿,从而降低流水线的效率和实际的加速比。

基本解决方法

  • 暂停部分指令执行:当一条指令被暂停时,在该暂停指令之后流出的所有指令都要被暂停,而在该暂停指令之前流出的指令则继续进行(否则就永远无法消除冲突)。

结构冲突

常见的导致结构冲突的原因有两个。一是功能部件不是完全流水,二是资源份数不够。

解决方法:

  • 插入暂停周期【通过拉开两条冲突指令在流水线之间的距离来避免冲突发生的】

    • 为消除结构冲突而插入的流水线气泡
  • 设置相互独立的存储器,使之分别存储指令与数据【通过增加资源数量,来避免访问同一资源导致的冲突】

    • 通过让读取指令与读写数据的访存请求落到不同的存储器中,从而避免访问同一存储器引起的冲突。对应的就是组成原理课程中讲过的哈弗结构。在现代通用处理器中,这种技术被普遍用于一级cache中。

不一定需要消除所有的结构冲突:

主要是为了考虑减少硬件成本。

数据冲突

写后读冲突(RAW)

对应的相关:最常见的一种数据冲突,对应于真数据相关。

发生条件

  • 有两条指令i和j,i在j之前进入流水线
  • 在 i 写入之前,j 先去读

结果

  • j 读出的内容是错误的

写后写冲突(WAW)

对应相关:对应于输出相关。

发生条件:

  • 流水线中不只一个段可以进行写操作,且指令被重新排序了
  • 有两条指令i和j,i在j之前进入流水线
  • 在 i 写入之前,j 先写。

结果

  • 最后写入的结果是 i 的。错误!

读后写冲突(WAR)

对应相关:反相关

发生条件

  • 有些指令的写结果操作提前了,而且有些指令的读操作滞后了;或是指令被重新排序了。
  • 有两条指令i和j,i在j之前进入流水线。在 i 读取之前,j 先写入。

结果

  • i读到的结果是错误的。

解决方案

  • 定向(短路、旁路):(减少冲突引起的停顿时间)
    • 在计算结果尚未出来之前,后面等待使用该结果的指令并不真正立即需要该计算结果,如果能够将该计算结果从其产生的地方直接送到其它指令需要它的地方,那么就可以避免停顿。

定向技术实际上是在原有的寄存器传递数据的基础上,增加了新的指令间传递数据的路径。

  • 指令调度(流水线调度):让编译器重新组织指令顺序来消除冲突

定向技术是基于硬件的运行时解决方案;指令调度是基于软件的的预处理方案

控制冲突

起因:执行分支指令的结果有两种

  • 分支成功:PC值改变为分支转移的目标地址。在条件判定和转移地址计算都完成后,才改变PC值。

  • 不成功或者失败:PC的值保持正常递增,指向顺序的下一条指令。

控制冲突

  • 分支延迟:分支指令引起的延迟

最简单的处理方法

  • “冻结”或者“排空”流水线
    • 前述5段流水线中,改变PC值是在MEM段进行的。给流水线带来了3个时钟周期的延迟

基本解决方案

通过设置专用的处理单元,在流水线中尽早判断出分支转移是否成功,以及分支目标地址。【硬件】

进一步:基于编译器的软件方法

预测分支失败

允许分支指令后的指令继续在流水线中流动,就好象什么都没发生似的;若确定分支失败,将分支指令看作是一条普通指令,流水线正常流动;若确定分支成功,流水线就把在分支指令之后取出的所有指令转化为空操作,并按分支目地重新取指令执行。

预测分支成功

假设分支转移成功,并从分支目标地址处取指令执行。这种方法起作用的前题是,需要先知道分支目标地址,后知道分支是否成功。显然,前述5段流水线中,这种方法没有任何好处。原因在于,分支目标地址与分支是否成功是同时计算出来的。

延迟分支

从逻辑上“延长”分支指令的执行时间。把延迟分支看成是由原来的分支指令和若干个延迟槽构成,不管分支是否成功,都要按顺序执行延迟槽中的指令。
延迟分支的优点在于无论分支成功还是失败都能够减少(掩盖)一个时钟周期的延迟。

通过在分支指令后增加延迟槽,并将有用的指令从别处调度到延迟槽执行,当分支是否成功与分支目标地址被算出时,恰好能够进行对应的取指操作,从而有效的消除了分支延迟。

–要点:在延迟槽中放入有用的指令,由编译器完成。能否带来好处取决于编译器能否把有用的指令调度到延迟槽中。

从调度的角度,延迟分支又可分为三类:从前调度,从目标处调度,从失败处调度。

  • 从前调度:从分支指令之前调度一条与该分支无关的指令到分支延迟槽中。
  • 从目标处调度:将分支目标地址对应的指令调度到延迟槽中。
  • 从失败处调度:将分支失败处的指令(下一条顺序地址)调入延迟槽。

由于只有当调入到延迟槽的指令实际上会被执行时,分支延迟技术才会真正起效,因此,可以认为:无论分支是否成功,从前调度均能消除控制冲突;只有当分支成功时,从目标处调度才能消除控制冲突;只有当分支失败时,从失败处调度才能消除控制冲突。

既然从前调度的效果远胜于后两种调度,为什么还要有后两种方法呢?原因在于,前面提到的从前调度需要找到一条与该分支无关的指令,这样的指令通常是极少的。

分支延迟的改善受到两个方面的限制:一是需要在延迟槽中放入有用的指令(由编译器完成);二是能否带来好处取决于编译器能否把有用的指令调度到延迟槽中。

此外,对于预测错误的情况,需要进一步的改进。引入分支取消机制,当分支的实际执行方向和事先所预测的一样时,执行分支延迟槽中的指令,否则就将分支延迟槽中的指令转化成一个空操作。

流水线的实现

MIPS的流水化:每一个时钟周期完成的工作看作是流水线的一段,每个时钟周期启动一条新的指令。

流水寄存器的位置和作用:

  • 位置:段与段之间设置流水寄存器

  • 作用

    • 将各段的工作隔开,使得它们不会互相干扰。
    • 保存相应段的处理结果。
    • 向后传递后面将要用到的数据或者控制信息
      所有有用的数据和控制信息每个时钟周期会随着指令在流水线中的流动往后流动一段

GOF23种设计模式

设计模式主要分三个类型:创建型、结构型和行为型。

创建型

  1. Singleton,单例模式:保证一个类只有一个实例,并提供一个访问它的全局访问点
  2. Abstract Factory,抽象工厂:提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们的具体类。
  3. Factory Method,工厂方法:定义一个用于创建对象的接口,让子类决定实例化哪一个类,Factory Method使一个类的实例化延迟到了子类。
  4. Builder,建造模式:将一个复杂对象的构建与他的表示相分离,使得同样的构建过程可以创建不同的表示。
  5. Prototype,原型模式:用原型实例指定创建对象的种类,并且通过拷贝这些原型来创建新的对象。

行为型

  1. Iterator,迭代器模式:提供一个方法顺序访问一个聚合对象的各个元素,而又不需要暴露该对象的内部表示。
  2. Observer,观察者模式:定义对象间一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知自动更新。
  3. Template Method,模板方法:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中,TemplateMethod使得子类可以不改变一个算法的结构即可以重定义该算法得某些特定步骤。
  4. Command,命令模式:将一个请求封装为一个对象,从而使你可以用不同的请求对客户进行参数化,对请求排队和记录请求日志,以及支持可撤销的操作。
  5. State,状态模式:允许对象在其内部状态改变时改变他的行为。对象看起来似乎改变了他的类。
  6. Strategy,策略模式:定义一系列的算法,把他们一个个封装起来,并使他们可以互相替换,本模式使得算法可以独立于使用它们的客户。
  7. China of Responsibility,职责链模式:使多个对象都有机会处理请求,从而避免请求的送发者和接收者之间的耦合关系
  8. Mediator,中介者模式:用一个中介对象封装一些列的对象交互。
  9. Visitor,访问者模式:表示一个作用于某对象结构中的各元素的操作,它使你可以在不改变各元素类的前提下定义作用于这个元素的新操作。
  10. Interpreter,解释器模式:给定一个语言,定义他的文法的一个表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子
  11. Memento,备忘录模式:在不破坏对象的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。

结构型

  1. Composite,组合模式:将对象组合成树形结构以表示部分整体的关系,Composite使得用户对单个对象和组合对象的使用具有一致性。
  2. Facade,外观模式:为子系统中的一组接口提供一致的界面,fa?ade提供了一高层接口,这个接口使得子系统更容易使用。
  3. Proxy,代理模式:为其他对象提供一种代理以控制对这个对象的访问
  4. Adapter,适配器模式:将一类的接口转换成客户希望的另外一个接口,Adapter模式使得原本由于接口不兼容而不能一起工作那些类可以一起工作。
  5. Decrator,装饰模式:动态地给一个对象增加一些额外的职责,就增加的功能来说,Decorator模式相比生成子类更加灵活。
  6. Bridge,桥模式:将抽象部分与它的实现部分相分离,使他们可以独立的变化。
  7. Flyweight,享元模式

一、工厂模式

1.1、简单工厂模式(静态工厂模式)

当你需要某个对象的时候,通常你是new出来的,但是这样代码耦合度太高,因此我们通过一个工厂来生产出来这个对象

  • 工厂:负责实现创建所有实例的内部逻辑,并且提供一个外界调用的方法,创建所需产品对象
  • 抽象产品:用来描述产品的公共接口
  • 具体产品:描述生产的具体产品
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
/**
* @ Product.java
* 抽象产品
* 描述产品的公共接口
*/
abstract class Product {
//产品介绍
abstract void intro();
}

/**
* @ AProduct.java
* 具体产品A
* (可以看成是一种饮料:可乐)
*/
public class AProduct extends Product{
@Override
void intro() {
System.out.println("可乐");
}
}

/**
* @ BProduct.java
* @具体产品B
* @(可以看成是一种饮料:奶茶)
*/
public class BProduct extends Product{
@Override
void intro() {
System.out.println("奶茶");
}
}

/**
* @ CProduct.java
* 具体产品C
* (可以看成是一种饮料:咖啡)
*/
public class CProduct extends Product{
@Override
void intro() {
System.out.println("咖啡");
}
}
/*Factory.java*/
/**
* 工厂
* 负责实现创建所有实例的内部逻辑,并提供一个外界调用的方法,创建所需的产品对象。
*/
public class Factory {
/**
* 供外界调用的方法
* (可以看成是对外提供的三种按钮)
* @param type
* @return 产品实例
*/
public static Product getProduct(String type) {
switch (type) {
case "A":
return new AProduct();
case "B":
return new BProduct();
case "C":
return new CProduct();
default:
return null;
}
}
}
/*test.java*/
public class Test {
public static void main(String[] args) {
//创建具体的工厂
Factory factory = new Factory();
//根据传入的参数生产不同的产品实例
//(按下不同的按钮,获取饮料)
Product A = Factory.getProduct("A");
A.intro();
Product B = Factory.getProduct("B");
B.intro();
Product C = Factory.getProduct("C");
C.intro();
}
}

优点:将创建使用工作分开,不必关心类对象如何创建,实现了解耦;
缺点:违背“开放 - 关闭原则”,一旦添加新产品就不得不修改工厂类的逻辑,这样就会造成工厂逻辑过于复杂。

1.2、工厂方法模式(工厂模式)

可以理解成将上述的静态工厂模式中的工厂进行拆分,让对应的实例产品都有一个单独的工厂对其进行生产。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* @ Product.java
* 抽象产品
*/
abstract class Product {
//产品介绍
abstract void intro();
}

/**
* @ ProductA.java
* 具体产品A
*/
public class ProductA extends Product{
@Override
void intro() {
System.out.println("饮料A");
}
}

/**
* @ ProductB.java
* 具体产品B
*/
public class ProductB extends Product{
@Override
void intro() {
System.out.println("饮料B");
}
}

工厂:Factory.java、FactoryA.java 、FactoryB.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* @ Factory.java
* 抽象工厂
*/
abstract class Factory {
//生产产品
abstract Product getProduct();
}

/**
* @ FactoryA.java
* 具体工厂A
* 负责具体的产品A生产
*/
public class FactoryA extends Factory{
@Override
Product getProduct() {
return new ProductA();
}
}

/**
* @ FactoryB.java
* @具体工厂B
* 负责具体的产品B生产
*/
public class FactoryB extends Factory{
@Override
Product getProduct() {
return new ProductB();
}
}

测试:Test.java

1
2
3
4
5
6
7
8
9
10
public class Test {
public static void main(String[] args) {
//创建具体的工厂
FactoryA factoryA = new FactoryA();
//生产相对应的产品
factoryA.getProduct().intro();
FactoryB factoryB = new FactoryB();
factoryB.getProduct().intro();
}
}

一个抽象产品类,可以派生出多个具体产品类。一个抽象工厂类,可以派生出多个具体工厂类。每个具体工厂类只能创建一个具体产品类的实例

优点:

  1. 符合开-闭原则:新增一种产品时,只需要增加相应的具体产品类和相应的工厂子类即可
  2. 符合单一职责原则:每个具体工厂类只负责创建对应的产品

缺点:

  1. 增加了系统的复杂度:类的个数将成对增加
  2. 增加了系统的抽象性和理解难度
  3. 一个具体工厂只能创建一种具体产品

1.3、抽象工厂模式

为了解决工厂模式中一个工厂只能生产一个具体产品的问题。抽象工厂模式使用抽象类添加了抽象工厂,然后让具体工厂继承该抽象工厂达到一个具体工厂可以生产多种具体产品的效果。

抽象工厂:描述具体工厂的公共接口
具体工厂:描述具体工厂,创建产品的实例,供外界调用
抽象产品族:描述抽象产品的公共接口
抽象产品:描述具体产品的公共接口
具体产品:具体产品

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
* @ Product.java
* 抽象产品族 (食品)
*/
abstract class Product {
//产品介绍
abstract void intro();
}

/**
* @ ProductA.java
* 抽象产品 (饮料)
*/
abstract class ProductA extends Product{
@Override
abstract void intro();
}

/**
* @ ProductB.java
* 抽象产品 (零食)
*/
abstract class ProductB extends Product{
@Override
abstract void intro();
}

/**
* @ ProductAa.java
* 具体产品 (矿泉水)
*/
public class ProductAa extends ProductA{
@Override
void intro() {
System.out.println("矿泉水");
}
}

/**
* @ ProductBb.java
* 具体产品 (面包)
*/
public class ProductBb extends ProductB{
@Override
void intro() {
System.out.println("面包");
}
}

工厂:Factory.java、FactoryA.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* @ Factory.java
* 抽象工厂
*/
abstract class Factory {
//生产饮料
abstract Product getProductA();
//生产零食
abstract Product getProductB();
}

/**
* @ FactoryA.java
* 具体工厂A
* 负责具体的A类产品生产
*/
public class FactoryA extends Factory{
@Override
Product getProductA() {
//生产矿泉水
return new ProductAa();
}
@Override
Product getProductB() {
//生产面包
return new ProductBb();
}
}

测试:Test.java

1
2
3
4
5
6
7
8
9
public class Test {
public static void main(String[] args) {
//创建零食售卖机(具体工厂),
FactoryA factoryA = new FactoryA();
//获取矿泉水与面包(具体产品)
factoryA.getProductA().intro();
factoryA.getProductB().intro();
}
}

多个抽象产品类,每个抽象产品类可以派生出多个具体产品类。一个抽象工厂类,可以派生出多个具体工厂类。 每个具体工厂类可以创建多个具体产品类的实例。.

优点:

  1. 降低耦合
  2. 符合开-闭原则
  3. 符合单一职责原则
  4. 不使用静态工厂方法,可以形成基于继承的等级结构。

缺点:难以扩展新种类产品

二、单例模式(singleton)

确保一个类只有一个实例,并提供该实例的全局访问点。

单例的实现主要是通过以下两个步骤

  1. 将该类的构造方法定义为私有方法,这样其他处的代码就无法通过调用该类的构造方法来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例
  2. 在该类内提供一个静态方法,当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用。

1、单例模式的应用场景和优缺点

举一个小例子,在我们的windows桌面上,我们打开了一个回收站,当我们试图再次打开一个新的回收站时,Windows系统并不会为你弹出一个新的回收站窗口。,也就是说在整个系统运行的过程中,系统只维护一个回收站的实例。这就是一个典型的单例模式运用。

继续说回收站,我们在实际使用中并不存在需要同时打开两个回收站窗口的必要性。假如我每次创建回收站时都需要消耗大量的资源,而每个回收站之间资源是共享的,那么在没有必要多次重复创建该实例的情况下,创建了多个实例,这样做就会给系统造成不必要的负担,造成资源浪费。

适用场景:

  • 1.需要生成唯一序列的环境
  • 2.需要频繁实例化然后销毁的对象。
  • 3.创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
  • 4.方便资源相互通信的环境

常见应用场景:

  • Windows的Task Manager(任务管理器)
  • windows的Recycle Bin(回收站)也是典型的单例应用
  • 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作 ,否则内容不好追加。
  • 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源
  • 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统
  • Application 也是单例的典型应用(Servlet编程中会涉及到)
  • 在Spring中,每个Bean默认就是单例的,这样做的优点是Spring容器可以管理
  • 在servlet编程中,每个Servlet也是单例
  • 在spring MVC框架/struts1框架中,控制器对象也是单例

优点

  • 在内存中只有一个对象,节省内存空间;
  • 避免频繁的创建销毁对象,可以提高性能;
  • 避免对共享资源的多重占用,简化访问;
  • 为整个系统提供一个全局访问点。

缺点

  • 不适用于变化频繁的对象;
  • 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;
  • 如果实例化的对象长时间不被利用,系统会认为该对象是垃圾而被回收,这可能会导致对象状态的丢失;

单例模式的要点有三个:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。单例模式是一种对象创建型模式。单例模式又名单件模式或单态模式。

单例模式的实现代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Singleton
{
private static Singleton instance=null; //静态私有成员变量
//私有构造函数
private Singleton()
{
}

//静态公有工厂方法,返回唯一实例
public static Singleton getInstance()
{
if(instance==null)
instance=new Singleton();
return instance;
}
}

在单例模式的实现过程中,需要注意如下三点:

• 单例类的构造函数为私有;

• 提供一个自身的静态私有成员变量;

• 提供一个公有的静态工厂方法。

2、饿汉式——线程安全、调用效率高、无法延时加载

类加载的方式是按需加载,且加载一次。。因此,在上述单例类被加载时,就会实例化一个对象并交给自己的引用,供系统使用;而且,由于这个类在整个生命周期中只会被加载一次,因此只会创建一个实例,即能够充分保证单例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 饿汉式单例
public class Singleton1 {

// 指向自己实例的私有静态引用,主动创建
private static Singleton1 singleton1 = new Singleton1();

// 私有的构造方法
private Singleton1(){}

// 以自己实例为返回值的静态的公有方法,静态工厂方法
public static Singleton1 getSingleton1(){
return singleton1;
}
}

优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。

缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。

3、懒汉式——线程安全、调用效率低、延时加载

单例实例被延迟加载,即只有在真正使用的时候才会实例化一个对象并交给自己的引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 懒汉式单例
public class Singleton2 {

// 指向自己实例的私有静态引用
private static Singleton2 singleton2;

// 私有的构造方法
private Singleton2(){}

// 以自己实例为返回值的静态的公有方法,静态工厂方法
public static synchronized Singleton2 getSingleton2(){//synchronized保证getSingleton2是一个同步方法,可以保证在多线程情况下单例对象唯一性

//没有synchronized,只能在单线程下使用。如果在多线程下,一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式。

// 被动创建,在真正需要使用时才去创建
if (singleton2 == null) {
singleton2 = new Singleton2();
}
return singleton2;
}
}

优点:单例只有在使用时才会被实例化,在一定程度上节约了资源

缺点:第一次加载时需要及时进行实例化,反应稍慢,最大的问题是每次调用getInstance都进行同步,造成不必要的同步开销。这种模式一般不建议使用。

4、双重加锁机制(Double Check Lock)——线程安全

Double-Check概念对于多线程开发者来说不会陌生,如代码中所示,我们进行了两次if (singleton == null)检查,这样就可以保证线程安全了。这样,实例化代码只用执行一次,后面再次访问时,判断if (singleton == null),直接return实例化对象。

使用双重检测同步延迟加载去创建单例的做法是一个非常优秀的做法,其不但保证了单例,而且切实提高了程序运行效率

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Singleton
{
private static Singleton instance;
//程序运行时创建一个静态只读的进程辅助对象
private static readonly object syncRoot = new object();
private Singleton() { }
public void dosomething()
{
System.out.println("do sth.");
}
public static Singleton GetInstance()
{
//先判断是否存在,不存在再加锁处理
if (instance == null)
{
//在同一个时刻加了锁的那部分程序只有一个线程可以进入
//lock(syncRoot) 获取对象syncRoot的互斥锁,可以简单理解为,当多个线程同时执行到lock的时候,大家排队,一个一个地进行。
lock (syncRoot)//可以使用synchronized (Singleton.class)
{
if (instance == null)
{
instance = new Singleton();
}
}
}
return instance;
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Singleton
{
//当声明对象的引用为volatile后,“问题的根源”的三行伪代码中的2和3之间的重排序,在多线程环境中将会被禁止,类在实例化过程中会严格按照1、2、3顺序执行下去。
private volatile static Singleton instance;
//程序运行时创建一个静态只读的进程辅助对象
private Singleton() { }
public void dosomething()
{
System.out.println("do sth.");
}
public static Singleton GetInstance()
{
//先判断是否存在,不存在再加锁处理
if (instance == null)
{
//在同一个时刻加了锁的那部分程序只有一个线程可以进入
synchronized (Singleton.class)
{
if (instance == null)
{
instance = new Singleton();
}
}
}
return instance;
}
}

第一次 if (instance == null)主要是为了避免不必要的同步,第二次的判断是为了在null的情况下创建实例。

优点:既能够在需要时才初始化单例,又能够保证线程安全,且单例对象初始化后调用getInstance不进行同步锁。
资源利用率高,第一次执行getInstance时单例对象才会被实例化,效率高。

缺点:第一次加载慢

参考文章

5、静态初始化

1
2
3
4
5
6
7
8
9
10
11
12
//阻止发生派生,而派生可能会增加实例
public sealed class Singleton
{
//在第一次引用类的任何成员时创建实例,公共语言运行库负责处理变量初始化
private static readonly Singleton instance=new Singleton();

private Singleton() { }
public static Singleton GetInstance()
{
return instance;
}
}

6、静态内部类

解决DCL在某些情况下出现失效的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton{
private Slingleton()
{ }
public static Singleton getInstance()
{
return SingleHolder.sInstance;
}
//静态内部类
private static class SingletonHolder
{
private static final Singleton sInstance = new Singleton();
}
}

当第一次加载singleton类时并不会初始化sInstance,只有在第一次调用Singleton的getInstance方法时才会导致sInstance被初始化。

第一次调用getInstance方法会导致虚拟机加载SingletonHolder类,这种方式不仅能够确保线程安全,也可以确保单例对象的唯一性,同时也延迟了单例的实例化。推荐。

7、枚举单例

1
2
3
4
5
6
7
public enum SingletonEnum{
INSTANCE;
public void dosomething()
{

}
}

8、使用容器实现

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SingletonManager{
private static Map<String,Object> objMap = new HashMap<String,Object>();
private SingletonManager(){}
public static void registerService(String key,Object instance)
{
if(!objMap.containsKey(key))
{
objMap.put(key,instance);
}
}
public static Object getService(String key)
return objMap.get(key);
}

在程序的初始时,可以将多种单例类型注入到一个统一的管理类中,在使用时根据key获取对象对应类型的对象。

优点:可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作,降低用户的使用成本,降低耦合度。

要想实现效率高的线程安全的单例,我们必须注意以下两点:

  • 尽量减少同步块的作用域;
  • 尽量使用细粒度的锁。

数据库完整性

  • 数据库的完整性

    • 数据的正确性和相容性
  • 数据的完整性和安全性是两个不同概念:

    • 数据的完整性:防止数据库中存在不符合语义的数据,也就是防止数据库中存在不正确的数据
    • 数据的安全性:保护数据库防止恶意的破坏和非法的存取
  • 为维护数据库的完整性,DBMS必须:

    • 提供定义完整性约束条件的机制
    • 提供完整性检查的方法
    • 违约处理

DBMS完整性控制机制应具有的功能:

  • 违约反应
  • 定义功能
  • 检查功能

实体完整性

实体完整性的定义机制:
–CREATE TABLE中用PRIMARY KEY定义
–列级约束条件 或表级约束条件

实体完整性的检查和违约处理:

在插入或对主码列进行更新操作时,RDBMS按照实体完整性规则自动进行检查。包括:
–检查主码值是否唯一,如果不唯一则拒绝插入或修改
–检查主码的各个属性是否为空,只要有一个为空就拒绝插入或修改

参照完整性

参照完整性,指多表之间的设计,主要使用外键约束【多表设计有:一对多,多对多,一对一】

主要是定义外码,将一个关系的主码放在另一个关系中,作为该关系的属性,就称其为外码。外码的取值有两种情况,一种为空,另外一种就是被参照表的主码的域。

子表的删除更新策略一共有4种:

  • CASCADE 级联策略。
    主表的修改会被同步到子表中。

  • NO ACTION 无动作策略。
    要删改主表,必须要先删改子表对应数据。

  • RSTRICT 主表约束策略。
    此策略,对主表的约束跟NO ACTION一样。即删、更新主表前的主键,必须要先删、更新子表中对应的。

  • SET NO 置空策略。
    使用此策略时,当主表主键删改,则子表中的外键 设置为NULL。
    但如果子表的外键是主键,或者子表外键设置NOT NULL,则此时就相当于NO ACTION策略。

参照完整性的定义机制:
–在CREATE TABLE中用FOREIGN KEY短语定义哪些列为外码,
–用REFERENCES短语指明这些外码参照哪些表的主码

被参照表 参照表 违约处理
可能破坏参照完整性 插入元组 拒绝
可能破坏参照完整性 修改外码值 拒绝/级联修改/设置为空值
修改主码值 可能破坏参照完整性 拒绝/级联修改/设置为空值
删除元组 可能破坏参照完整性 拒绝/级联修改/设置为空值

若要在参照完整性中采用级联更新的修改方式,则应该在外码定义中说明。

用户定义完整性

用户定义的完整性就是针对某一具体应用的数据必须满足的语义要求

分为:属性上的约束和元组上的约束。

属性上的约束:在CREATE TABLE中定义属性时定义
–列值非空(NOT NULL),列值唯一(UNIQUE),检查列值是否满足一个布尔表达式(CHECK)
–插入元组或修改属性值时检查,不满足则拒绝执行。

元组上的约束
–在CREATE TABLE时可以用CHECK短语定义元组上的约束条件,即元组级的限制
–同属性值限制相比,元组级的限制可以设置不同属性之间的取值的相互约束条件
–插入元组或修改属性值时检查,不满足则拒绝执行

在一张表上可以定义多个完整性约束,为方便起见,在CREATE TABLE中可用CONSTRAINT语句对所定义的约束条件命名

1
2
CONSTRAINT<完整性约束条件名><完整性约束条件>
其中,<完整性约束条件>包括:NOT NULLUNIQUE、PRIMARYKEY、FOREIGN KEYCHECK短语等。

语义: 对定义的约束条件命名

触发器

是用户定义在关系表上的一类由事件驱动的特殊过程。亦称为:事件-条件-动作规则。

  • 一旦定义,触发器将被保存在数据库服务器中。
  • 任何用户对表的增、删、改操作均由服务器自动激活相应的触发器,在DBMS核心层进行集中的完整性控制。
  • 比约束更为灵活,规则中的动作可以很复杂,可以包含 if/ while/case 等程序控制指令,可以涉及其他表和其他数据库对象,实施更为复杂的检查和操作,具有更精细和更强大的数据控制能力。
1
2
3
4
5
6
CREATE TRIGGER <触发器名>  
{ BEFORE| AFTER} <触发事件>
ON<表名>
FOR EACH { ROW | STATEMENT }
WHEN<触发条件>]
<触发动作体>
  • 触发时机:AFTER 表示 执行条件,有 BEFORE(之前 ) AFTER(之后)

  • 触发事件:INSERT ON 表示在执行了 插入操作 ,有INSERT/UPDATE/DELETE 三种

  • 触发者类型:FOR EACH ROW BEGIN 固定语法

    • 行级触发器(FOR EACH ROW)
    • 语句级触发器(FOR EACH STATEMENT)【那么执行完该语句后,触发动作只发生一次】
  • 触发动作体:是一段程序。如果触发器是行级触发器,则这段程序中还可以使用NEW和OLD分别引用触发事件发生前后的元组值。

激活触发器

  • 触发器的执行,由触发事件激活,并由数据库服务器自动执行。

  • 一个数据表上可能定义了多个触发器,同一个表上的多个触发器激活时遵循如下的执行顺序:

    ​ (1) 执行该表上的BEFORE触发器;
    ​ (2) 执行激活触发器的SQL语句;
    ​ (3) 执行该表上的AFTER触发器。

  • 有多个触发器时,按创建时间顺序依次执行。

  • 任一触发器的执行失败都将中止整个操作。

删除触发器

删除触发器的SQL语法:

1
DROP TRIGGER <触发器名> ON<表名>;

触发器必须是一个已经创建的触发器,并且只能由具有相应权限的用户删除。

注意:不同的DBMS对于触发器的支持方式和语法是有所区别的!

数据库安全性概述

1.数据库安全性定义数据库的安全性是指保护数据库以防止不合法的使用所造成的数据泄露、更改或破坏。

2.重要性

  • 数据库的一大特点是数据可以共享

  • 数据共享必然带来数据库的安全性问题

  • 数据库系统中的数据共享不能是无条件的共享

系统的安全保护措施是否有效是数据库系统的主要性能指标之一。

数据库的不安全因素

1)非授权用户对数据库的恶意存取和破坏

  • 安全措施:用户身份鉴别、存取控制、和视图等

2)数据库中重要或敏感数据被泄露

  • 安全措施:审计、日志、入侵检测、数据加密等

3)安全环境的脆弱性,包括:硬件、OS、网络等。

  • 可信计算机系统

数据库安全技术

  • 用户身份验证
  • 多层存取控制
  • 审计
  • 视图
  • 数据加密等

用户身份鉴别

  • 用户标识与鉴别(Identification & Authentication):
    • 系统提供的最外层安全保护措施;
    • 系统提供一定的方式让用户标识自己的名字和身份,系统进行核实,通过鉴定后才提供系统使用权。
  • 常用鉴别方法:
    • 口令:静态(易被窃取)、动态(一次一密)
    • 生物特征识别:指纹、声音、照片等
    • 智能卡
    • 回答问题

存取控制

1.什么是存取控制?对于获得上机权的用户还要根据系统预先定义好的外模式(视图)或用户权限进行存取控制,保证用户只能存取他有权存取的数据。

2.方法

  • 定义用户权限
  • 合法权限检查

3.常用存取控制方法

  • 自主存取控制(Discretionary Access Control,DAC),C2级,灵活
  • 强制存取控制(Mandatory Access Control,MAC),B1级,严格

视图机制

  • 视图能够把要保密的数据对无权存取这些数据的用户隐藏起来,因此对数据提供一定程度的安全保护。
  • 视图机制间接实现了支持存取谓词的用户权限定义。
    [例] 建立计算机系学生的视图,把对该视图的SELECT权限授于王平,把该视图上的所有操作权限授于张明。
    -先建立计算机系学生的视图CS_Student
    CREATE VIEW CS_Student AS SELECT * FROM Student WHERE Sdept=’CS’;
    -在视图上进一步定义存取权限:
    GRANT SELECT ON CS_Student TO 王平;
    GRANT ALL PRIVILIGES ON CS_Student TO 张明

审计

  • 审计

    • 系统自动建立审计日志,将用户对数据库的所有操作记录在上面。
    • DBA利用审计日志找出非法存取数据的人、时间和内容。
    • C2以上安全级别的DBMS必须具有。
  • 审计分类

    –用户级审计:针对自己创建的数据库表或视图进行审计。
    记录所有用户对这些表或视图的一切成功和(或)不成功的访问要求以及各种类型的SQL操作 。

    –系统级审计:由DBA设置

    • 监测成功或失败的登录要求;

    • 监测GRANT和REVOKE操作以及其他数据库级权限下的操作。

审计AUDIT语句

  • AUDIT语句:设置审计功能

  • NOAUDIT语句:取消审计功能

数据加密

  1. 思想
    数据库中的数据以密码形式存放,使用时由用户设计的解码程序将其转换成用户可读的数据。这样,数据库中的数据即使被窃取,也只能是一些无法辨认的代码。
  2. 数据库加密:存储加密、传输加密
  3. 数据库加密在哪个层次实现?
    1. OS层
    2. DBMS内核层
    3. DBMS外层

自主存取控制技术

自主存取控制

用户“自主”决定将数据的存取权限授予何人、决定是否将“授权”的权限授予别人,称为“自主存取控制”。

  • 定义:用户对于不同的数据库对象有不同的存取权限,不同的用户对同一对象也有不同的权限,而且用户还可将其拥有的存取权限转授给其他用户。
  • 用户权限组成要素 :数据对象、操作类型
  • 授权:定义用户可以在哪些数据库对象上进行哪些类型的操作。

授权与回收

SQL中使用GRANT语句和REVOKE语句来实现自主存取控制。

GRANT语句

  • GRANT语句的一般格式:

    1
    2
    3
    4
    GRANT<权限>[,<权限>]...
    [ON<对象类型> <对象名>]
    TO<用户>[,<用户>]...
    [WITH GRANT OPTION];
  • 语义:将对指定操作对象的指定操作权限授予指定的用户

    • 授权人:DBA、数据库对象创建者、拥有该权限的用户
    • 被授权限的用户: 一个或多个具体用户、PUBLIC(全体用户)
    • WITH GRANT OPTION子句:指定:可以再授予,没有指定:不能传播
    • 不允许循环授权

REVOKE语句

  • 授予的权限可以由DBA或其他授权者用REVOKE语句收回。

  • REVOKE语句的一般格式为:

    1
    2
    3
    REVOKE<权限>[,<权限>]...
    [ON<对象类型> <对象名>]
    FROM<用户>[,<用户>]...[CASCADE | RESTRICT];

创建数据库模式的权限

DBA在创建用户时,可以创建数据库模式的权限。

CREATE USER语句格式:

1
2
CREATE  USER  <username> 
WITH][DBA| RESOURCE| CONNECT

数据库角色

被命名的一组与数据库操作相关的权限

  • 角色是权限的集合
  • 可以为一组具有相同权限的用户创建一个角色
  • 简化授权的过程

1、角色的创建

1
CREATE  ROLE  <角色名> 

2、给角色授权

1
2
3
GRANT<权限>[,<权限>]... 
ON<对象类型>对象名
TO<角色>[,<角色>]...

3、将一个角色授予其他的角色或用户

1
2
3
GRANT<角色1>[,<角色2>]...
TO<角色3>[,<用户1>]...
[WITH ADMIN OPTION]

4、角色权限收回

1
2
3
REVOKE<权限>[,<权限>]...
ON<对象类型> <对象名>
FROM<角色>[,<角色>]...

示例如下:

自主存取控制缺点

  • 可能存在数据的“无意泄露”
  • 原因:这种机制仅仅通过对数据的存取权限来进行安全控制,而数据本身并无安全性标记

强制存取控制机制

对系统控制下的所有主客体赋予安全性标记,系统根据标记安全策略,实施强制存取控制

强制存取控制(MAC)

  • 保证更高程度的安全性
  • 用户不能直接感知或进行控制
  • 适用于对数据有严格而固定密级分类的部门
    • 军事部门
    • 政府部门

强制存取控制方法

  • 主体是系统中的活动实体
    • DBMS所管理的实际用户
    • 代表用户的各进程
  • 客体是系统中的被动实体,是受主体操纵的
    • 文件、基表、索引、视图
  • 敏感度标记(Label)
    • 绝密(Top Secret)> 机密(Secret)> 可信(Confidential)> 公开(Public)
  • 主体的敏感度标记称为许可证级别(Clearance Level)
  • 客体的敏感度标记称为密级(Classification Level)
  • 强制存取控制就是通过对比主体的许可证级别和客体的密级,最终确定主体能否存取客体。

主体可更新密级高于他的客体

强制存取控制规则

  • 强制存取控制规则:
    1. 仅当主体的许可证级别大于或等于客体的密级时,该主体才能读取相应的客体
    2. 仅当主体的许可证级别小于或等于客体的密级时,该主体才能写相应的客体
  • 规则的关键点:
    禁止拥有高许可证级别的主体更新低密级的数据对象

强制存取控制的优点

  • 禁止拥有高许可证级别的主体更新低密级的数据对象,从而保证了敏感数据的可靠性;
  • 禁止低许可证级别的主体浏览高密级的数据,避免了敏感数据的泄漏;
  • MAC对数据本身进行密级标记,无论数据如何复制,标记与数据是不可分割的整体。只有符合密级标记要求的用户才可以操作相应数据,提高了安全性级别。

强制存取控制方法

  • DAC与MAC共同构成DBMS的安全机制
  • 实现MAC时要首先实现DAC
    • 原因:较高安全性级别提供的安全保护要包含较低级别的所有保护
  • 先进行DAC检查,通过DAC检查的数据对象再由系统进行MAC检查,只有通过MAC检查的数据对象方可存取。

数据库安全机制的设计目标:试图破坏安全的人所花费的代价 >> 得到的利益

SQL语言概述

SQL语言是面向集合的非过程化的语言。是介于关系代数和关系演算之间的标准查询语言。

集数据定义语言DDL、数据操纵语言DML、数据控制语言DCL功能于一体。

核心功能:9个动词

  • 数据查询:SELECT(最为复杂的操作)

  • 数据定义:CREATE(创建)、DROP(删除)、ALTER(更新)

  • 数据操纵DML:INSERT、UPDATE、DELETE

  • 数据控制DCL:GRANT(授权)、REVOKE(撤销,回收权限)

  • 基本概念:

数据定义概述

数据定于语言DDL:是SQL中提供给用户定义DBMS支持的抽象对象的SQL语句。

DBMS支持4种抽象对象:模式、表、视图、索引

一个RDBMS的实例中,可建立多个数据库;

一个数据库中可以建立多个外模式,只有一个内模式;

一个模式下可以建立多个表、视图和索引等数据库对象

基本表的定义、修改和删除

定义基本表

1
CREATE TABLE<表名><列名> <数据类型>[ <列级完整性约束条件> ][,<列名> <数据类型>[ <列级完整性约束条件>]]...[,<表级完整性约束条件> ]);

可以加多个列级完整性约束条件。

根据已有的表创建新表:
A:create table tab_new like tab_old (使用旧表创建新表)
B:create table tab_new as select col1,col2… from tab_old definition only

模式和表

  • 每一个基本表都属于某一个模式,一个模式包含多个基本表
  • 定义基本表所属模式:
    • 方法一:在表名中明显地给出模式名
      CREATE TABLE S-T.Student(……); /模式名为S-T/
    • 方法二:在创建模式语句中同时创建表
      CREATE SCHEMA S-T AUTHORIZATION WANG
      CREATE TABLE Student(……);

修改表

1
2
3
4
5
6
ALTER TABLE<表名>
[ ADD<新列名> <数据类型> [ 完整性约束 ] ]
[ DROP[COLUMN]<列名> [CASCADE|RESTRICT]]
[ADD<表级完整性约束名> ]
[DROP CONSTRAINT<完整性约束名> [CASCADE|RESTRICT]]
[ALTER COLUMN<列名> <数据类型> ];

//新增加的列一律为空值。

删除表

1
DROP TABLE <表名>[RESTRICT| CASCADE];

RESTRICT:删除表是有限制的。欲删除的基本表不能被其他表的约束所引用,如果存在依赖该表的对象,则此表不能被删除

CASCADE:删除该表没有限制。在删除基本表的同时,相关的依赖对象一起删除

数据查询

基本语法:

1
SELECT A1,A2··· FROM R1,R2··· WHERE F

这里R1,…,Rm为关系,F是公式,A1,…,An为属性。

完整语法:

1
2
3
4
5
SELECT[ALL|DISTINCT]<目标列表达式>[,<目标列表达式>]...
FROM <表名或视图名>[,<表名或视图名> ] ...
[WHERE<条件表达式>]
[GROUP BY<列名1>[HAVING<条件表达式>] ]
[ORDER BY<列名2>[ ASC|DESC ] ];
  • SELECT子句,投影运算,选择表中的若干列;
  • WHERE子句,行筛选条件,选择表中的若干行;
  • GROUP子句,分组运算;
  • HAVING子句,分组条件;
  • ORDER子句,排序计算。

单表查询

  • 消除重复元组Distinct

  • where子句:如何从表中选择指定元组?

    • 对应于关系代数运算σP,SQL提供WHERE子句解决表元组的选择。

    • 格式:WHERE <条件表达式>【<条件表达式>是包含属性名的逻辑表达式P】

    • 范围查询:

      • 比较运算符:=、<、>、>=、<=、!=、<>、!>、!
      • <确定范围:BETWEEN … AND …NOT BETWEEN … AND …
    • 集合查询:

      • x IN <值表>, x NOT IN <值表>
    • 字符匹配查询:

      • [NOT]LIKE‘<匹配串>’[ESCAPE‘ <换码字符>’]

        查找指定的属性列值与<匹配串>相匹配的元组。<匹配串>可以是一个完整的字符串,也可以含有通配符。
        通配符:SQL规定符号百分号%及下划线__ 具有其他含义:
        百分号%:代表任意长度的字符串
        下划线__ :代表任意一个字符
        ESCAPE:是将百分号% 或下划线__转回其本意

    • 涉及空值查询:

      • IS NULL 或IS NOT NULL
    • 多重条件查询:

      • 逻辑运算符:AND和OR可以用来将多个简单查询条件复合成更加复杂的条件
      • 优先级:NOT>AND>OR
  • Order By子句:将查询结果的元组按照一个或多个属性列的排列次序显示

    • 格式:SELECT 块 ORDER BY 子句;
      其中,ORDER BY子句可以按一个或多个属性列排序;
      每列可选择:升序:ASC;降序:DESC;缺省值为升序

使用函数

函数只是将取出的数据进行处理,不会改变数据库中的值。

单行函数

1、字符函数

函 数 功 能 示 例 结 果
INITCAP (char) 首字母大写 initcap (‘hello’) Hello
LOWER (char) 转换为小写 lower (‘FUN’) fun
UPPER (char) 转换为大写 upper (‘sun’) SUN
LTRIM (char, set) 左剪裁 ltrim (‘xyzadams’, ‘xyz’) adams
RTRIM (char, set) 右剪裁 rtrim (‘xyzadams’, ‘ams’) xyzad
TRANSLATE (char, from, to) 按字符翻译 translate (‘jack’, ‘abcd’, ‘1234’) j13k
REPLACE (char, search_str, replace_str) 字符串替换 replace (‘jack andjue’, ‘j’, ‘bl’) black and blue
INSTR (char, substr[, pos]) 查找子串位置 instr (‘worldwide’, ‘d’) 5
SUBSTR (char, pos, len) 取子字符串 substr (‘abcdefg’,3,2) cd
CONCAT (char1, char2) 连接字符串 concat (‘Hello’, ‘world’) Helloworld

多行函数

1、SQL支持聚集函数来解决求集合特征值问题:如该集合的个数、最小值等

集函数只能用于SELECT子句和HAVING子句中

2、GROUP BY子句:将一个查询结果集合进行分组

  • GROUP BY A1,A2,…,An(其中:Ai为属性名)【按指定的一列或多列,对一个SELECT块按值分组,值相等的为一组】

3、HAVING子句:使用限定条件选择部分分组,则可以使用HAVING子句

注意:HAVING短语与WHERE子句的区别:
– WHERE子句作用于基表或视图,从中选择满足条件的元组
– HAVING短语作用于组,从中选择满足条件的组。

多表连接查询

1
2
3
4
5
6
7
8
9
10
11
SQL1999规范中规定的连接查询语法

select 字段列表
from table1
[cross join table2] | //1:交叉连接
[natural join table2] | //2:自然连接
[join table2 using (字段名)] | //3using子句
[join table2 on (table1.column_name
= table2.column_name)] | //4on子句
[(left | right | full outer) join table2
on (table1.column_name = table2.column_name)]; //5:左/右/满外连接
  • 合并:FROM
  • 选择:WHERE
  • 投影:SELECT

自身连接

两种连接表示方法:

1、在FROM子句中指明进行连接的表名,在WHERE子句中指明连接的列名及其连接条件

2、利用关键字JOIN进行连接,通过关键词ON与JOIN相对应,指明连接条件。具体分为以下几种:

交叉连接

  • Cross join产生了一个笛卡尔集,其效果等同于在两个表进行连接时未使用WHERE子句限定连接条件;
  • 可以使用where条件从笛卡尔集中选出满足条件的记录。

用法举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!--两张表连接-->
select * from emp,dept
where emp.deptno=dept.deptno
--等价于
select * from emp cross join dept
where emp.deptno=dept.deptno
<!--三张表连接-->
select * from emp,dept,salgrade
where emp.deptno=dept.deptno and
(emp.sal>salgrade.lostl and emp.sal<salgrade.hisal)
--等价于
select * from emp "E"
cross join dept "D"
cross join salgrade "S"
where "E".deptno="D".deptno and
("E".sal>"S".lostl and "E".sal<"S".hisal)

自然连接

  • Natural join基于两个表中的全部同名列建立连接(连接两个table之后,两个table共用的属性就会合并在一起)
  • 如果连个table没有共有的属性,则进行笛卡尔乘积,也就是进行两两相乘,如果table 1有3行,table 2有4行,自然连接后就有12行。
    • 从两个表中选出同名列的值均对应相等的所有行
    • 如果两个表中同名列的数据类型不同,则出错
    • 不允许在参照列上使用表名或者别名作为前缀
    • 自然连接的结果不保留重复的属性

举例:

1
2
3
select empno, ename, sal, deptno, dname
from emp natural join dept
where deptno = 10;

Using子句

using等价于join操作中的on

  • 使用using必须满足如下两个条件:

    1. 查询必须是等值连接。

    2. 等值连接中的列必须具有相同的名称和数据类型。

  • using子句引用的列在sql任何地方不能使用表名或者别名做前缀。

  • 可以在using子句中指定多个列名

举例:

1
2
3
select * from my_test_user a , my_test_teacher b where a.userid = b.userid;
=
select * from my_test_user inner join my_test_teacher using(userid);

join连接

img

  • INNER JOIN:返回符合连接条件的记录;
  • LEFT [OUTER] JOIN:返回符合连接条件的数据行以及左边表中不符合条件的数据行,此时右边数据行以NULL来显示,称为左连接;
  • RIGHT [OUTER] JOIN:返回符合连接条件的数据行以及右边表中不符合条件的数据行,此时左边数据行以NULL来显示,称为右连接;
  • FULL [OUTER] JOIN:返回符合连接条件的数据行以及左边表和右边表中不符合条件的数据行,此时缺乏数据的数据行会以NULL来显示

on子句

外连接

LEFT JOIN、RIGHT JOIN与 FULL JOIN统称为外连接。可用来显示不满足连接条件的元组。某些查询要求只能用外连接来表达

嵌套查询

一个SELECT-FROM-WHERE语句称为一个查询块;将一个查询块嵌套在另一个查询块的WHERE子句或HAVING短语的条件中的查询称为嵌套查询,相当于在SELECT中调用另一段SELECT。

  • 子查询不能使用ORDER BY子句;
  • 层层嵌套方式反映了SQL语言的结构化;
  • 有些嵌套查询可以用连接运算替代
  • AS的作用是起别名

子查询

  • 特点
    • 子查询在主查询前执行一次
    • 主查询使用子查询的结果
  • 使用子查询注意事项
    • 在查询是基于未知值时应考虑使用子查询
    • 子查询必须包含在括号内
    • 建议将子查询放在比较运算符的右侧,以增强可读性。
    • 除非进行Top-N 分析,否则不要在子查询中使用ORDER BY 子句。
    • 如果子查询返回单行结果,则为单行子查询,可以在主查询中对其使用相应的单行记录比较运算符
    • 如果子查询返回多行结果,则为多行子查询,此时不允许对其使用单行记录比较运算符

单行子查询

  • 单行子查询只返回一行记录
  • 对单行子查询可使用单行记录比较运算符
  • < 、 > 、 = 、 >=、 <= 、 <>

多行子查询

  • 多行子查询返回多行记录

  • 对多行子查询只能使用多行记录比较运算符

    • ALL 和子查询返回的所有值比较
    • ANY 和子查询返回的任意一个值比较
    • IN 等于列表中的任何一个

带EXISTS谓词的子查询

EXISTS:本质上是一个返回值为“真”/“假”的集函数,用于判断一个集合是否为空。
EXISTS(R),R为非空则返回真。

NOT EXISTS:语义与EXISTS函数相反的逻辑函数。

带EXISTS的子查询的用法:

1.不同形式的查询间的替换:带IN谓词、比较运算符、ANY和ALL谓词的子查询 与 带EXISTS谓词的子查询 等价替换

2.用EXISTS/NOT EXISTS实现全称量词(难点)

3.用EXISTS/NOT EXISTS实现逻辑蕴函(难点)

作用:把带有全称量词的谓词转换为等价的带有存在量词的谓词

数据更新

插入数据

1
2
3
INSERT INTO <表名> [(<属性列1>[,<属性列2>]...)]
VALUES (<常量1>[,<常量2>]....)
| SELECT子查询;

INTO子句 属性列的顺序可与表定义中的顺序不一致,可以不指定或指定部分属性列

VALUES子句 提供的值必须与INTO子句匹配

修改数据

1
2
3
UPDATE<表名> 
SET<列名>=<表达式>[, <列名>=<表达式>]...
[WHERE<条件>];

SET子句给出<表达式>的值用于取代相应的属性列值。

省略WHERE子句,则表示要修改表中的所有元组。

删除数据

1
2
3
DELETE 
FROM<表名>
[WHERE<条件>];

视图

  • 视图的特点:
    • 是从一个或几个基本表(或视图)导出的表,是虚表
    • 数据库只存放视图的定义,不存放视图对应的数据
    • 基表中的数据发生变化,从视图中查询出的数据也随之改变
  • 基于视图的操作:
    • 查询
    • 删除
    • 定义基于该视图的新视图
    • 受限更新

创建视图

创建视图语句格式:

1
2
3
CREATE  VIEW <视图名>  [(<列名>  [,<列名>]...)]
AS<子查询>
[WITH CHECKOPTION];

其中,

  • 组成视图的属性列名:全部省略或全部指定
  • 子查询是否可含有ORDER BY子句和DISTINCT短语,取决于具体系统的实现。
  • WITH CHECK OPTION表示对视图进行增删改时要保证删除、插入、更新的行满足视图定义中的谓词条件(即子查询中的条件表达式)。

删除视图

删除视图语句格式:

1
DROP  VIEW  <视图名> [CASCADE];

从数据字典中删除指定的视图定义。

  • 如果该视图上还导出了其他视图,使用CASCADE级联删除语句,把该视图和由它导出的所有视图一起删除
  • 删除基表时,由该基表导出的所有视图定义都必须显式删除

查询视图

RDBMS采用视图消解法实现视图查询。

更新视图

视图不实际存储数据,因此,对视图的更新最终要转换为对基本表的更新。为防止用户通过视图更新,对不属于视图范围内的基本表数据进行更新操作,在定义视图时加上WITH CHECK OPTION操作。

  • 一般RDBMS允许对行列子集视图进行更新
  • 对其他类型视图的更新不同系统有不同限制

作用

  • 视图能够简化用户的操作
  • 视图使用户能以多种角度看待同一数据
  • 视图对重构数据库提供了一定程度的逻辑独立性
  • 视图能够对机密数据提供安全保护
  • 适当的利用视图可以更清晰的表达查询

key、index、primary key和unique key

[参考文章](https://www.cnblogs.com/zjfjava/p/6922494.html

SQL 约束(Constraints)

SQL 约束用于规定表中的数据规则。

如果存在违反约束的数据行为,行为会被约束终止。

约束可以在创建表时规定(通过 CREATE TABLE 语句),或者在表创建之后规定(通过 ALTER TABLE 语句)。

SQL CREATE TABLE + CONSTRAINT 语法

1
2
3
4
5
6
7
CREATE TABLE table_name
(
column_name1 data_type(size) constraint_name,
column_name2 data_type(size) constraint_name,
column_name3 data_type(size) constraint_name,
....
);

在 SQL 中,我们有如下约束:

  • NOT NULL - 指示某列不能存储 NULL 值。
  • UNIQUE - 保证某列的每行必须有唯一的值。
  • PRIMARY KEY - NOT NULL 和 UNIQUE 的结合。确保某列(或两个列多个列的结合)有唯一标识,有助于更容易更快速地找到表中的一个特定的记录。
  • FOREIGN KEY - 保证一个表中的数据匹配另一个表中的值的参照完整性。
  • CHECK - 保证列中的值符合指定的条件。
  • DEFAULT - 规定没有给列赋值时的默认值。

关系模型概述

关系数据理论是建立在集合代数理论基础上的,有着坚实的数学基础

  • 数据结构:二维表
  • 关系操作:
    • 增加(insert)、删除(delete)、修改(updated)
    • 查询(Query):•选择(select)、投影(project)、连接(join)
      • 除(divide)、并(union)、交(intersection)
      • 差(difference)
    • 关系代数,关系演算,SQL
  • 关系的三类完整性约束:
    • 实体完整性、参照完整性、用户自定义的完整性

关系代数是用对关系的运算来表达查询要求的方式。

关系演算是用谓词表达查询要求的方式。

  • 按谓词变元的基本对象是元组变量还是域变量分为元组关系演算和域关系演算

关系数据结构

    • 域是一组具有相同数据类型的值的集合
  • 笛卡儿积

    • 一组域D1 , D2 ,…, Dn的笛卡尔积为:D1×D2×…×Dn= {(d1 , d2 , … , dn) | di∈Di, i=1,…,n}

    • 笛卡尔积的每个元素(d1 , d2 , … , dn)称作一个n-元组(n-tuple)

    • 元组的每一个值di叫做一个分量(Component)

    • 若Di的基数(元组个数)为mi,则笛卡尔积的基数为
      $$
      \prod^n_{i=1}m_i
      $$

      一个域允许的不同取值个数称为这个域的基数

笛卡儿积可以表示成一张二维表。表中的每行对应一个元组,表中的每一列的值来自一个域。

  • 关系

    • 笛卡尔积D1×D2×…×Dn的子集叫做在域D1 , D2 ,…, Dn上的关系,用R(D1 , D2 ,…, Dn)表示

    • R是关系的名字,n是关系的度或目

      当n=1时,称该关系为单元关系

    • 关系是笛卡尔积中有意义的子集,关系中的每个元素是关系中的元组

      一般来说,笛卡儿积是没有实际语义的,只有它的某个真子集才有实际含义

    • 关系也可以表示为二维表

  • 属性

    • 关系中不同列可以对应相同的域,为了加以区分,必须对每列起一个名字,称为属性
    • n目关系必有n个属性

关系数据结构

关系的性质

  • 列是同质的;即每一列中的分量是同一类型的数据,来自同一个域。
  • 行列的顺序无关紧要;行列的次序是可以任意交换的
  • 任意两个元组不能完全相同
  • 每一分量必须是不可再分的数据
  • 不同的属性,属性名不能相同

候选码(Candidate Key)

  • 关系中的一个属性组,其值能唯一标识一个元组。若从属性组中去掉任何一个属性,它就不具有这一性质了,这样的属性组称作候选码如DEPT中的D#,DN都可作为候选码
  • 任何一个候选码中的属性称作主属性如SC中的S#,C#

主码(Primary Key)

  • 若一个关系有多个候选码,则选定其中一个作为主码如可选定D#作为DEPT的主码

外码(Foreign Key)

  • 关系R中的一个属性组,它不是R的码,但它与另一个关系S的码相对应,则称这个属性组为R的外码
    如S关系中的D#属性

关系模式

形式化的表示为R(U,D,DOM,F)

R为关系名,U为组成该关系的属性名集合;D为U中属性所来自的域;DOM为属性向域的映象集合;F为属性间数据的依赖关系集合。

  • 关系的描述称作关系模式,包括关系名、关系中的属性名、属性向域的映象、属性间的数据依赖关系等,记作R(A1 , A2 ,…, An )
  • 属性向域的映象一般直接说明为属性的类型、长度等
  • 某一时刻对应某个关系模式的内容(元组的集合)称作关系
  • 关系模式是型,是稳定的
    关系是某一时刻的值,是随时间不断变化的

关系数据库

  • 其型是关系模式的集合,即数据库描述,称作数据库的内涵(Intension),或关系数据库模式

  • 其值是某一时刻关系的集合,称作数据库的外延(Extension),或关系数据库

关系的完整性约束

实体完整性

  • 关系的主码中的属性值不能为空值
  • 空值:不知道或无意义
  • 意义:关系对应到现实世界中的实体集,元组对应到实体,实体是相互可区分的,通过主码来唯一标识,若主码为空,则出现不可标识的实体,这是不容许的

参照完整性

  • 如果关系R2的外码Fk与关系R1的主码Pk相对应,则R2中的每一个元组的Fk值或者等于R1 中某个元组的Pk值,或者为空值【即属性Fk本身不是主属性,则可以取空值,否则不能取空值。】
  • 意义:如果关系R2的某个元组t2参照了关系R1的某个元组t1,则t1必须存在
  • 例如:关系S在D#上的取值有两种可能
    • 空值,表示该学生尚未分到任何系中
    • 若非空值,则必须是DEPT关系中某个元组的D#值,表示该学生不可能分到一个不存在的系中
      DEPT(D#, DN , DEAN)S(S#, SN , SEX , AGE , D#)

用户定义的完整性

  • 用户定义的完整性

    • 用户针对具体的应用环境定义的完整性约束条件

    • 如S#要求是8位整数,SEX要求取值为“男”或“女”

  • 系统支持

    • 实体完整性和参照完整性由系统自动支持
    • 系统应提供定义和检验用户定义的完整性的机制

关系代数概述

  • 属于关系操作的一种
  • 关系代数是一种抽象的查询语言
  • 通过对关系的运算来表达查询操作
  • 运算对象、结果均为关系
  • 运算包括四类:
    • 集合运算、关系运算、比较运算、逻辑运算

集合运算

并运算

  • 所有至少出现在两个关系中之一的元组集合

  • 两个关系R和S若进行并运算,则它们必须是相容的:

    • 关系R和S必须是同元的,即它们的属性数目必须相同
    • 对i,R的第i个属性的域必须和S的第i个属性的域相同

差运算

所有出现在一个关系而不在另一关系中的元组集合

R和S必须是相容的

交运算

  • 所有同时出现在两个关系中的元组集合
  • 交运算可以通过差运算来重写

基本关系运算

选择运算

在关系R中选择满足给定条件的元组(从行的角度)

投影运算

投影的结果中要去掉相同的行

广义笛卡儿积

  • 元组的连串(Concatenation)

    • 若r = (r1,…,rn),s = (s1 ,… ,sm),则定义r与s的连串为:rs = (r1,…,rn,s1 ,…,sm)
  • 广义笛卡尔积两个关系R,S,其度分别为n,m,则它们的笛卡尔积是所有这样的元组集合:元组的前n个分量是R中的一个元组,后m个分量是S中的一个元组
    RxS={rs|r属于R并且s属于S}

  • RxS的度为R与S的度之和,RxS的元组个数为R和S的元组个数的乘积

其它关系运算

0连接就是有附加条件的自然连接。

外连接

把上述中舍弃的元组也保存在结果关系中,在其他属性上填上NULL。

左外连接:只把左边关系R中要舍弃的元组保留。同理:右外连接。

除运算

把S和R中相同的属性组拿出来,然后取R中独有的属性

句柄和规范归约

归约:根据文法的产生式规则,把串中出现的产生式的右部替换成左部符号

从语法树的角度看,是从叶子出发逐步向上进行构造

句柄:最左两代子树末端就是句柄

规范归约的关键是寻找句柄:

1、根据语法树找句柄

2、在栈中根据三类信息:

  • 历史:已经移入符号栈的内容
  • 展望:根据产生式推迟未来可能遇到的输入符号
  • 现实:当前的输入符号

LR分析

LR分析法也是一种“移进—归约”的自底向上语法分析方法,其本质是规范归约【句柄作为可归约串】

其思想为一方面记住已移进和归约出的整个符号串,另一方面根据所用产生式推测未来可能碰到的输入符号。

LR分析方法:把“历史”以及“展望”综合抽象成状态;由栈顶的状态和现行的输入符号唯一确定每一步工作。

解释:每一个符号对应一个状态,分析栈每次弹出一个符号,就要把对应的状态也弹出。然后LR分析程序会根据输入串在LR分析表中进行查找:是进行归约、移进还是报错操作。

LR分析器实质上是一个带先进后出存储器(栈)的确定有限自动机,其核心部分是一张分析表,包括两部分:

(1)ACTION[s,a]动作表,规定当状态s面临输入符号a时,应采取什么动作(移进、归约、接受、 报错)【也就是告诉我们当栈顶状态为s时,输入的符号是a时,我们应该采取什么操作:归约、移进还是报错

(2)GOTO[s,X]状态转换表规定了状态s面对文法符号X时,下一状态是什么。【当归约完了后,要把规约后的非终结符压到栈里面的时候,跟新压入栈的这个非终结符所对应的状态是什么

LR文法

对于一个文法,如果能够构造一张分析表,使得它的每个入口均是唯一确定的,则这个文法就是LR文法

一个文法,如果能用一个每步顶多向前检查k个输入符号的LR分析器分析,则这个文法就称为LR(K)文法

LR文法不是二义的

LR(0)分析

拓广文法

对于文法 G = (VN, VT, P , S ) , 增加如下产生式:S’->S ,其中, S’ ∈ VN∪ VT , 得到 G 的拓广文法,G’ = (VN ’, VT, P ’ , S’ )

其实就是增加了一条右部为开始符号的产生式,就变成了拓广文法

可归前缀

将符号串的任意含有头符号的子串称为前缀。特别地,空串ε为任意串的前缀。

采取归约过程前符号栈中的内容,称做可归前缀。
这种前缀包含句柄且不包含句柄之后的任何符号;

活前缀

就是在LR分析中为了描述栈内符号的特点给出的概念

对于文法 G = (VN, VT, P , S ) , 设 S’ 是其拓广文法的开始符号(即有产生式 S’-> S), 且α,β∈(VN∪VT)* , ω∈VT
若 S’ =^
^=>α A ω 且 A ->β, 即 β 为句柄,则 αβ 的任何前缀 γ 都是文法 G 的活前缀。【活前缀就是不含句柄之后任何符号的前缀
注:由于 S’ =^*^=>S’ 且 S’ -> S, 故 S 是 G 的活前缀 。

也就是说可归前缀的所有前缀(包括可归前缀)都是活前缀。

例:文法 G[S] :
(1) S -> AB
(2) A -> aA
(3) A -> ε
(4) B -> b
(5) B -> bB
句子 aaab 是一个句型,其唯一的句柄为:ε (aaaεb); 活前缀有:ε,a,aa,aaa。

规范归约过程中,保证分析栈中总是活前缀,就说明分析采取的移进/归约动作是正确的

LR(0)项目

对于文法G,其产生式右部添加一个特殊的符号“.”,就构成文法的一个LR(0)项目,简称项目

每个项目的含义是:欲用改产生式归约时,圆点前面的部分为已经识别了的句柄部分,圆点后面的部分为期望的后缀部分。

分类:

移进项目: 形如 A -> α • aβ(a∈VT),对应移进状态,把a移进符号栈。
待约项目: 形如 A -> α • Bβ,对应待约状态,需要等待分析完非终结符B的串再继续分析A的右部。
归约项目: 形如 A -> α •,句柄已形成,可以归约。
接受项目: 形如 S’ -> S •。【也是一个归约项目,表示整个句子已经识别完毕】
初始项目: 形如 S’ -> • S。
其中a∈VT , α,β∈(VN∪VT)*, A,B∈VT
后继项目: 表示同属于一个产生式的项目,但是圆点的位置仅相差一个文法符号,则称后者为前者的后继项目。

• 的左边是已经归约,右边是没有归约的

例:对于产生式S -> aAcBe,它有6个项目:
S -> ·aAcBe
S -> a·AcBe
S -> aA·cBe
S -> aAc·Be
S -> aAcB·e
S -> aAcBe·

LR(0)有限状态机的构造方法

构造识别活前缀的NFA

NFA的构造方法

(1)状态集:由每个项目所对应的状态组成的集合;

(2)输入字符集合:由文法符号组成,包括:终结符、非终结符和e;

(3)初态:对于文法G[S]的拓广文法G[S’],有项目S’® . S ,由于S’ 仅在第一个产生式的左部出现,所以规定它为初态;

(4)终态:每个状态均为NFA的终态(活前缀的识别态)。

  • 若状态i为X->X1····Xi-1•Xi····Xn;状态j为X->X1····Xi-1Xi•Xi+1····Xn

    则从状态i画一条标志为Xi的有向边到状态j

  • 若状态i为X->α• Aβ,A为非终结符,
    则从状态i画一条ε边到所有状态A->• y【可以理解成X想要把A进行归约,然后画一条ε边,交给A需要归约的符号】

NFA转换成DFA

DFA是用子集法得到的

求项目集规范族

每个项目集对应一个DFA状态,它们的全体称为这个文法的项目集规范族

有效项目

项目A->β1• β2对活前缀αβ1是有效的,其条件就是存在规范推导

1.用闭包函数(CLOSURE)来求DFA一个状态的项目集

I是拓广文法G的任意项目集:
CLOSURE(I)是这样定义的:
首先I的项目都属于CLOSURE(I);
如果A->α• Bβ,则左部为B的每个产生式中的形如B->·γ项目,也属于CLOSURE(I);

例子:已知文法G[E]如下:
(1) E -> E+T
(2) E -> T
(3) T ->( E )
(4) T -> d

可以直到它的拓广文法G’ [E’]为 :
(0) E’ -> E
(1) E -> E+T
(2) E -> T
(3) T -> ( E )
(4) T -> d

令I0 = CLOSURE({E’->.E})

则I0 = {
E’ -> • E,
E -> • E+T,
E -> • T,
T -> •( E ),
T -> • d
}

2.LR(0) FSM 的状态转移函数

从一个状态出发,到达下一个状态的转换函数:
GO (I,X) = CLOSURE(J)
其中,I为LR(0) FSM 的状态(闭包的项目集),X为文法符号, J={ A -> αX•β | A -> α• Xβ∈I} ;【J是非终结符包进去后的集合】
表示对于一个状态项目集中的一个项目A -> α• Xβ,在下一个输入字符是X的情况下,一定到另一个新状态 A -> αX•β。

3.LR(0) 有限状态机的构造

从 LR(0) FSM 的初态出发 ,先求出初态项目集的闭包(CLOSURE({S’->.S})),然后应用上述转移函数,通过项目分析每种输入字符下的状态转移,若为新状态,则就求出新状态下的项目集的闭包,级可逐步构造出完整的 LR(0) FSM。

1
2
3
4
5
6
7
8
9
10
构造项目集规范族的算法:
PROCEDURE ITEMSET(G');
BEGIN
C:{CLOSURE{S'->.S}};
REPEAT
FOR C中的每一个项目集I和G'的每一个符号X DO
IF GO(I,X)非空且不属于C THEN
把GO(I,X)放入C族中
UNTIL C不再增大
End

LR(0) FSM 的构造举例
给定文法G[E]:
(1) E -> E+T
(2) E -> T
(3) T -> ( E )
(4) T -> d

构造LR(0) FSM
① G[E]的拓广文法,得到G’ [E’]:
(0) E’ -> E
(1) E -> E+T
(2) E -> T
(3) T -> ( E )
(4) T -> d

②构造G’[E’] 的 LR(0) FSM

两种构造识别活前缀的DFA的方法:

  • 项目->NFA->DFA
  • Closure->GO->DFA

LR(0) 分析法

1.LR(0) 文法定义

文法 G 是 LR(0) 文法,当且仅当它的LR(0)FSM中的每个状态都满足:
①不同时含有移进项目和归约项目,即不存在移进-归约冲突。
②不含有两个以上归约项目,即不存在归约-归约冲突。

2.LR(0)分析表的构造

令每个项目集Ik的下标k作为分析器的状态

ACTION 表项和 GOTO表项可按如下方法构造:

  • 若项目A ->α • aβ属于 Ik 且 GO (Ik, a)= Ij, 期望字符a 为终结符,则置ACTION[k, a] =sj (j表示新状态Ij);

  • 若项目A ->α •属于Ik, 那么对任何终结符a, 置ACTION[k, a]=rj;其中,假定A->α为文法G 的第j个产生式;【对k到a进行归约】

  • 若项目S’ ->S • 属于Ik, 则置ACTION[k, #]为“acc”;【单词处理完毕】

  • 若项目A ->α • Aβ属于 Ik,且GO (Ik, A)= Ij,期望字符 A为非终结符,则置GOTO(k, A)=j (j表示文法中第j个产生式);

  • 分析表中凡不能用上述规则填入信息的空白格均置上“出错标志”

    翻译一下:

    1. 如果圆点不在项目k最后且圆点后的期待字符a为终结符,则ACTION[k, a] =sj (j表示新状态Ij);
    2. 如果圆点不在项目k最后且圆点后的期待字符A为非终结符,则GOTO(k, A)=j (j表示文法中第j个产生式);
    3. 如果圆点在项目k最后且k不是S’ ->S,那么对所有终结符a,ACTION[k, a]=rj (j表示文法中第j个产生式);
    4. 如果圆点在项目k最后且k是S’ ->S,则ACTION[k, #]为“acc”;

例子:

考虑文法G[S] :
S → (S) | a
相应的LR(0) FSM如下,构造其LR(0)分析表。

LR(0) FSM

从I0看,S‘->·S,期望字符是非终结符S,根据上面的规则2,得到GOTO(0,S)=1;
S‘->·(S),期望字符是终结符(,根据上面的规则1,得到ACTION(0,()=S2
从I3看,S->a·,根据规则3,置ACTION[3, a]为r2;
从I1看,S‘->S·,根据规则4,置ACTION[1, #]为“acc”;

LR(0)分析表

3.LR(0) 分析流程

设输入串为w,ip指向输入串w的首符号a,i指向符号栈顶;状态栈的初始栈顶为0,符号栈初始栈顶为#。

算法流程图为:

SLR(1)分析法

1.SLR(1)解决的问题

LR(0)文法的要求是①不同时含有移进项目和归约项目,即不存在移进-归约冲突。②不含有两个以上归约项目,即不存在归约-归约冲突。

例如项目集Ii中存在: Ii ={A->α•bγ , B→ γ•,C→β• },此时就同时存在移进-归约冲突和归约-归约;因为你不知道下一步是选择归约还是移进,选择归约的话选择哪个产生式归约。

而事实上一般文法满足这种要求有一定难度,但是假如在归约时出现了移进-归约冲突或者归约-归约冲突,我们可以通过在待分析的字符串中向后再看一位,大多数情况下通过这一位字符就可以确定,选择哪一个表达式归约,或是移进操作。

这种方法就叫做SLR(1)分析法,即简单的LR(1)分析法。

2.SLR(1)冲突解决方法

如上面所述,我们需要知道下一位待分析的字符,然后和现有项目进行比较。

分析过程与LR(0)一样,但是需要解决分析表上的冲突问题。

假如LR(0) 项目集规范族中有项目集 Ii 含有移进-归约冲突和归约-归约冲突:
Ii ={A1->α1•b1γ1,… , Am->m•bm γm, B1->β1• ,…, Bn-> βn• },若集合{b1 ,b2,… ,bm }、FOLLOW(B1) 、 FOLLOW(B2) ,…,FOLLOW(Bn)均两两不相交,则可用SLR(1)解决方法解决分析表上第i行上的冲突问题。

假设下一个移进的字符为b:

1、若b∈ {b1 ,b2,… ,bm },则移进输入符;

2、若b∈FOLLOW(Bj) ,j=1 ,… ,n,则用Bj-> βj 归约;

3、此外,报错

通过这个方法,就可以在知道下一位待分析的字符的情况下,解决冲突。

继续采用SLR(1)分析的方法,我们可以对出错情况进行优化:

在LR(0)和SLR(1)分析中,我们在可以归约且没有冲突时(假如归约成A),是不关心下一位待分析的字符a和follow(A)的关系的,假如a!∈follow(A)则当前字符串是不被接受的,当然这会在之后的继续移进字符过程中发现错误,但是如果不管是否有冲突看,将SLR(1)分析方法应用到所有分析表的构建过程中,可以提前发现字符串的错误。

3.构造SLR(1)分析表的方法

1、把G扩广成G’

2、对G’构造:得到LR(0)项目集规范族C;活前缀识别自动机的状态转换函数GO

3、使用C和GO,构造SLR分析表:构造action和goto子表:

  • 若项目A ->α • aβ属于 Ik 且 GO (Ik, a)= Ij,期望字符a为终结符,则置ACTION[k, a] =sj (j表示新状态Ij);
  • 若项目A ->α • Aβ属于 Ik,且GO (Ik, A)= Ij,期望字符 A为非终结符,则置GOTO(k, A)=j (j表示文法中第j个产生式);
  • 若项目A ->α •属于Ik, 那么对任何终结符a,当满足a属于follow(A)时, 置ACTION[k, a]=rj;其中,假定A->α为文法G 的第j个产生式;
  • 若项目S’ ->S • 属于Ik, 则置ACTION[k, #]=“acc”;
  • 分析表中凡不能用上述规则填入信息的空白格均置上“出错标志”

SLR分析是有选择的放置的

4.SLR(1)文法

按上述方法构造出的ACTION和GOTO表如果不含多重入口,则称该文法是SLR(1)文法。

使用SLR表的分析器叫做一个SLR分析器

每个SLR(1)文法都是无二义的。
$$
LR(0)\subset SLR(1)\subset 无二义文法
$$

5.SLR(1)分析的例子

算术表达式文法G[E]:
E→E +T | T
T→T * F | F
F→ (E)| i
求此文法的识别规范句型活前缀的DFA,分析句子i+i *i。

  1. 将文法拓广为G’[E’]:
    (0) E’ ->E
    (1) E-> E +T
    (2) E ->T
    (3) T ->T * F
    (4) T ->F
    (5) F ->(E)
    (6) F ->i
  2. 构造识别规范句型活前缀的DFA

  1. 判断有无冲突:

    I1 ,I2 ,I9有移进_归约冲突。
    I1:E´ ->E· E ->E·+T
    I2: E ->T · T ->T · *F
    I9: E ->E+T· T ->T · *F

  2. 考虑能否用SLR(1)方法解决冲突:

    对于I1: { E´ ->E· E ->E·+T} 因为:{+} ∈FOLLOW(E´)= {+} ∩ {#} =∅, 所以可用SLR(1)方法 解决I1的冲突。

    对于I2: {E ->T· T ->T·*F} 因为:{*} ∈ FOLLOW(E)= {*} ∩ {#,),+} = ∅ 所以可用SLR(1)方法解决I2的冲突。

    对于I9: {E ->E+T· T ->T ·*F} 因为:{*} FOLLOW(E)= ∅, 所以可用SLR(1)方法解决I9的冲突。

  3. 构建分析表:

6.句子i+i *i的分析过程:

步骤 状态栈 符号栈 输入符 剩余输 入串 查表 操作
1 0 # i i+i*i# Action[0,i]=S5 移进i
2 0 5 # i + +i*i# Action[5,+]=r6,GOTO(0,F)=3 用F -> i 归约
3 0 3 # F + +i*i# Action[3,+]=r4,GOTO(0,T)=2 用F -> T归约
4 0 2 # T + +i*i# Action[2,+]=r4,GOTO(0,E)=1 用F -> E归约
5 0 1 # E + +i*i# Action[1,+]=S6 移进+
6 0 1 6 # E + i i*i# Action[6,i]=S6 移进+
7 0 1 6 5 # E + i * *i# Action[5,*]=r6,GOTO(6,F)=3 用F -> i 归约
8 0 1 6 3 # E + F * *i# Action[3,*]=r6,GOTO(6,T)=9 用F -> F 归约
9 0 1 6 9 # E + T * *i# Action[9,*]=S7 移进*
10 0 1 6 9 7 # E + T * i i# Action[7,i]=S5 移进i
11 0 1 6 9 7 5 # E + T * i # # Action[5,#]=r6,GOTO(7,F)=10 用F -> i 归约
12 0 1 6 9 7 10 # E + T * F # # Action[10,#]=r3,GOTO(6,T)=9 用T -> T+F归约
13 0 1 6 9 # E + T # # Action[9,#]=r1,GOTO(0,E)=1 用E -> E+T归约
14 0 1 # E # # Action[1,#]=acc 接受

LR(1)分析法

SLR冲突消解存在的问题

在SLR方法中,如果项目集Ii含项目A ->α •而且下一输入符号α∈FOLLOW(A),则状态i面临α时,可选用“用A ->α归约”动作。

但是在某些情况下,当状态i显现于栈顶时,当前单词是α,栈里的活前缀βα未必允许把α归约为A,因为可能根本不存在一个形如“βAα”的规范句型。在这种情况下,用“A ->α”归约不一定合适

【因为FOLLOW(A)集合提供的信息太泛】

构造LR(1)分析表的方法

1、把G扩广成G’

2、对G’构造:得到LR(1)项目集规范族C;活前缀识别自动机的状态转换函数GO

3、使用C和GO,构造LR(1)分析表:

LR(K)项目:扩展LR(0)项目,附带有k个终结符[A->α •β,α1α2····αk]
α1α2····αk称为向前搜索符串(或展望串)

归约项目[A->α •,α1α2····αk]的意义:当它所属的状态呈现现在栈顶且后续的k个输入符号为α1α2····αk时,才可以把栈顶上的α归约为A

对于任何移进或待约项目[A->α •β,α1α2····αk],β不等于α,搜索符串α1α2····αk没有直接作用

1
2
3
4
5
6
7
8
9
10
构造项目集规范族的算法:
PROCEDURE ITEMSET(G');
BEGIN
C:{CLOSURE{S'->.S,#}};
REPEAT
FOR C中的每一个项目集I和G'的每一个符号X DO
IF GO(I,X)非空且不属于C THEN
把GO(I,X)放入C族中
UNTIL C不再增大
End
  • 若项目[A ->α • aβ,b]属于 Ik 且 GO (Ik, a)= Ij,期望字符a为终结符,则置ACTION[k, a] =sj (j表示新状态Ij);
  • 若项目[A ->α •,a]属于Ik, 那么对任何终结符a,当满足a属于follow(A)时, 置ACTION[k, a]=rj;其中,假定A->α为文法G 的第j个产生式;
  • 若项目[S’ ->S • ,#]属于Ik, 则置ACTION[k, #]=“acc”;
  • 若GO[Ik,A]=Ij,则置GOTO[K,A]=j
  • 分析表中凡不能用上述规则填入信息的空白格均置上“出错标志”

$$
LR(0)\subset SLR(1)\subset LR(1)\subset 无二义文法
$$

(SLR(1)和LR(1)的区别在于LR(1)多使用了一个预判信息,即项目后面的符号如A →·e,c中的c,这个预判信息是用first集而非follow集得出的)

LR(0)、SLR(1)、LL(1)等的区别

LL(1)定义:一个文法G是LL(1)的,当且仅当对于G的每一个非终结符A的任何两个不同产生式 A→α|β,下面的条件成立:SELECT( A→α)∩SELECT( A→β)=,其中,

α|β不能同时ε。

解释:LL(1)的意思是,第一个L,指的是从左往右处理输入,第二个L,指的是它为输入生成一个最左推导。1指的是向前展望1个符号。LL(1)文法是上下文无关文法的一个子集。它用的方法是自顶向下的(递归式的处理)。它要求生成的预测分析表的每一个项目至多只能有一个生成式。上面的定义说的是,任何两个不同的产生式 A→α和 A→β,选择A→α或者 A→β是不能有冲突的,即SELECT( A→α)∩SELECT( A→β)=,具体来说,就是,第一:First( A→α) ∩ First( A→β)=,首符集不能有交集,否则当交集中的元素出现时,选择哪个产生式进行推导是不确定的,(这其中也包含了α|β不能同时ε,否则交集就是{ε}不为空),第二:若任何一个产生式β,有ε属于First(β),应有First(A)∩Follow( A)为空(当ε属于First(β),则A有可能被空串代替,那么就要看A的下一个字符,即Follow集,即要求Follow集和First集不能相交,否则可能发生冲突)。

LR文法:定义:如果某一文法能够构造一张分析表,使得表中每一个元素至多只有一种明确动作,则该文法称为LR文法。

拓展:由上面的定义可以看到,LL(1)和LR文法都是无二义性的:LL(1)要求生成的预测分析表的每一个项目至多只能有一个生成式,即对于读头下的每一个字符,都可以明确地选择哪个产生式来推导,LR文法要求每一步都有明确的动作,移进和归约都是可确定的,没有二义性。

比较两大类型(自顶向下 vs 自底向上)的文法的特点:

1.首先LL(1)分析法是自上而下的分析法。LR(0),LR(1),SLR(1),LALR(1)是自下而上的分析法。
2.自上而下:从开始符号出发,根据产生式规则推导给定的句子。用的是推导
3.自下而上:从给定的句子规约到文法的开始符号。用的是归约
4.自上而下就是一种试探过程,怎么试探?需要你写出它的FIRST()集与FOLLOW()集。写出这两个集合后根据LL(1)分析表构造规则画出LL(1)分析表。现在基本完成了大半,当计算机输入句子时,分析程序便会根据输入去和分析表进行匹配,如果每步都能够匹配成功则说明符合该语法规则,分析成功。
FIRST()集:其实是终结符的集合,看该非终结符A能不能产生以它里面的某个符号开头的句子。(这也是自上而下分析法的思想)
5.自下而上就是把句子变成非终结符,在把非终结符变成非终结符,这样不断的进行如果能到根节点则成功。

LL(1)就是向前只搜索1个符号,即与FIRST()匹配,如果FIRST为空则还要考虑Follow。
  LR需要构造一张LR分析表,此表用于当面临输入字符时,将它移进,规约(即自下而上分析思想),接受还是出错。
LR(0)找出句柄前缀,构造分析表,然后根据输入符号进行规约。不考虑先行,只要出现终结符就移进,只要出现归约状态,就无条件归约,这样子可能出现归约-移进,归约-归约冲突。
SLR(1)使用LR(0)时若有归约-归约冲突,归约-移进冲突,所以需要看先行,则只把有问题的地方向前搜索一次。

SLR(1)定义:满足下面两个条件的文法是SLR(1)文法

a.对于在s中的任何项目 A→α.Xβ,当X是一个终结符,且X在Follow(B)中时,s中没有完整的项目B→r.

b.对于在s中的任何两个完整项目A→α.和 B→β.,Follow(A)∩Follow(B)为空。

解释:a.当X是一个终结符且X出现在读头上,对于项目 A→α.Xβ应该采用移进,若有完整的项目B→r.且Follow(B)中有X,当X出现在读头上时,此时应该归约,于是,就产生了移进和归约冲突

b.假设Follow(A)∩Follow(B)为{ X },对于A→α.,若Follow(A)[A后面的元素]出现时,应该归约,同理B也一样,于是,会产生归约-归约冲突,SLR(1)是为了消除LR(0)的两个冲突。
LR(1)1.在每个项目中增加搜索符。2.举个列子如有A->α.Bβ,则还需将B的规则也加入。
LALR(1)就是假如两个产生式集相同则将它们合并为一个,几合并同心集

总结:

见到First集就移进,见到Follow集就归约。

LR(0):见到First集就移进,见到终态就归约

SLR(1)见到First集就移进,见到终态先看Follow集,与Follow集对应的项目归约,其它报错。

SLR分析法包含的展望信息是体现在利用了Follow(A)信息,可以解决“归约-归约”冲突

SLR分析法没有包含足够的展望信息,不能完成解决“移进-归约”冲突,需要改进。

下面是LR(0),SLR(1),LALR(1),LR(1)文法处理能力的比较,圆圈越大说明能力越强。

词法分析程序设计

词法分析任务

词法分析阶段是编译的第一阶段,它的主要任务是从左至右扫描文本格式的源程序,从基于字符理解的源程序中分离出符合源语言词法的单词,最终转换成基于单词理解的源程序。

输出形式为: (单词种类,单词)

单词种类类似于自然语言的词性,由构词规则等因素确定的。

计算机高级语言一般都有关键字、标识符、常数、运算符和定界符这5类单词。

词法分析程序和语法分析程序的接口方式

词法分析程序通常与后阶段语法分析程序接口有下列两种方式。

⑴词法分析程序和语法分析程序各自独立一趟方式。即词法分析程序把字符流的源程序转换成单词流的内部程序形式,供语法分析程序之用。

⑵词法分析程序和语法分析程序合并为一趟方式。即词法分析程序由反复语法分析程序调用,每调用一次从源程序中一个新单词返回给语法分析程序。

第一种方式的效率比较低。

单词的描述工具

基于生成观点、计算观点和识别观点,分别形成了正规文法、正规式和有穷自动机 3种用于描述计算机高级语言词法的工具。

正规文法

对应的是生成观点。

左线性/右线性正规文法描述。

正规式

对应的是计算观点。

基于字母表∑上的正规式(也称为正则表达式)定义如下,正规式e的计算值称为正规集,记为L(e)。

  1. ε是∑上的正规式,L(ε)= {ε} 【ε是空串】

  2. Ф是∑上的正规式,L(Ф)=Ф【Ф和ε不一样,它表示的是空集,对应的是实体的完整性】

  3. 任何a∈∑,a是∑上的正规式,L(a)= {a}

  4. 如果e1和e2是∑上的正规式,则

    4.1. (e1)是∑上的正规式,L((e1))=L(e1)【直接脱括号】

    4.2. e1︱e2 是∑上的正规式,L(e1︱e2)=L(e1)∪L(e2)

    4.3. e1 · e2 是∑上的正规式,L(e1· e2)=L(e1)·L(e2)

    4.4. e1* 是∑上的正规式,L(e1*)=L(e1)* 【闭包运算】

字母表∑1和∑2的乘积( product):

  ∑1∑2 ={ab|a ∈∑1, b ∈ ∑2}

  例: {0, 1} {a, b} ={0a, 0b, 1a, 1b}

字母表∑的n次幂( power):长度为n的符号串构成的集合

  ∑0 ={ ε }
  ∑n =∑n-1 ∑ , n ≥0

  例: {0, 1}3 ={0, 1} {0, 1} {0, 1}={000, 001, 010, 011, 100, 101, 110, 111}

字母表的正闭包(positive closure):长度正数的符号串构成的集合:

  ∑+ = ∑ ∪∑2 ∪∑3 ∪…

  例:{a, b, c, d }+ = {a, b, c, d,aa, ab, ac, ad, ba, bb, bc, bd, …, aaa, aab, aac, aad, aba, abb, abc, …}

字母表的克林闭包(Kleene closure):任意符号串(长度可以为零)构成的集合:

  ∑* = ∑0 ∪∑+ = ∑0 ∪∑ ∪∑2 ∪∑3 ∪…

  例:{a, b, c, d }* = {ε, a, b, c, d,aa, ab, ac, ad, ba, bb, bc, bd, …, aaa, aab, aac, aad, aba, abb, abc, …}

例 3.1 令∑={a,b},则∑上正规式的例子如下,

​ a、a︱b、ab、(a︱b)* 、(a︱b)*a,

且 L(a)={a}

L(a︱b)=L(a)∪L(b)= {a}∪{b} = {a,b}

L((a︱b)*)=L(L(a︱b))*=({a,b})*={a,b}*

L((a︱b)*a)=L((a︱b)*)·L(a)= {a,b}*{a}【集合运算更有利于计算机的操作】

两个正规式e1和e2相等,是指正规式e1和e2计算值相等(即L(e1)= L(e2)),记为e1= e2

设r,s,t为正规式,则正规式有如下定律:

1. 交换律:r︱s = s︱r

2. 结合律:(r︱s)︱t = r︱(s︱t)

(r·s)·t = r·(s·t)

3. 分配律:r·(s︱t)= r·s︱r·t

(s︱t)·r = s·r︱t·r

(1) 描述“标识符”单词的正规式

a(a︱b)*

​ 其中,∑={a,b},a —— 字母, b —— 数字

正规集 : L(a(a︱b)*)={a}{a,b}*

(2) 描述“整数”单词的正规式

dd*︱+dd*︱-dd

​ 其中,∑={+,-,d}, d —— 数字

正规集: L(dd*︱+dd*︱-dd*)={+,-, ε}{d}{d}*

正规式和正规文法之间的转换

如果正规式r和文法G,有L(r)=L(G)则称正规式r和文法G是等价的。

转换方法:

设∑上正规式r,则等价文法G=(VN,VT,P,S)。其中,VT=∑;从形如产生式 S→r 开始,按下表规则进行转换, 直到全部形如产生式, 符合正规文法之规则形式为止,可得到P和VN

规则1 A→xy A→xB,B→y
规则2 A→x*****y A→xB,A→y;B→xB,B→y
规则3 A→x︱y A→x, A→ y
注:A,B∈VN ,B为新增非终结符

逆过程

规则1 A→xB,B→y A→xy
规则2 A→xA︱y A→x*y
规则3 A→x, A→ y A→x |y

有穷自动机

参考文章

参考文章

对应的是识别观点。

有穷自动机首先包含一个有限状态的集合,还包含了从一个状态到另外一个状态的转换。有穷自动机看上去就像是一个有向图,其中状态是图的节点,而状态转换则是图的边。此外这些状态中还必须有一个初始状态和至少一个接受状态

这里提到的自动机特指有限状态自动机,简称为FA,根据状态转移的性质又分为确定的自动机(DFA)和非确定的自动机(NFA)。FA的表达能力等价于正规表达式或者正规文法。FA可以看做是一个有向带权图,图的顶点集合称为自动机的状态集合,图的权值集合为自动机的字母集合。

DFA、NFA和正则表达式是等价的。

转换图 (Transition Graph)
  结点:FA的状态
  初始状态(开始状态):只有一个,由start箭头指向
  终止状态(接收状态):可以有多个,用双圈表示
  带标记的有向边:如果对于输入a,存在一个从状态p到状态q的转换,就在p、q之间画一条有向边,并标记上a

确定有穷自动机DFA

一个确定的有穷自动机DFA M是一个五元组:M=(K,Σ,f,S,Z)。

其中:

  • K是非空有穷集,每个元素称为状态;
  • Σ是有穷字母表;
  • f是K×S→K映射,称为状态转换函数;s∈S, a∈Σ, δ(s,a)表示从状态s出发,沿着标记为a的边所能到达的状态。
  • S∈K,称为开始状态;
  • Z ⊂K,称为结束状态集,或接受状态集。

转换函数f可以扩充为f’: K×Σ*→K映射,并以f替代f’使用。

上图中r=(a|b)*abb;状态1:串以a结尾;状态2:串以ab结尾;状态3:串以abb结尾;

DFA识别的语言

设DFA M=(K,Σ,f,S,Z),如果α∈Σ*,f’(S,α)∈Z,则称符号串α是DFA M所接受(或识别)的。DFA M所接受的符号串的集合记为L(M),即:

L(M)={α︱α∈Σ*,f’(S,α)∈Z}。

一个DFA M=(K,Σ,f,S,Z),以带权有向图G=(V,E)观点,还可采用图形直观描述:

  • 顶点表示状态(即V=K)
  • 加上粗箭头的顶点表示开始状态
  • 双圈顶点表示接受状态
  • 权为a的弧<A,B>(∈E)表示f(A,a)=B。

f(A,a)=B也读作“状态A经过a转换到状态B”。

不确定有穷自动机NFA

一个不确定的有穷自动机NFA M是一个五元组:M=(K,Σ,f,S,Z)。

其中:

  • K是非空有穷集,每个元素称为状态;
  • Σ是有穷字母表;
  • f是K×Σ∪{ε}→ρ(K)映射;f称为状态转换函数,ρ(K) 表示K之幂集。
  • S ⊂K,称为开始状态集;
  • Z ⊂K,称为结束状态集,或接受状态集。

上图中r=(a|b)*abb;状态1:串以a结尾;状态2:串以ab结尾;状态3:串以abb结尾;

带有“ε-边”的NFA

NFA识别的语言

设NFA M=(K,Σ,f,S,Z),如果α∈Σ*,f’(S,α)∩Z≠Φ,则称符号串α是NFA M所接受(或识别)的。NFA M所接受的符号串的集合亦记为L(M),即:

L(M)={α︱α∈Σ*,f’(S,α)∩Z≠Φ}。

自动机的等价性

如果FA M1和FA M2接受相同的符号串的集合(即L(M1)=L(M2)),则称FA M1和FA M2是等价的。

对任何非确定的有穷自动机N ,存在定义同一语言的确定的有穷自动机D
对任何确定的有穷自动机D ,存在定义同一语言的非确定的有穷自动机N

DFA和NFA的比较

两类引擎要顺利工作,都必须有一个正则式和一个文本串,一个捏在手里,一个吃下去。DFA捏着文本串去比较正则式,看到一个子正则式,就把可能的匹配串全标注出来,然后再看正则式的下一个部分,根据新的匹配结果更新标注。而NFA是捏着正则式去比文本,吃掉一个字符,就把它跟正则式比较,匹配就记下来:“某年某月某日在某处匹配上了!”,然后接着往下干。一旦不匹配,就把刚吃的这个字符吐出来,一个个的吐,直到回到上一次匹配的地方。

DFA与NFA机制上的不同带来5个影响:

  1. DFA对于文本串里的每一个字符只需扫描一次,比较快,但特性较少;NFA要翻来覆去吃字符、吐字符,速度慢,但是特性丰富,所以反而应用广泛,当今主要的正则表达式引擎,如Perl、Ruby、Python的re模块、Java和.NET的regex库,都是NFA的。
  2. 只有NFA才支持lazy和backreference等特性;
  3. NFA急于邀功请赏,所以最左子正则式优先匹配成功,因此偶尔会错过最佳匹配结果;DFA则是“最长的左子正则式优先匹配成功”。
  4. NFA缺省采用greedy量词;
  5. NFA可能会陷入递归调用的陷阱而表现得性能极差。

e.g.

例如用正则式/perl|perlman/来匹配文本 ‘perlman book’。如果是NFA,则以正则式为导向,手里捏着正则式,眼睛看着文本,一个字符一个字符的吃,吃完 ‘perl’ 以后,跟第一个子正则式/perl/已经匹配上了,于是记录在案,往下再看,吃进一个 ‘m’,这下糟了,跟子式/perl/不匹配了,于是把m吐出来,向上汇报说成功匹配 ‘perl’,不再关心其他,也不尝试后面那个子正则式/perlman/,自然也就看不到那个更好的答案了。

如果是DFA,它是以文本为导向,手里捏着文本,眼睛看着正则式,一口一口的吃。吃到/p/,就在手里的 ‘p’ 上打一个钩,记上一笔,说这个字符已经匹配上了,然后往下吃。当看到 /perl/ 之后,DFA不会停,会尝试再吃一口。这时候,第一个子正则式已经山穷水尽了,没得吃了,于是就甩掉它,去吃第二个子正则式的/m/。这一吃好了,因为又匹配上了,于是接着往下吃。直到把正则式吃完,心满意足往上报告说成功匹配了 ‘perlman’。

ε闭包运算

当处于某个指定状态时,如果该状态有ε边,那么,不需要吸收任何字符,就可以从该状态转换到ε边所指向的状态。

e.g:

一开始,状态机处于起始状态12,

在状态12,通过ε边可直达状态2,6,

在状态2,可以通过ε边,直达状态0,3。也就是说,当处于状态12时,通过ε边的连接,可以同时抵达状态的集合是 {12,2,6,0,3}。

通过一个状态,推算出它能同时抵达的状态集合,这个状态集合称作ε闭包集合,这种运算称之为ε闭包运算
ε-closure(12) = {12, 2, 6, 0, 3}。

接下来读入字符1,我们从闭包集合中看看,哪个状态节点有能够吸收数字的转换边。从上图观察,我们发现,

状态6和0,拥有吸收数字字符的转换边。

状态6吸收一个数字字符后,跳转到状态7,

状态0吸收字符1后,跳转到状态1,

这样我们可以说,状态集合{12, 2, 6, 0, 3} 在吸收字符1后,跳转到集合{1,7},

后面这个集合{1,7},我们称为转移集合(move set), 我们把这种跳转运算标记如下:
move({12, 2, 6, 0, 3}, D} = {1, 7}。

NFA到DFA转换方法(子集法)

参考文章

设 NFA M=(K,Σ,f,S,Z)则与之等价的DFA M¢=(K’,Σ’,f’,S’,Z’),其中

⑴ K’=ρ(K)(ρ(K)是K全部子集之集合称为K之幂集)

⑵ Σ’=Σ

⑶ f’(q,a)=ε_closure(M(q,a))【转换闭包ε-closure(s)表示由状态s经由条件ε可以到达的所有状态的集合】

⑷ S’=ε_closure(S)

⑸ Z’={q︱q∈K’, q∩Z≠Φ}

注解:

①从FA开始状态不存在路径到达的状态,称为不可达状态。

②考虑舍弃不可达状态的转换状态之计算,子集法可以简化从S’=ε_closure(S)开始计算。

这些步骤的目的是为了消掉ε。

设 NFA M=(K,Σ,f,S,Z),子集法得到与其等价的DFA M¢=(K’,Σ,f’,S’,Z’之具体计算步骤可以是:

① 置K’为空集;

② 计算M’的开始状态S’=ε_closure(S), S’作为K’新增状态;

③ 对于K’每一新增状态q,计算出每个a∈S的转换状态p,即f’(q,a) =p=ε_closure(M(q,a))。如果p∉K’,则p作为K’新增状态;

④ 重复③,直到K’不再出现新增状态为止;

⑤ 计算接受状态集Z’={q︱q∈K’,q∩Z≠Φ}。

例子:

DFA的最小化

消除多余状态

  • 什么是多余状态
    • 从这个状态出发没有通路到达终态(也称为死状态)
    • 从开始状态出发,任何输入串也不能到达的那个状态
  • 如何消除多余状态
    • 删除

例如:

如下为正规文法G[S]

​ S→aA|bQ; A→aA|bB|b;B→bD|aQ;Q→aQ|bD|b;D→bB|aA;E→aB|bF;F→bD|aE|b

构造相应的DFA。

此处我们观察到E不出现在任何产生式的右部,所以E是无效符号,(其对应的状态就是多余状态)
删除E所在的产生式之后,符号F也不出现在任何产生式的右部,则F是无效符号,
删除F及其所在产生式。此时除了文法开始符号S之外,其余非终结符都是从S可达的。

等价状态

  • 何为等价状态,对于两个状态s和t
    • 一致性条件状态s和t必须同时为终态或非终态
    • 蔓延性条件对于所有输入符号,状态s和状态t必须转化到等价的状态里

DFA的化简算法:分割法

对于DFA M=(S,Σ,f,S0,Z)

  • 首先将DFA的状态集进行初始化,分成Π=(Z,S-Z);

  • 用下面的过程对Π构造新的划分Π new
    for (Π中每个组G) do //每个组都是一个状态集
    begin
    把G划分成小组,G中的任意两个状态Si和Sj在同一组中,当且仅当对于Σ中任意输入符号a ,Si和Sj的a转换是到同一组中,move(Si,a) ∈Gi ,move(Sj,a) ∈Gi。这样,只要Si和Sj的a转换是到不同的组中,则说明Si和Sj是可区别的,可进行划分。在Π new中用刚完成的对G的划分代替原来的G。
    end ; Π := Π new;

  • 重复执行(2),直到Π中每个状态集不能再划分(Π new= Π)为止;

  • 合并等价状态 ,在每个G中,取任意状态作为代表,删去其它状态;

  • 删去无关状态,从其它状态到无关状态的转换都成为无定义。

①首次划分: Π0=({2,3,4,5},{0,1})
②在G={2,3,4,5}中:f(2,a)=1,f(4,a)=0(转向终态集{0,1});f(3,a)=3,f(5,a)=5(转向非终态集{2,3,4,5}),故{2,4}和{3,5}是可区别的,得Π1=({2,4},{3,5},{0,1});
③在G={2,4}中,f(2,a)=1,f(4,a)=0(转向终态子集),而f(2,b)=3,f(4,b)=5(转向非终态子集{3,5}),所以不可区别,不再进行划分;
④考察G={3,5},f(3,a)=3,f(5,a)=5(转向非终态子集{3,5}),f(3,b)=2,f(5,b)=4(转向非终态子集{2,4}), 所以不可区别,不再进行划分;
⑤考察G={0,1},f(0,a)=f(1,a)=1(转向终态集{0,1}); f(0,b)=2,f(1,b)=4(转向非终态子集{2,4}),所以不可区别,不再进行划分;
⑦进一步进行考察,可以发现每个子集都不能再划分了;
⑧消去等价状态:{0,1}用0表示,{2,4}用2表示,{3,5}用3表示,如右图所示
⑨去掉无关状态,因DFA M’中没有无关状态,所以下图即为最后结果。

img

正规式和有穷自动机的等价性

正则式 到NFA的转换

□ ε对应的NFA

img

□ 字母表Σ中符号a对应的NFA

img

□ r = r1r2对应的NFA

img

□ r = r1|r2对应的NFA

img

□ r = (r1)*对应的NFA

img

例:r=(a|b)*abb 对应的NFA

img

NFA到正规式的转换方法

设NFA M=( K,Σ,f,S,Z),则与之等价的Σ上正规式R,可以由下列方法构造。

⑴ 在NFA M上,新增两个状态X和Y作为开始状态和接受状态,且将X经ε指向M的开始状态(任意q∈K,增加f(X,ε)=q), 将将M的开始状态经ε指向Y(任意q∈Z,增加f(q, ε)=Y)。这样,得到一个与NFA M等价的、只有唯一开始状态X和唯一接受状态Y的NFA M’;

⑵ 按下列转换规则,逐步消除NFA M’中的状态,直到只剩下X和Y两个状态为止。弧上符号串,即为等价的S上正规式R。

DFA到右线性正规文法转换

要把一个 DFA 转化为正则表达式,我们可以通过将它分解为更简单的子问题来迭代求解:

  • 将状态按 1、2、…、n 的顺序依次标号
  • 先求任意状态 i 到 j(i 可以等于 j,即自身到自身),不经过其它状态的路径对应的正则表达式
  • 求状态 i 到 j,最高只经过状态 1 的路径对应的正则表达式
  • 求状态 i 到 j,最高只经过状态 2 的路径对应的正则表达式
  • 依此类推……
  • 求状态 i 到 j,最高只经过状态 n 的路径对应的正则表达式,即经过所有状态的路径对应的正则表达式

正规文法和有穷自动机间的转换

正规文法到NFA转换方法

设右线性正规文法G=(VN,VT,P,S),则与之等价的NFA M=(VN∪{Z},VT,f,{S},{Z}),其中VN∩{Z}=Φ,转换函数f可以由下列方法构造:

(1)如果A→a∈P , 则f(A,a)=Z;

(2) 如果A→ ε ∈P ,则f(A, ε)=Z;

(3)如果A→aB∈P, 则f(A, a)=B。

DFA到正规文法转换方法

设DFA M=(K,Σ,f,S,Z),则与之等价的右线性正规文法G=(K,Σ,P,S),其中规则集转换P可以由下列方法构造:

(1) 如果 f(B,a)=C, 则B→aC∈P。

(2) 对接收状态 ∈Z, 增加S→ε

构造词法分析程序的技术线路

(1)依据给定的源语言之单词集,设计其正规文法或正规式;

(2)之后等价地转换成非确定有穷自动机;

(3)再通过子集法将其确定化,最终将确定有穷自动机最小化;

(4最后依据最小化确定有穷自动机,设计词法分析程序。

自顶向下语法分析方法

分析思想

自顶向下语法分析方法(即推导法)是从文法开始符S出发,逐步进行推导,以证实S=>α的推导过程是否存在的方法。

问题是每步推导会面临两次多种可能选择:

⑴ 选择句型中哪一个非终结符进行推导

⑵ 选择非终结符的哪一个规则进行推导

问题⑴可以采用最左推导解决。问题⑵通常需要穷举每一个规则的可能推导,即不确定的自顶向下语法分析。具体思想是:

一旦寻找到一个符号串α之推导过程,便结束穷举过程,断定符号串α是句子。

只有当穷举全部可能的推导,而没有一个符号串α之推导过程的时候,才可以断定符号串α不是句子。

递归向下分析

  递归向下的语法分析可能需要回溯(aka需要重复扫描输入),考虑以下文法: S -> aBc ,B -> bc | b ,当我们用递归向下分析,输入为abc时,语法树如下图:

  

  当我们第一次匹配时识别失败了(a匹配a,bc匹配B,最后一个c未匹配到),输入必须回到b,用B的另外一种方式匹配。

  递归向下的分析十分直观,实现起来也比较方便,但效率较低,所以一般不采用。递归向下的分析方法实际上是深度优先搜索+回溯。而下面要说的预测分析则是用高效的动态规划来实现语法分析。

递归预测向下分析

   在讨论使用动态规划的预测向下分析之前,我们先来看一种特殊的预测向下分析。它在本质上也是递归的,唯一的区别在于它不需要回溯。考虑以下文法:A -> aBb | bAB,伪代码实现如下:

proc A {

case 当前标记 {

  ‘a’:匹配a, 移动到下个标记;

     调用函数B;
     匹配b, 移动到下个标记;
  ‘b’:匹配b,移动到下个标记;
     调用函数A;

     调用函数B;

       }
    }

 其实这种分析方式与前者的区别就在于它用了case语句来预测A的两种可能性,从而做出不同的判断。但这种方式的效率也是不如动态规划的。

非递归预测向下分析

  非递归预测向下分析是表驱动的分析方法,也叫做LL(1)分析。第一个”L”表示从左到右扫描。第二个”L”表示产生最左推导。”1”表示每次只要往前走一步就可以决定语法分析的动作。

所谓表驱动就是通过查表的方式来分析一个输入流是否符合文法。假设我们已经得到了这张语法分析表,现在来具体分析这种方式是如何工作的。

  

首先我们需要一个栈来存储start symbol,即语法树的根。然后从表中查找当栈顶为S,输入为a时对应的文法,然后将S替换为aBa(注意入栈顺序),然后a与输入的a匹配,非终结标志B对应到了b,此时查找表中相应的文法,将B弹出栈,将bB压入栈(注意顺序)。以此类推直到栈底的终止字符匹配到了输入的终止字符,表示匹配成功。

上面是实例,下面我们给出一个高度的分析行为概括:

  当栈顶为X,当前输入为a时,有以下四种分析行为:

  1.如果X和a都为终止符号$,匹配成功,停止匹配。

  2.如果X和a都是同一种终结标志(terminal symbol),将X弹出栈,将输入移动到下个标志。(表示该标志成功匹配,准备匹配下个标志)

  3.如果X是非终结标志(nonterminal symbol),查询语法分析表,找到[S,a],如果[S,a]为 X->Y1Y2Y3…Yk,则将Y1Y2Y3…Yk逆序放入栈中。(即Y1为栈顶)

  4.不符合以上三种情况,匹配失败,进入错误恢复模式。

FIRST集的定义

设文法G=(VN,VT,P,S),则FIRST(α)={a︱α=>*a β,a∈VT,α,β∈V *}

特别地,α=>*ε,约定ε∈FIRST(α)。

FIRST(α)是由α可以推导以终结符号开头符号串的头符号集合。如果所有非终结符右部的FIRST集合两两相交为空,可以使用确定的最左推导。

计算规则如下:

  1.如果X是终结符号,first(X)={X}

  2.如果X是非终结符号且X->ε是一个文法规则,那么ε属于first(X)

  3.如果X是非终结符号且X->Y1Y2Y3…Yn是一个文法规则,那么:①如果终结符号a在first(Yi)中且ε在所有的first(Yj) (j-1,2,…i-1)中,那么a也属于first(X) ②如果ε在所有的first(Yj) (j=1,2…n) 那么ε也属于first(X)

  4.如果X本身为ε,那么first(X)={ε}

  以上的规则将一直使用直到没有元素能够加入到任何first()当中。

如A->aB | CD

这里面包含了组成First(A)的两种情况:
以终结符开头,当然要把这个终结符(a)放到A的First里
以非终结符开头,先把C的First放到A的First里

FOLLOW集的定义

一般形式:

输 入 串: a1a2……ai-1 ai……an

句型推导:S=>* a1a2……ai-1

如果使用空规则,意味着需要:β =>* ai……an

则有句型: S=>* a1a2……ai-1 A ai……an

设文法G=(VN,VT,P,S),则FOLLOW(A)={a︱S=>* αAβ,A∈VN, a∈FIRST(β),α,β∈V*}

(或者:FOLLOW(A)={a︱S =>* ···Aa···,A∈VN,a∈VT })

 follow(A):从A之后可以立即得到(可以理解为与A相邻)的终结符号的集合,其中A是非终结符号。

如果对非终结符A,有一条空规则,则A的FOLLOW集合和A的非空右部的FIRST集合两两相交为空,可以使用确定的最左推导。

计算规则如下:

  1.如果A->aBb是一个文法规则,那么所有在first(b)中的元素除了ε都包含在follow(B)中。

  2.如果A->aB是一个文法规则或者A->aBb是一个文法规则且ε包含在first(b)中,那么在follow(A)中的所有元素都在follow(B)中。即follow(A)属于follow(B)

  以上的规则也将一直使用直到没有元素能够加入到任何follow()当中。

如S->(L) | aL | LC

找Follow的三种情况:
先在候选式(右边)中找到该非终结符,如L(注意例中只有一个定义,但找Follow要看到所有右边出现该非终结符的)

如果L的右边是终结符, 那么这个终结符加入L的Follow

如果L的右边是非终结符, 那么把这个非终结符的First除去空加到L的Follow中

如果L处在末尾,那么,’->’左边符号的Follow成为L的Follow

另外要注意的是:
开始符号的Follow中要加上‘#’

SELECT集的定义

$$
设文法G=(V_N,V_T,P,S),A\in V_N,A\to{\alpha}\in P,则\
SELECT(A\to{\alpha})=\begin{cases}
FIRST(\alpha),&(\alpha\not\Rightarrow\star\varepsilon)\
(FIRST(\alpha)-{(\varepsilon)})\cup(FOLLOW(A))&(\alpha\Rightarrow\star\varepsilon)\
\end{cases}
$$

SELECT(A→α)称为规则A→α的选择集。它是FIRST(α)和FOLLOW(A)组成,是终结符号集VT的子集。

LL(1)文法

文法G是LL(1)的,当且仅当G的任意两个具有相同左部的产生式A→α∣β满足下面的条件:

  • 不存在终结符a使得α和β都能推导出以a开头的串。

  • α和β至多有一个能推导出ϵ

  • 如果$\beta \Rightarrow^*{\epsilon}$⇒则$FIRST(α)∩FOLLOW(A)=Φ$
    如果$α\Rightarrow^*{\epsilon}$⇒ 则$FIRST(β)∩FOLLOW(A)=Φ$
    因为如果$\beta \Rightarrow^*{\epsilon}$⇒ 那么$SELECT(β)$就包含了$FOLLOW(A)$,所以$FIRST(\alpha)$就不能包含$FOLLOW(A)$中元素。不然两个的SELECT集将会相交。

  • 同一非终结符的各个产生式的可选集互不相交

判定

  1. 检查产生式中是否有含有左递归或左公因子:
    含有左递归或左公因子的文法一定不是LL(1)文法;
    不含有左递归或左公因子的文法也不能确定是否为LL(1)文法;

  2. 计算每个产生式的FIRST集:

    ①如果这个产生式右部第一个字符是终结符,那么这个终结符就属于它的FIRST集。

    ②如果这个产生式右部第一个字符是非终结符,那么这个非终结符的FIRST集就属于它的FIRST集。

    如果这个非终结符的FIRST集中含ε,那么后面的字符如果是终结符……

    ③如果这个产生式右部可以推出ε,那么ε也属于它的FIRST集。

  3. 计算每个非终结符的FOLLOW集:

    首先向开始符号的FOLLOW集中添加#,然后对于所有非终结符,不断的找含有它的产生式右部:

    ①该非终结符后面的字符若是终结符,那么这个终结符就属于它的FOLLOW集;

    ②该非终结符后面的字符若是非终结符,那么这个非终结符的FIRST()集中的所有元素就属于它的FOLLOW集;

    如果这个非终结符的FIRST()集中含ε,将ε删去,同时将这个产生式左部FOLLOW集中的所有元素添加至它的FOLLOW集中;

    注意:不需要考虑后面的字符了,因为已经包含在FIRST()集中了。

  4. 计算每个产生式的SELECT集:

    ①如果这个产生式可以推出ε,那么它的SELECT集是{FIRST(该产生式右部)-ε}∪FOLLOW(该产生式左部的非终结符)

    ②如果这个产生式不能推出ε,那么它的SELECT集是{FIRST(该产生式右部)}

  5. 检查相同左部产生式的SELECT集的交集:

    检查相同左部产生式的SELECT集的交集,如果全为空集说明该文法是LL(1)文法,反之则不是。

非LL(1)文法到LL(1)文法的等价变换

但下面讨论的等价变换方法,仅仅确保变换的等价性(即L(G)=L(G′)),不能保证变换后的文法G′一定是LL(1)文法。因此,对于变换后的文法G′,必须判别它是LL(1)文法后,方可使用确定的自顶向下语法分析方法。

提取左公共因子法

A→αβ1︱αβ2︱···︱αβn︱γ1︱γ2︱···︱γm可以推导出:

A→αB︱γ1︱γ2︱···︱γm;B→β1︱β2 ︱···︱βn

消除左递归法

有直接左递归和间接左递归和一般左递归,对于间接左递归要先化成直接;

  • 直接左递归经过一次推导就可以看出文法存在左递归,如P→Pa|b。
  • 间接左递归侧需多次推导才可以看出文法存在左递归,如文法:S→Qc|c,Q→Rb|b,R→Sa|a有S =>Qc =>Rbc =>Sabc

消除直接左递归法

A→Aα1︱Aα2︱···︱Aαm︱β1︱β2︱···︱βn =>

A →β1 A′︱β2 A′︱···︱βn A′;A′→α1A′︱α2A′︱···︱αmA′︱ε

有文法G(E):

E→E +T |T

T→T*F | F

F→i| (E)

消除该文法的直接左递归。

解:按转换规则,可得:

E→TE’

E’→+TE’|ε

T→FT ‘

T’→*FT’|ε

F→i| (E)

消除间接左递归法

设非终结符按某种规则排序为A1,A2,,An。

For i﹕=1 to n do

begin

For j﹕=1 to i-1 do

begin

若Aj的所有产生式为:

Aj →δ1| δ2 | … | δn

替换形如Ai → Aj γ的产生式为:

Ai →δ1γ |δ2γ | … |δnγ

end

消除Ai中的一切直接左递归

end

以文法G6为例消除左递归:

(1)A→aB

(2)A→Bb

(3)B→Ac

(4)B→d

解:用产生式(1),(2)的右部代替产生式(3)中的非终结A得到左部为B的产生式:

(1)B→aBc

(2)B→Bbc

(3)B→d

消除左递归后得到:

B→aBcB’ |dB’

B’→bcB’ |ε

再把原来其余的产生式A→aB,A→Bb加入,最终得到等价文法为:

(1) A→aB

(2) A→Bb

(3) B→(aBc|d)B’

(4) B’→bcB’|ε

c)消除文法中一切左递归的算法

递归下降分析法

通过计算的SELECT集判断编写子程序:

递归下降分析法

ParseE’函数表示进入E’的产生式,通过switch函数分离相同左部的产生式,然后依次检查产生式右部字符,如果是终结符,则通过MatchToken函数判断符合,不符合则出错;如果是非终结符,则继续递归跳转至它所对应的Parse函数。

递归下降分析法对应的是最左推导过程
优点:程序结构和层次清晰明了,易于手工实现;
对于语义加工,这种方法十分灵活;
缺点:递归调用可能带来效率问题。

预测分析法

首先根据计算出的SELECT集绘制出预测分析表

然后新建一个分析栈,向空栈中依次压入#和文法的开始符号E,然后比较剩余输入串的首字符和分析栈顶元素,如果不同,则先将分析栈顶元素出栈,然后将对应预测分析表中的产生式右部从后向前依次入栈;如果相同,则先将分析栈顶元素出栈,并将剩余输入串的首字符删去;然后重复以上过程直到栈为#,剩余输入串也为#,则表示语法匹配成功。

LL(1)分析中的一种错误处理办法

发现错误的情况:
(1) 栈顶的终结符与当前输入符不匹配;
(2) 非终结符A于栈顶,面临的输入符为a,但分析表M的M[A,a]为空 (FIRST(A)中没有a);

应急”恢复策略:
对于错误(1) 跳过输入串中的一些符号直至遇到和栈顶的终结符相同的字符为止。

对于错误((2) 跳过输入串中的一些符号直至遇到“同步符号”为止 。

同步符号的选择
(1) 把FOLLOW(A)中的所有符号作为A的同步符号。跳过输入串中的一些符号直至遇到这些“同步符号”,把A从栈中弹出,可使分析继续。(跳过A)
(2) 把FIRST(A)中的符号加到A的同步符号集,当FIRST(A)中的符号在输入中出现时,可根据A恢复分析。 (不跳过A)

自底向上优先分析

优先分析概述

优先分析法是利用句型相邻两个符号之间的所谓“优先关系”确定句柄。

优先关系由文法规则确定,其本质含义是在句型相邻两个符号中哪个符号可以优先归约。

采用简单优先分析法或算符优先分析法构造语法分析程序时,语法分析程序的总体框架如图所示。

简单优先分析法

优先关系定义

1、X和Y优先级相等,表示为 X=·Y,当且仅当G中存在产生式规则A=>···XY···。

解读:X、Y的优先级相同,当XY存在一个句柄之中,它们将同时被归约。表现在语法树中S=·b。

2、X优先级小于Y,表示为 X<·Y ,当且仅当G中存在产生式规则A=>···XB···,B=+=>Y···。

解读:X优先级小于Y,当XY存在一个句型中时,它们将不可能出现在同一个句柄中,Y一定比X先被规约。表现在语法树中b<·a。

3、X优先级大于Y,表示为 X>·Y ,当且仅当G中存在产生式规则A=>··BD···,B=+=>···X,D=*=>Y···。

解读:X优先级大于Y,当XY存在一个句型中时,它们将不可能出现在同一个句柄中,X一定比Y先被规约。表现在语法树中a>·S。

X和Y的优先级为空,表示在文法的任何句型中都不会出现该符号对相邻出现的情况。

简单优先文法定义

一个文法是简单优先文法,需要满足以下两个条件:

  1. 在文法符号集中V,任意两个符号之间必须之后一种优先关系存在。(显然满足)
  2. 在文法中,两个产生式不能有相同的右部。

简单优先分析法的操作步骤

  1. 将输入输入串a1a2···an#依次压栈,不断比较栈顶符号ai和下一个待输入符号aj的优先级,若ai>·aj则进行下一步,否则重复此步骤。

    解读:停止条件是ai>·aj表示前面输入串一定比后面先归约,所以只需要在前面找句柄就行了。

  2. 栈顶符号ai即为句柄尾,从此处向左寻找句柄头ak,满足ak-1<·ak

    解读:从后向前找ak-1<·ak表示ak之前的输入串一定比ai···ak后归约,由此确定现在就是要归约ai···ak

  3. 由句柄ai···ak在文法中寻找右部为ai···ak的产生式;找到则将句柄替换为相应左部,找不到则说明该输入串不是该文法的句子。

  4. 重复以上步骤直到归约完成。

算符优先分析法

基本概念

  1. **算符文法(OG)**:文法G中没有形如A=>···BC···的产生式,其中B、C为非终结符,则G为算符文法(operator grammar)。

    也就是说产生式的右部不能出现两个非终结符相邻,就好像算式中两个操作数相连。

    算符文法的两个性质:

    ①算符文法中任何句型都不包含两个相邻的非终结符。

    ②如果Ab(bA)出现在算符文法的句型y中,则y中含b的短语必含A,含A的短语不一定含b。

  2. **算符优先文法(OPG)**:一个不含ε产生式的算符文法G,任意终结符对(a,b)之间最多只有一种优先关系存在,则G为算符优先文法(operator precedence grammar)。

    以算式类比,也就是说我们只关心算符之间的优先关系,不关心操作数的优先关系,·利用算符的优先性和结合性来判断哪个部分先计算(归约)。

    注意 :这里的优先关系与简单优先分析法中不一样。

    a、b为终结符,A、B、C为非终结符

    1. a和b优先级相等,表示为 a=·b ,当且仅当G中存在产生式规则A=>···ab···或者A=>···aBb···。

      解读:表示a、b在同一句柄中同时归约。

    2. a优先级小于b,表示为a<·b,当且仅当G中存在产生式规则A=>···aB···,且B=^+^=>b···或B=^+^=>Cb···。

      解读:表示b、a不在一个句柄中,b比a先归约。

    3. a优先级大于b,表示为 a>·b ,当且仅当G中存在产生式规则A=>··Bb···,且B=^+^=>···a或B=^+^=>···aC。

      解读:表示b、a不在一个句柄中,a比b先归约。

    1. FIRSTVT():FIRSTVT(B)={b|B=^+^=>b···或B=^+^=>Cb···,B∈VN,C∈VN,b∈VT }【在算符优先中,非终结符只会和一个终结符相邻】
    2. LASTVT():LASTVT(B)={b|B=^+^=>···b或B=^+^=>···bC,B∈VN,C∈VN,b∈VT}
    3. 素短语:(a)它首先是一个短语,(b)它至少含一个终结符号,(c)除自身外,不再包含其他素短语。

最左素短语定理

FIRSTVT()的构造算法

  1. 原理:

    ①如果有这样的表达式:A=>a···或者A=>Ba···,那么a∈FIRSTVT(A)。

    ②如果有这样的表达式:B=>A···且有a∈FIRSTVT(A),则a∈FIRSTVT(B)。

  2. 算法:

    数据结构:

    布尔数组F[m,n],m为非终结符数量,n为终结符数量,为真时表示对应a∈FIRSTVT(A)。

    栈S:暂存用于进行原理②的元素。

    流程图:

算符优先关系矩阵的构造算法

  1. 原理

    =·关系

    查看所有产生式的右部,寻找A=>···ab···或者A=>···aBb···的产生式,可得a=·b。

    <·关系

    查看所有产生式的右部,寻找A=>···aB···的产生式,对于每一b∈FIRSTVT(B),可得a<·b。

    >·关系

    查看所有产生式的右部,寻找A=>··Bb···的产生式,对于每一a∈LASTVT(B),可得a>·b。

  2. 算法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    for 每条规则U::= x1 x2…xn do
    for i:=1 to n-1 do
    begin
    if xi和xi+1均为终结符, THEN 置 xi=xi+1
    if i≤n-2,且xi和xi+2都为终结符号但xi+1为非终结符号 then 置 xi=xi+2

    if xi为终结符号xi+1为非终结符号 then
    for FIRSTVT(xi+1)中的每个b do 置xi<b

    if xi为非终结符号xi+1为终结符号 then
    for LASTVT(xi)中的每个a do 置a>xi+1
    end

    流程图:

算符优先分析法

实现算符优先分析法:找句型的最左子串(最左素短语)【在语法树中,位子在句型最左边的那个素短语】并进行规约

具体实现:当栈内终结符的优先级<或=栈外终结符的优先级时,移进;当栈内终结符的优先级>栈外终结符的优先级时,表明找到了素短语的尾,再往前找其头,并进行规约。

读入字符串为X1X2···Xn#

数组S[n+2]用于存放压入栈的字符

流程图:

算符优先函数

迭代法

若已知运算符之间的优先关系,可按如下步骤构造优先函数:

1、对每个运算符a(包括#在内)令f(a)=g(a)=1

2、如果a⋗b且f(a)<=g(b)令f(a)=g(b)+1

3、如果a⋖b且f(a)>=g(b)令g(b)= f(a)+1

4、如果a≐b而f(a) ≠g(b),令min{f(a),g(b)}=max{f(a),g(b)}

5、重复2~4,直到过程收敛。如果重复过程中有一个值大于2n,则表明不存在算符优先函数。

算符优先关系表

算符优先归约和规范规约

自下而上的语法分析,其分析过程为边输入单词符号,边归约,直至归约到文法的开始符号。(归约是指根据文法的产生式规则,把产生式的右部替换成左部符号)自下而上的分析方法的关键就是找到可归约串。

对于简单问题(不用考虑优先级等问题)的自下而上语法分析有以下方法:
1.移进归约,即用一个寄存符号的先进后出栈,把输入符号一个一个地移进到栈里,当栈顶形成某个产生式的候选式时,即把栈顶的这一部分替换成(归约为)该产生式的左部符号;
2.规范规约,首先了解规范规约的定义,假定α是文法G的一个句子,如果序列αn,αn-1,… ,α0满足:(1)αn=α(2)α0为文法的开始符号,即α0=S(3) 对任何i,0<i≤n,αi-1是αi把句柄(句型的最左直接短语即最左端的简单子树)替换成为相应产生式左部符号而得到的。我们称该序列是α的一个规范归约,规范规约即最左归约,可通过修剪最左简单子树实现;
3.用符号栈进行自下而上的语法分析,取一个栈作为符号栈,在分析开始时,’#’预先进栈,作为栈底符号,将输入串中的符号依次入栈并规约,’#’作为输入串的结束符。

在实际问题中往往能够需要考虑优先级,对于优先级问题有以下处理方法:(1)算符优先分析法,即定义算符之间的某种优先关系,借助这种优先关系找到可归约串并规约。这种优先关系往往是单向的,没有自反性。在算符优先分析法中将最左素短语作为可归纳串(算符优先分析一般不等于规范归约);(2)优先函数法,优先函数是把每个终结符α与两个自然数f(α)与g(α)相对应,使得若α1 <. α2,则f(α1) < g(α2),若α1 =. α2,则f(α1) = g(α2),若α1 >. α2,则f(α1) > g(α2),f称为入栈优先函数,g称为比较优先函数。

文法的直接概念

文法是阐述语法的一个工具,语句是语法的实例 。

语言的构成:组成语言的基本形式是句子,句子是由单词序列构成的,单词是由语言基本符号(字母或单字)组成的。

语言既包含单词和句子这样的语言成分,又包含将这些成分组织起来的语言规则,如词法规则、句法规则等。

  • 语法:是一组规则,定义符号如何排列,排列与符号含义无关。
  • 语句:是一组规则,定义符号如何排列,排列与符号含义无关。
  • 语义:研究语法的含义

约定(1):符号”::=”表示“···是由···组成的”

约定(2):符号”|”表示“或者”的意义

约定(3):符号”=>”表示“推导”

eg:

<句子> ∷=<主语> <谓语> <宾语>
<主语> ∷=<名词>
<主语> ∷=<代词>
<谓语> ∷=<动词>
<宾语> ∷=<名词>
<宾语> ∷=<代词>
<代词> ∷= 我
<代词> ∷= 你
<动词> ∷= 吃
<动词> ∷= 做
<名词> ∷= 饭
<名词> ∷= 菜

<句子> => <主语> <谓语> <宾语>
=> <代词> <谓语> <宾语>
=> 我 <谓语> <宾语>
=> 我<动词> <宾语>
=> 我吃<宾语>
=> 我吃<名词>
=> 我吃饭

1、推导过程不唯一

2、推导起点的不同,导致语法意义上差异的推导结果

语法形式化方法要点:

  • 语法规则的形式化
  • 语法规则含有语法单位符号
  • 语法规则含有构成语句的单词符号
  • 特殊的语法单位符号——开始符号

语法形式化的最终目的在于将语法分析的问题将装换成形式化的推导过程。

符号和符号串

基本概念

  • 字母表:字母表∑是非空有穷集合,其元素称为符号。
  • 符号串 由字母表∑中的符号组成的有穷序列称为 (字母表∑上的)符号串。特别地,不含任何符号的有穷序列称为空串,记为ε。单词和源程序都是符号串!

eg

设字母表∑={0,1},则

​ 101是∑上的符号串,201不是∑上的符号串。

  • 符号串长度:符号串α的长度是指符号串α中含有符号的个数,记为︱α︱。特别约定,空串ε为零,即︱α︱=0。
  • 符号串集合:如果集合A的元素都是字母表∑上的符号串,则称集合A为∑上的符号串集合,简称串集。

eg

设字母表∑={a,b,c},A={ε,a,ba,cab},B={a1,ba,cab},则

A是∑上的符号串集合,B不是∑上的符号串集合。

基本运算

  • 符号串连接运算:设x和y是字母表∑上的符号串,在符号串x的最后一个符号之后顺序接上符号串y的符号得到的新符号串z,则称符号串z是由符号串x和符号串y经过连接运算的结果,记为z=x·y,其中,·是连接运算符。

设字母表∑={a,b,c,0,1},x=abc,y=01cba,则 z=x·y= abc01cba

  • 符号串方幂运算 设x是字母表∑上的符号串,z是由n(≥0)个x自身连接得到的符号串,则称符号串z是由符号串x的n次方幂运算的结果,记为z = x^n^ 。特别约定,x^0^ =ε, x^1^=x 。

  • 符号串集连接运算 设A,B是字母表∑上的符号串集,·是符号串集连接运算,则C=A·B={x·y︱x∈A ,y∈B}。 笛卡尔积

  • 符号串集方幂运算 设A是字母表∑上的符号串集,则C是由n(≥0)个A自身连接得到的符号串集,则称符号串集C是由符号串A的n次方幂运算的结果,记为C = A^n^ 。特别约定,A^0^ ={ε},A^1^=A 。

  • 符号串集正闭包运算 设A是字母表∑上的符号串集, A+是A的正闭包,则: A+=A^1^∪A^2^∪A^3^∪···∪A^n^··· 。

  • 符号串集闭包运算 设A是字母表∑上的符号串集, A*是A的闭包,则 : A* =A0∪A+ ,

    即:A* =A^0^∪A^1^∪A^2^∪A^3^∪···∪A^n^··· 。

文法和语言的形式定义

规则是字母表V上形如 a∷=b的式子,可以简写成a→b。其中,符号串a∈V^+^称为规则的左部,符号串b∈V*称为规则的右部。规则也称为重写规则、产生式或生成式。

特别地,a∷=ε(ε空串)称为a的空规则。

对于相同左部的多个规则,可以使用符号∣简写。如,规则a∷=b和a∷=δ,简写成a∷=b∣δ。 简写为a → b∣δ

文法

文法G定义为一个四元组(VN,VT,P,S),记为G=(VN,VT,P,S)。其中,

① VN是非空有穷集合,称为非终结符集,其元素称为非终结符;

② VT是有穷集合,称为终结符集,其元素称为终结符;

③ P是非空有穷集合,称为规则集,其元素是字母表VN∪VT上的规则,VN∪VT称为文法的字母表V,且VN∩VT=空集;

④ S∈VN,称为开始符。

直接推导、直接归约

设文法G=(VN,VT,P,S),如果α→β∈P,则称γ α δ推导出γ β δ,记为γ α δ=>γ β δ,其中,γ,δ∈V。

γ α δ=>γ β δ也称为直接推导或一步推导。

如果γ α δ=>γ β δ,则也称为γ β δ归约到γ α δ,也称为直接归约或一步归约。

例如,例3.1 定义的文法G1=({S},{a,b},{S→aSb,S→ab},S),推导例子有:

(1)S=> aSb (α=S,β=aSb,γ=ε,δ=ε)【ε是空集】

(2)aSb => aaSbb (α=S,β=aSb,γ=a,δ=b)

(3)aSb => aabb (α=S,β=ab,γ=a,δ=b)

(4)aSbSb => aaSbbSb (α=S,β=aSb,γ=a,δ=bSb )

多步推导、多步归约

设文法G=(VN,VT,P,S),α,β∈(VN∪VT)*, 如果α,β之间存在推导序列:

α= W0 => W1 => W2 ··· => Wn =β(n≥1),

则称α经过n步推导出β,记为α=>^+^β。其中,Wi∈(VN∪VT)*

(1≤i≤n)。α=>^+^β也称n步推导或多步推导。

如果α=>^+^β,也称为β归约到α,也称为n步归约或多步归约。

例如,例3.1 定义的文法G1=({S},{a,b},{S→aSb,S→ab},S) ,多步推导(Þ)例子有:

(1)S=>^+^ ab (∵S=> ab)

(2)S=>^+^ aabb (∵ S=> aSb=> aabb)

(3)S=>^+^ aaaSbbb (∵ S=> aSb=> aaSbb=> aaaSbbb)

(4)aSb =>^+^ aaabbb (∵ aSb=> aaSbb=> aaabbb)

0步或0步以上推导与归约

设文法G=(VN,VT,P,S),α,β∈(VN∪VT ) ^*^,如果有α→β或α=>^+^β,则称α经过0步或0步以上推导出β,记为α=>*β。亦称β经过0步或0步以上归约到α。

例如,例3.1 定义的文法G1=({S},{a,b},{S→aSb,S→ab},S) , 0步或0步以上推导(Þ)例子有:

(1)S=>* ab,因为有S=>^+^ ab

(2)S=>* aabb, 因为有S=>^+^aabb

(3)S=>* aaabbb,因为有S=>^+^aaabbb

(4)aSb =>* aaabbb,因为有aSb=>^+^ aaabbb

(5)aSbSb =>*aSbSb,因为有aSbSb =>^+^ aSbSb

句型、句子

设文法G=(VN,VT,P,S),如果有S=>* β,则称β是文法G的句型。如果有S=>* β,且β∈VT*,则称β是文法G的句子。

例如,例3.1 定义的文法G1=({S},{a,b},{S→aSb,S→ab},S) ,句型和句子例子有:

(1)ab是G的句子,因为有S=>* ab ,ab∈VT*

(2)aabb是G的句子,因为有S=>* aabb,aabb∈VT*

(3)aaaSbbb是G的句型,因为有S=>* aaaSbbb(aaaSbbb ∉ VT*)

语言

文法G=(VN,VT,P,S)的产生语言定义为文法G的句子集合,记为L(G)。即:

L(G)={β︱S=>^^β,β∈VT^^}。

文法等价

设G1和G2是两个文法,如果L(G1)=L(G2),则称文法G1和G2是等价的。

例如,下列文法G2和G3是等价的。因为它们产生的语言都是以字母a开头、字母a和b构成的符号串的集合。即L(G2)=L(G2)={a}{a,b}*。

G2=({S,C},{a,b},P,S),

其中,P={S→aC,C→aC ,C→bC, C→ε}。

G3=({S},{a,b},P,S),

其中,P={S→Sa,S→Sb ,S→a}。

文法类型

0型文法

设文法G=(VN,VT,P,S),如果任意α→β∈P,α中至少含有一个非终结符,则称文法G属于0型文法。0型文法,也称为短语文法。

1型文法

设文法G=(VN,VT,P,S),如果任意α→β∈P,α中至少含有一个非终结符,且除空规则之外,α的长度不大于β的长度,即︱α︱≤︱β︱,则称文法G属于1型文法。1型文法,也称为上下文有关文法。

文法G5定义如下,显然G5是1型文法。

​ L(G5)={a^n^b^n^c^n^︱n≥1}。

G5 =(VN,VT,P,S),

其中,VN={S,B,C},

VT={a,b,c},

P ={S→aSBC︱aBC,CB→BC,

​ aB→ab,bB→bb,

​ bC→bc,cC→cc}

2型文法

设文法G=(VN,VT,P,S),如果任意α→β∈ P,α∈VN ,则称文法G属于2型文法。2型文法,也称为上下文无关文法。

例3.6 文法G6定义如下,显然G6是2型文法。

L(G6)={w$w^R^︱n≥0, w^R^ 为w之逆,w∈{0,1}*}

G6 =(VN,VT,P,S),

其中,VN={S},

​ VT ={$,0,1},

​ P ={S→0S0︱1S1︱$ }

3型文法

文法 设文法G=(VN,VT,P,S),如果任意α→β∈ P,α∈ VN ,且β只能是aB或a(除空规则之外),则称文法G属于右线性3型文法。【B是非终结符】

设文法G=(VN,VT,P,S),如果任意α→β∈ P,α∈ VN ,且β只能是Ba或a(除空规则之外),则称文法G属于左线性3型文法。

左线性3型文法和右线性3型文法,统称3型文法,也称为正规文法。

例2.7 文法G7定义如下。显然G7是3型文法。

​ L(G7)={00,01,10,11}。

G7 =(VN,VT,P,S),

其中,VN={S,A,B},

VT={0,1},

P ={S→A0︱B1,A→0︱1,B→0︱1}

文法分类是对规则形式逐步加以限制而得。换言之,从0型文法到1型文法、2型文法和3型文法,其规则形式逐步简单。自然,其表达力也随之逐步减弱。

如果L0、L1、L2和L3分别是0型文法、1型文法、2型文法和3型文法能产生的语言之集,则有如下关系:

L0 ⊋ L1 ⊋ L2 ⊋ L3。

上下无关文法及其语法树

上下无关文法一个显著特征是规则左部一定有且仅有一个非终结符。利用这个特征,可以不列出VN和VT ,给出一个上下无关文法的简洁描述方法:①文法名G改写成G[S],其中,S表示开始符;②规则集P,仅书写其具体规则。

最左推导、最右推导

如果在推导的每一步总是选择当前句型的最左(最右)边非终结符进行推导,则称这种推导过程为最左(最右)推导。最右推导,也叫规范推导。由规范推导所得的句型,叫做规范句型。规范推导的逆过程,叫做规范归约。

G[S]:S→aAS︱a

A→SbA︱SS︱ba

最左推导:S => aAS => aSbAS => aabAS => aabbaS => aabbaa

最右推导:S => aAS => aAa => aSbAa => aSbbaa => aabbaa

一般推导:S => aAS => aSbAS => aSbAa => aabAa => aabbaa

语法树

假设文法G=(VN,VT,P,S),则文法G的语法树是一个满足下列条件的多叉树:

(1)以文法开始符S做为树根;

(2)以终结符号或非终结符号做为树的其他结点,且子树根和其孩子结点分别是某规则的左部和右部。

推论: ①非叶子结点一定是非终结符

           **②全部叶子结点组成的符号串是文法的句子**

语义二义性

如果一个文法G,某个句子存在对应的至少两棵不同的语法树,则称文法G是二义性的。

image-20200328134255807

推论

① 如果文法是无二义性的,一个句子的语法树反映了该句子的全部推导过程;

如果文法是无二义性的,一个句子的最左(最右)推导是唯一的。

语义的先天二义性

文法的二义性,并不等同于语言的二义性,尽管两者之间可能存在非必然的联系。

因为二义性文法G,可能存在与之等价的无二义性的文法G′,即L(G)=L(G′)。

如果一个语言不存在无二义性的文法,则称该语言是先天二义性的。

例如,语言L={a^i^b^j^c^k^︱(i=j 或i=k),(i,j,k≥1)}不存在无二义性的文法,是先天二义性的语言。

已经证明:文法的二义性判定问题是递归不可解的。即不存在这个判定问题的算法。

句型分析

假设文法G[S]是语言L之文法,即L(G)=L,则“符号串α是否符合语言L的语法问题”被等价地转化成“推导或归约问题”,即:

【从起始符推导出α,并且α是非终结符组成的串,也就是α不能再细分】

【归约和推导可以视作是一个相反的过程】

这样,自然地形成了推导法和归约法两大类分析方法。推导法和归约法,也分别称为自上而下的分析方法和自下而上的分析方法。

自上而下的分析方法

从文法开始符号出发,反复使用规则,寻找匹配符号串(推导)的句型,直到推导出句子或规则用遍。进行每步推导时,存在两个选择问题:

⑴ 选择句型中哪一个非终结符进行推导

⑵ 选择非终结符的哪一个规则进行推导

问题⑴可以采用最左推导解决。问题⑵通常需要穷举每一个规则的可能推导。

成功:在推到过程中一旦出现个符号串α,便结束穷举过程,断定符号串α是句子。

失败:当穷举全部可能的推导,而不存在一个符号串α之推导过程的时候,才可以断定符号串α不是句子。

自下而上的分析方法

从输入符号串α开始,逐步进行“归约”,直至归约出文法的开始符号 S,则输入串α是文法G定义的语言的句子。否则不是。

这种分析方法在进行每步归约时,存在两个如何选择句型α的子串β进行归约的问题(α=β δ)。

如果文法规则没有相同的右部,则在语法分析的过程中,一旦出现子串β与某条规则的右部相同,就可以使用这条规则进行归约,简单优先分析法就是采用此方法进行归约。

但这种限制,实际上也限制了文法的表达能力,所以通常是通过在句型中寻找所谓的“句柄”的途径解决的。

短语、直接短语、句柄

设G[S]是一文法,αβδ是文法G的句型,如果有S=>^*^αAδ且A=>^+^β,则称β是句型αβδ的、相对于非终结符A的短语。

特别地,当A=>^+^β实际是A=>β即一步推导时,则又称β是句型αβδ的、相对于非终结符A的直接短语(或简单短语)。

句型的最左直接短语,称为该句型的句柄。

短语的理解:

“αβδ是文法G的句型”,即S =>^*^αβδ

“S=>^*^αAδ且A=>^+^β”,即S=> … =>αAδ=> … =>αβδ

这表明,如果β是句型αβδ的、相对于A的短语,则至少存在一个推导,使得αAδ =>^+^ αβδ,或者αβδ<=^+^ αAδ。

特别地,如果β是直接短语,则αAδ => αβδ,或者αβδ<=αAδ。

【直接短语、短语都是某一个句型的子串】

在语法树中,短语是子树的叶子的组合直接短语是两层子树的末端【i3和F1是两层子树】

文法在实用中的一些说明

在实际应用中,对于文法规则提出了一些限制条件,但这些并没有限制文法的语言描述能力。限制下列 3种规则的使用:

(1)有害规则 形如U→U的规则,称为有害规则。

(2)不可达规则 不在任何规则右部出现的非终结符对应的规则,称为不可达规则。

(3)不可终止规则 如果从某非终结符开始,不可能推导出任意终结串来,则该非终结符对应的规则称为不可终止规则。

不含有多余规则的文法,称为压缩过的文法。在后面讨论的文法时,都假设是压缩过的的文法。

ε规则问题

在文法设计中,使用ε规则有时会带来方便,但会导致文法讨论和证明的复杂。

一个上下文无关文法G是否必须使用ε规则,完全取决于文法G产生的语言L(G[S])中是否含有ε语句。

可以证明,如果ε不属于L(G[S]),则存在一个等价的文法G’[S’] ,且G’ 不含ε规则。

如果ε∈ L(G[S]),则存在一个等价的文法G’[S’] ,且G’ 仅含S’ →ε的一个空规则。

提示:使用“代入法”,即可得到等价的文法G’(S’)