输入系统
它的作用是将源文件从磁盘或内存中读入,根据模块化设计原理,如果输入系统是一个独立模块,通过固定接口与词法解析器交互的话,那么它的修改和维护将会非常灵活。
输入系统的效率,决定着整个编译系统的效率。 我们用C语言的时候,经常会用到他提供的输入函数例如 scanf 等C语言的输入系统设计得不是很合理。当C语言的库函数将数据读入程序的过程中,有三次拷贝,这些拷贝都需要耗费时间和空间。
- 从磁盘上将数据拷贝到操作系统中
- 将数据从操作系统拷贝到一个 FILE 结构中
- 将数据从FILE 结构拷贝到程序的内存中
另外,词法解析器在解析时需要预先读入一些字符(look ahead), 以便对输入的字符串打上合适的标签(想象前面的typeof 语句, 一旦读到 typeof 那就需要将 typeof 后面的字符读进来才好解析),预先读入的字符,使用完后,可能需要重新放回到缓冲区中,这一取一放,如果不加以良好的设计,那很可能会产生 I/O 性能上的影响。
一个输入系统的实现
鉴于以上特点,现有的输入系统无法满足编译系统输入的各种要求,因此,我们需要自己开发一个专用的输入系统。我们的输入系统应当具备以下特点:
- 输入过程要尽可能的快,尽力减少不必要的拷贝。
- 支持多个字符的预读取和回放。
- 当解析当前分割的字符串时,上一个解析过的字符串需要容易的获取。
- 磁盘读写要足够便捷。
startBuf
指向缓冲区的起点
END
指向缓冲区的物理结束位置
endBuf
指向缓冲区的逻辑结束位置,也就是数据最多存储到endBuf 处
MAXLEX
是分割后字符串的最多长度,一般设为128, 大家在写代码时没有写过长度达128个字符的变量名吧。我们一次从磁盘读入缓冲区的数据量是MAXLEX 的倍数,也就是说,endBuf – startBuf 一定是MAXLEX的整数倍。
Next
指向下一个要读取的字符,词法解析器是一个字符一个字符从缓冲区读取数据的。
pMark
指向上一个被解析的字符串的起始位置
sMark
指向当前被解析的字符串的起始位置
eMark
指向当前解析的字符串的结束位置
sMark – pMark 是上一个字符串的长度
eMark – sMark 是当前解析字符串的长度
词法解析器在解析时需要读取字符串后的若干字符,所有Next 往后挪动了一段距离。这也就是前面提到的look ahead。
我们前面提到过,预先读取和读取后放回缓冲区需要加以处理,在我们给的的内存模型中,当需要把预先读到的字符退回缓存区时,只要把 Next 设置成 eMark 就可以了。
look ahead 也就是预读取的字符个数是有限制的,限制的个数用常量MAXLOOK表示,也就是
Next – eMark <= MAXLOOK
当 Next 的值接近danger 时,表明缓冲区内的有效数据快被读取完了,当 Next 指针越过 danger 时,就会触发一次 flush
操作
flush 操作的效果是,将pMark 到 endBuf间的数据整体平移到startBuf处,
平移的距离是:pMark – startBuf
数据的拷贝和平移很耗时间,只要内存空间取得足够大,数据取得足够合理,平移次数会很少。所以这种方式在 I/O 性能上,具有促进作用
项目结构
Input.java
是项目的主体,该文件是输入系统的具体实现。FileHandler
是一组输入流读取接口,输入系统根据它提供的接口,从输入流中获取字符信息。DiskFileHandler
, StdInHandler
是 FileHandler 的具体实现。
Eof_read
表示输入流中是否还有可读的信息,如果输入流来自于文件,那么当读到文件末尾时 Eof_read 设置为 true。
我们需要注意,当输入流中没有多余信息时,我们的缓冲区中有可能还有没有读取的信息,因为我们是将信息从输入流放入缓冲区后,再从缓冲区中取出信息进行处理的。
noMoreChars
返回 true 则表明,缓冲区和输入流都没有可读信息了。
大家可能会发觉 End_Buf 是缓冲区的逻辑结束地址,按照前面讲的,在它后面其实还有一段浪费的可用内存,为何这里它直接等于实际的内存结束地址呢,真实原因是在后面的代码中,它的值会做相应调整。
在接下来的代码中,有一个函数是 ii_newfile
, 该接口用于决定输入流是磁盘文件还是控制台,如果 ii_newfile 的输入参数 filename 不是null 那么就以磁盘文件作为输入流,要是 null,就以控制台为输入流。
要注意ii_newfile 并没有做将数据从输入流读入缓冲区的操作,它仅仅是初始化一些指针或变量。
真正的将输入流读入缓冲区的是 ii_advance
函数,该函数的作用是从缓冲区中读取字符数据。
将数据读入缓冲区是比较耗时的操作,因此输入系统会等待外部真正请求数据时,才好触发数据读取操作。
当 Next 指针越过 Danger 时将会引发 Flush 操作,也就是将数据从输入流中读入缓冲区。在 ii_newfile 的初始化中,我们特意将 Next 指针设成缓冲区的末尾,当 ii_advance 第一次执行时,发现 Next 越过了 Danger,于是引发 Flush 操作,这样,数据就从输入流写入缓冲区了。
ii_text()
ii_length()
ii_lineno()
用于返回当前要分析的字符串,字符串的长度,和所在的行号。ii_ptext()
ii_plength()
ii_plineno()
用于返回上一个被解析的字符串
ii_flush
该函数负责执行 flush 操作,它先把缓冲区中还没有读取的数据向左平移,接着从输入流中读入数据,填充平移后产生的可用空间。
/*
* flush 缓冲区,如果Next 没有越过Danger的话,那就什么都不做
* 要不然像上一节所说的一样将数据进行平移,并从输入流中读入数据,写入平移后
* 所产生的空间
* pMark DANGER
* | |
* Start_buf sMark eMark | Next End_buf
* | | | | | | |
* V V V V V V V
* +---------------------------------------------------------+---------+
* | 已经读取的区域 | 未读取的区域 | 浪费的区域|
* +--------------------------------------------------------------------
* |<---shift_amt------>|<-----------copy_amt--------------->|
* |<-------------------------BUFSIZE---------------------------------->|
*
* 未读取区域的左边界是pMark或sMark(两者较小的那个),把 未读取区域平移到最左边覆盖已经读取区域,返回1
* 如果flush操作成功,1 如果操作失败,0 如果输入流中已经没有可以读取的多余字符。如果force 为 true
* 那么不管Next有没有越过Danger,都会引发Flush操作
*/
ii_flush 要做的是将未读取的区域,向左平移到 Start_buf 处,平移的距离就是 shift_amt, 未读取区域的长度是 copy_amt, shift_amt + copy_amt 就等于End_buf。
ii_flush 先判断 Next 指针是否在 DANGER 边界后面,然后才会执行平移写入操作。如果 Next 在 DANGER 前面,但是传进来的参数 force 为 true,那么也会强制进行平移写入操作。
接下来通过 java 库函数 arraycopy 进行数据的平移操作,left_edge 其实就是未读取区域的起始地址,平移后,从缓冲区的开头直到 copy_amt 处的数据都是未读取数据,因此从输入流填入数据是要从缓冲区的 copy_amt 处之后才开始,ii_fillbuf 的作用就是将数据从输入流写入缓冲区。平移后,一些指针也要做相应的调整。
ii_fillbuff
先从 fileHandler 的 read 函数中获取信息,也就是从输入流中读入数据,每次读入数据的数量用need表示,need是MAXLEX的整数倍。got 返回的是读到的数据量,如果读到的数据少于想要读取的数据,那就表明输入流中已经没有多余的信息可读了。