zoukankan      html  css  js  c++  java
  • 扫描器的高效实现

    扫描器的高效实现

     

    编译器进行词法分析时,不可避免地需要对源文件进行扫描,实现该功能的模块称为扫描器。扫描器读取源文件,按序返回文件内的字符,直到文件结束。

    扫描器的功能

    实现文件的读一般使用库函数fscanf或者fread,那么按照怎样的读取方式才能让扫描器的性能更佳呢?

    (1)使用fscanf逐字扫描,并返回。

    char scan(FILE*file){

        char ch;

        if(fscanf(file,"%c",&ch)==EOF){

           ch=-1;

        }

        return ch;

    }

    这是最简单的实现方式,缺点是每次读取字符时都需要访问文件进行IO。

    我们先看看它的效率,首先在主函数内调用扫描器。

    //主函数

    int main()

    {

        FILE*file=fopen("test.c","r");

        for(int i=0;i<1000000;i++)

           while(scan(file)!=-1);

        fclose(file);

        return 0;

    }

    主函数内使用扫描器扫描测试文件1000000次,然后使用time命令查看代码的执行时间(测试文件可以是任意文件,这里不给出文件内容了,区别只是执行的时间不同而已)。

    $ time main

    我们测试了五次,并取了平均值(单位:ms)。

    次数

    1

    2

    3

    4

    5

    平均值

    real

    370

    366

    374

    368

    377

    371

    user

    112

    140

    132

    144

    140

    133.6

    sys

    256

    228

    240

    224

    232

    236

    可以计算代码的CPU平均执行时间为133.6+236=369.6ms。

    (2)使用fscanf和缓冲区结合的方式,每次读取字符时首先尝试从缓冲区内取,缓冲区为空时使用fscanf重新加载缓冲区。

    #define BUFLEN 80

    int lineLen=0;

    int readPos=-1;

    char line[BUFLEN];

    char scan(FILE*file)

    {

        if(readPos==lineLen-1)//缓冲区读取完毕

            {

           int pos;

            for(pos=0;pos<BUFLEN;pos++)//尝试将缓冲区加载满

               if(fscanf(file,"%c",&line[pos])==EOF){//加载时文件结束

                   line[pos++]=-1;//文件结束标记

                  break;

               }

            lineLen=pos;//记录缓冲区长度

           readPos=-1;//恢复读取位置

        }

        readPos++;//移动读取点

        return line[readPos]; //获取新的字符

    }

    按照前边的方式测试代码的执行时间。

    次数

    1

    2

    3

    4

    5

    平均值

    real

    374

    380

    371

    371

    374

    374

    user

    140

    124

    148

    136

    108

    131.2

    sys

    232

    252

    220

    232

    264

    240

    计算代码的CPU平均执行时间为131.2+240=371.2ms。虽然这种方法只是在缓冲区为空时重新加载缓冲区,避免了每次读取符号进行文件IO的问题,但是却比第一种方式的性能还差。主要是因为加载缓冲区时使用fscanf是按照字节进行加载的,如果使用fread可能会不同。

    (3)和方法二类似,只是加载缓冲区时使用fread。fread的参数size表示加载数据块的大小,count表示加载数据块的个数,这里每次加载BUFLEN个1字节数据块。

    #define BUFLEN 80

    int lineLen=0;

    int readPos=-1;

    char line[BUFLEN];

    char scan(FILE*file)

    {

        if(readPos==lineLen-1)//缓冲区读取完毕

            {

           lineLen=fread(line,1,BUFLEN,file);//重新加载缓冲区数据

           if(lineLen==0)//没有数据了

            {

               lineLen=1;

               line[0]=-1;//文件结束标记

            }

            lineLen=pos;//记录缓冲区长度

           readPos=-1;//恢复读取位置

        }

        readPos++;//移动读取点

        return line[readPos]; //获取新的字符

    }

    测试代码的执行时间。

    次数

    1

    2

    3

    4

    5

    平均值

    real

    339

    332

    334

    341

    332

    335.6

    user

    96

    92

    116

    108

    84

    99.2

    sys

    244

    236

    216

    228

    244

    233.6

    计算代码的CPU平均执行时间为99.2+233.6=332.8ms,可见使用fread读取文件比使用fscanf的效率要高。该方法是每次加载BUFLEN个1字节数据块,如果每次加载1个BUFLEN字节数据块是不是更高效呢?

    (4)和方法三类似,只是这里每次加载BUFLEN个1字节数据块。

    #define BUFLEN 80

    int lineLen=0;

    int readPos=-1;

    char line[BUFLEN];

    static int offset=0;//记录文件指针的偏移,每次打开文件需要初始化为0

    char scan(FILE*file)

    {

        if(readPos==lineLen-1)//缓冲区读取完毕

            {

           lineLen=fread(line,BUFLEN,1,file);//重新加载缓冲区数据

            if(lineLen)//成功加载

               offset+=BUFLEN;//累计偏移

               lineLen=BUFLEN*1;

            }

            else{//最后一段不足BUFLEN,无法获取读取的长度

                fseek(file,offset,SEEK_SET);//撤回文件指针

                lineLen=fread(line,1,BUFLEN,file);//按字节重读最后一段

                line[lineLen++]=-1;//文件结束标记

            }

            lineLen=pos;//记录缓冲区长度

           readPos=-1;//恢复读取位置

        }

        readPos++;//移动读取点

        return line[readPos]; //获取新的字符

    }

    测试代码的执行时间。

    次数

    1

    2

    3

    4

    5

    平均值

    real

    2241

    2223

    2259

    2223

    2221

    2233.4

    user

    620

    540

    616

    600

    648

    604.8

    sys

    1616

    1680

    1640

    1620

    1568

    1624.8

    计算代码的CPU平均执行时间为604.8+1624.8=2229.6ms,这有点出乎意料。分析原因,可能是因为文件较小时,当读取到最后一块缓冲区时,撤回文件指针比较消耗时间。因此,使用方法三实现的扫描器性能更稳定,且代码更简洁。

    (5)如果使用fread不是读入1个BUFLEN字节数据,而是读取BUFLEN/2个2字节数据,那么最后一次将读入最后一个字节。

    #define BUFLEN 80

    int lineLen=0;

    int readPos=-1;

    char line[BUFLEN];

    char scan(FILE*file)

    {

        if(readPos==lineLen-1)//缓冲区读取完毕

            {

           lineLen=fread(line,2,BUFLEN/2,file);//重新加载缓冲区数据

            if(lineLen){//成功加载

                   lineLen=lineLen*2;//读取了偶数字节

            }

            else{//最后一字节已经存储在line[0]

               line[1]=-1;//文件结束标记

               lineLen=2;//长度

            }

            lineLen=pos;//记录缓冲区长度

           readPos=-1;//恢复读取位置

        }

        readPos++;//移动读取点

        return line[readPos]; //获取新的字符

    }

    测试代码的执行时间。

    次数

    1

    2

    3

    4

    5

    平均值

    real

    663

    657

    646

    665

    659

    658

    user

    208

    224

    204

    212

    184

    206.4

    sys

    452

    428

    440

    456

    472

    449.6

    计算代码的CPU平均执行时间为206.4+449.6=656ms。虽然该方法不需要撤回文件指针,但是性能仍不如方法三。

    综上所述,方法三是实现扫描器比较高效的方式:使用缓冲区代替文件的频繁访问,并在缓冲区读取完毕时,使用fread每次加载BUFLEN个1字节数据块更新缓冲区数据。希望本文对你有所帮助。

     

  • 相关阅读:
    第五章 Internet协议
    第四章 地址解析协议
    Learn the shell
    Linux学习前的准备
    第三章 链路层
    第二章 Internet 地址结构
    后台数据导出为Excel
    C#开发客户端、JAVA和tomcat开发服务端
    Java基础
    C++学习笔记--(1)
  • 原文地址:https://www.cnblogs.com/fanzhidongyzby/p/3577459.html
Copyright © 2011-2022 走看看