简洁之美,一段精彩的模拟器代码分析(c/c++)

简洁之美,一段精彩的模拟器代码分析(c/c++)

by Yiran Xie

*如要转载请包含本文链接

背景

某日在Youtube上闲逛,看到一个名为 “Bisqwit”的家伙上传的视频,宣称他把编写NES模拟器的coding全过程记录了下来。抱着好奇的心态点进去一看,顿时惊为天人。这家伙仅用了几十分钟时间就编写出了一个非常精简的版本,简直不可思议。视频分为三部分:

-Part1

-Part2

-FAQ

后来逐渐了解到Bisqwit是个有十多年开发经典的资深coder,之前已经编写过nes模拟器,这次只是觉得好玩+挑战才发布的视频。

 顺便说一下,这个网站可以在线分析youtube地址,并转化为相应的下载链接。这组视频很适合下载下来慢慢看

 

 

 

 

 

前言

Bisqwit编写模拟器用的是c++(11标准),总共才1000行左右,非常精简。看了它的代码,整体风格是成熟而晦涩的。其中不乏很多“代码之美”,有些片段细细推敲之后,发现颇有意思。

举个例子,下面这段代码是模拟CPU的,根据不同的opcode,模拟不同的指令。

void ExecuteInstruction(unsigned opcode)

    {

        int temp;



        #define t(pattern) if(decode_base64(pattern[opcode / 6]) & (1 << (opcode % 6)))

        t("/DAAAAAAAAAA") temp  = a;

        t("zAAAAAAAAAAA") temp += b;

        t("MDAAAAAAAAAA") temp -= b;

        t("iAAAAAAAAAAA") temp += c;

        t("ICAAAAAAAAAA") temp -= c;

        t("/DAAAAAAAAAA") a     = temp;

        t("PAAAAAAAAAAA") UpdateFlags(temp);

    }

第一次看的时候一点头绪也没有,后来看了他的FAQ才慢慢了解是怎么个思路。今天就针对这段精华代码做一下分析。

 

 

 

 

 

正文

首先创建一个CPU的类

class SomeCPUemulator

{

    //Registers

    int a, b, c;

    bool zeroflag, negativeflag;

    

    //Utility function which updates the flags after an operation

    void UpdateFlags(int value)

    {

        zeroflag = (value == 0);

        negativeflag = (value < 0);

    }

    

    //This function executes a single CPU instruction

    void ExecuteInstruction(unsigned opcode)

    {

        switch(opcode)

        {

            ...

        }

     }

};

上面的代码应该没什么难度。这个CPU类有3个通用寄存器(a, b, c),两个状态标志(zeroflag和negativeflag)。

有一些指令,要求在写入寄存器值的时候同时更新状态标志,这个功能由UpdateFlags()来完成。比如最近一条指令,涉及到把一个负数存储到了寄存器a里,那么只要调用UpdateFlags(a),negativeflag就会被置为true。

ExecuteInstruction()是执行CPU 指令(opcode)的主程序,每条opcode都有一个唯一的整数序号。整个架构用的是switch/case语句来完成根据不同的opcode进行不同的动作,这也是模拟器中实现opcode最常见的方式。另外两种常见的方式包括label +goto语句,以及函数指针表。

 

添加以下几条opcode作为范例:

         switch(opcode)

        {

            case 0://Add a and b, store to a

                a = a + b;

                UpdateFlags(a);

                break;

            case 1://Add a and b and c, store to a

                a = a + b + c;

                UpdateFlags(a);

                break;

            case 2://Subtract b from a, store to a

                a = a - b;

                UpdateFlags(a);

                break;

            case 3://Subtract b and c from a, store to a

                a = a - b - c;

                UpdateFlags(a);

                break;

         }

上面是一批需要update flags(更新状态位)的opcodes,再添加一批执行同样功能,但是不更新状态位的opcodes。现在的完整版本是:

void ExecuteInstruction(unsigned opcode)

    {

        switch(opcode)

        {

            //opcodes that update flags

            case 0://Add a and b, store to a

                a = a + b;

                UpdateFlags(a);

                break;

            case 1://Add a and b and c, store to a

                a = a + b + c;

                UpdateFlags(a);

                break;

            case 2://Subtract b from a, store to a

                a = a - b;

                UpdateFlags(a);

                break;

            case 3://Subtract b and c from a, store to a

                a = a - b - c;

                UpdateFlags(a);

                break;

                
//==============Newly Added==========================
//same as above, but don't update flags case 4://Add a and b, store to a a = a + b; break; case 5://Add a and b and c, store to a a = a + b + c; break; case 6://Subtract b from a, store to a a = a - b; break; case 7://Subtract b and c from a, store to a a = a - b - c; break;
            //===================================================
} }

 以上的代码看上去还不错,功能是完成了,但是显得很臃肿。那我们能不能做得更好呢?

经过观察,其中很多opcode执行的指令都很相似,比如case2/3/6/7中都做了a-b的操作,case 0/1/2/3中都调用了UpdateFlags()。于是想到,我们能不能把公用部分的提取出来呢?一种显而易见的思路是把a-b单独作为一个function,用的case调用它。

 

 

另一种更具创造力的思路是把代码写成如下的形式:

    void ExecuteInstruction(unsigned opcode)

    {

        int temp;

        if() temp  = a;//Read a

        if() temp += b;//Add  b

        if() temp -= b;//Subtract b

        if() temp += c;//Add c

        if() temp -= c;//Subtract c

        if() a     = temp;//Store to a

        if() UpdateFlags(temp);//Update flags

    }

以上的语句包含了上面所有opcode执行中所要调用的“子功能”,就像是一个流水线。

比如原来的case0,就相当于执行现在的

temp = a;

temp +=b;

UpdateFlags(temp)三条指令,或者说等价于下面的写法:

  void ExecuteInstruction(unsigned opcode)

    {

        int temp;

        if(true)  temp  = a;//Read a

        if(true)  temp += b;//Add  b

        if(false) temp -= b;//Subtract b

        if(false) temp += c;//Add c

        if(false) temp -= c;//Subtract c

        if(false) a     = temp;//Store to a

        if(true)  UpdateFlags(temp);//Update flags

    }

类似的,建立一个更详细的关于每条if语句被不同的opcode所执行的情况(见注释):

    void ExecuteInstruction(unsigned opcode)

    {

        int temp;

        if() temp  = a;//Read a                (opcode:0,1,2,3,4,5,6,7)

        if() temp += b;//Add  b                (opcode:0,1,    4,5    )

        if() temp -= b;//Subtract b            (opcode:    2,3,    6,7)

        if() temp += c;//Add c                 (opcode:  1      ,5    )

        if() temp -= c;//Subtract c            (opcode:      3,      7)

        if() a     = temp;//Store to a         (opcode:0,1,2,3,4,5,6,7)

        if() UpdateFlags(temp);//Update flags  (opcode:0,1,2,3        )

    }

 下面的问题就是如何根据上面右边的表格来填充左边的if()语句。这里用了如下的办法

    void ExecuteInstruction(unsigned opcode)

    {

        int temp;

        if("xxxxxxxx"[opcode] == 'x') temp  = a;//Read a                (opcode:0,1,2,3,4,5,6,7)

        if("xx__xx__"[opcode] == 'x') temp += b;//Add  b                (opcode:0,1,    4,5    )

        if("__xx__xx"[opcode] == 'x') temp -= b;//Subtract b            (opcode:    2,3,    6,7)

        if("_x___x__"[opcode] == 'x') temp += c;//Add c                 (opcode:  1      ,5    )

        if("___x___x"[opcode] == 'x') temp -= c;//Subtract c            (opcode:      3,      7)

        if("xxxxxxxx"[opcode] == 'x') a     = temp;//Store to a         (opcode:0,1,2,3,4,5,6,7)

        if("xxxx____"[opcode] == 'x') UpdateFlags(temp);//Update flags  (opcode:0,1,2,3        )

    }

即根据右边的表格建立一个常字符数组(同时也可以看做是一种roadmap),可以看到如要序号为i的opcode去执行第n条指令,则在第n条指令的roadmap中把第i个字符置为'x',否则置为'_'。判断时也一样,如果为'x'则需要执行,很容易理解。

 

PS:这样的写法第一眼看过去可能有点怪

"xxxxxxxx"[opcode]

举个例子就明白了。常见写法是这样:

char myStr[] = "abcde";

putchar(myStr[3]);//Prints 'd'

同时它等价于

int idx = 3;

putchar("abcde"[idx]);//Prints 'd'

 

 

 

好了,接着前面,把'x'替换为'1',把'_'替换为'0'(这步其实多此一举,只是为了引出之后的bitmap) 

     void ExecuteInstruction(unsigned opcode)

    {

        int temp;

        if("11111111"[opcode] == '1') temp  = a;//Read a                (opcode:0,1,2,3,4,5,6,7)

        if("11001100"[opcode] == '1') temp += b;//Add  b                (opcode:0,1,    4,5    )

        if("00110011"[opcode] == '1') temp -= b;//Subtract b            (opcode:    2,3,    6,7)

        if("01000100"[opcode] == '1') temp += c;//Add c                 (opcode:  1      ,5    )

        if("00010001"[opcode] == '1') temp -= c;//Subtract c            (opcode:      3,      7)

        if("11111111"[opcode] == '1') a     = temp;//Store to a         (opcode:0,1,2,3,4,5,6,7)

        if("11110000"[opcode] == '1') UpdateFlags(temp);//Update flags  (opcode:0,1,2,3        )

    }

然后可以把上面的包含1/0的字符串理解为一个二进制表示的数,不过与常见的表示相反,低位在左边,高位在右边。把他们转化为16进制,例如“11111111”可以“转化”为0xFF,而"11001100"(正常二进制写法实际为00110011)可以"转化"为0x33.

其实上面就是一种bitmap的概念,即每个bit表示一个相应序号的状态。接着我们可以通过移位去取当前opcode序号的状态,代码就变成了

void ExecuteInstruction(unsigned opcode)

    {

        int temp;

        if(0xFF & (1 << opcode)) temp  = a;//Read a                (opcode:0,1,2,3,4,5,6,7)

        if(0x33 & (1 << opcode)) temp += b;//Add  b                (opcode:0,1,    4,5    )

        if(0xCC & (1 << opcode)) temp -= b;//Subtract b            (opcode:    2,3,    6,7)

        if(0x22 & (1 << opcode)) temp += c;//Add c                 (opcode:  1      ,5    )

        if(0x88 & (1 << opcode)) temp -= c;//Subtract c            (opcode:      3,      7)

        if(0xFF & (1 << opcode)) a     = temp;//Store to a         (opcode:0,1,2,3,4,5,6,7)

        if(0x0F & (1 << opcode)) UpdateFlags(temp);//Update flags  (opcode:0,1,2,3        )

    }

以上是一种表示方法,对于opcode数量不太多时是比较管用的。不过假设我们有不止8条opcodes,而是有72条opcodes。则可以想象到的,那个2进制数会变为72位(之前是8位)。比如之前为"11001100",现在为成"110011000000000000000000000000000000000000000000000000000000000000000000"。

如果用HEX(16进制)去表示,则也需要18位(72 / log2(16))。那么写起来将是类似"0x000000000000000033"这样长的数,看起来很没有美感。

 

 

那么有没有办法去更紧凑地表示它呢?

这里想到用26个大写英文字母,26个英文小写字母,10个数字和两个可打印的符号来表示,一共64位:

即"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"

没错,这就是传说中的"BASE64",用在MIME(email协议)等地方。wiki中的解释:点我

 

用HEX去表示需要18位,现在用BASE64去表示的话,嗯。。只需要12位(72 / log2(64))就够了。好像也没节省太多呢?唔,能节省一点算一点吧。

 

现在我们把开始的二进制数转为base64表示吧。

比如还是这个例子"110011000000000000000000000000000000000000000000000000000000000000000000",取6位为1位,转化为十进制,然后查表

Value Char   Value Char   Value Char   Value Char
0 A 16 Q 32 g 48 w
1 B 17 R 33 h 49 x
2 C 18 S 34 i 50 y
3 D 19 T 35 j 51 z
4 E 20 U 36 k 52 0
5 F 21 V 37 l 53 1
6 G 22 W 38 m 54 2
7 H 23 X 39 n 55 3
8 I 24 Y 40 o 56 4
9 J 25 Z 41 p 57 5
10 K 26 a 42 q 58 6
11 L 27 b 43 r 59 7
12 M 28 c 44 s 60 8
13 N 29 d 45 t 61 9
14 O 30 e 46 u 62 +
15 P 31 f 47 v 63 /

 得到其12位的BASE64表示为”zAAAAAAAAAAA“。

 

相应地,更新所有命令,得到类似这样的代码

 void ExecuteInstruction(unsigned opcode)

    {

        int temp;

        unsigned index = opcode / 6;

        unsigned bitmask = 1 << (opcode % 6);

        if("/DAAAAAAAAAA"[index] & bitmask) temp  = a;//Read a                (opcode:0,1,2,3,4,5,6,7)

        if("zAAAAAAAAAAA"[index] & bitmask) temp += b;//Add  b                (opcode:0,1,    4,5    )

        if("MDAAAAAAAAAA"[index] & bitmask) temp -= b;//Subtract b            (opcode:    2,3,    6,7)

        if("iAAAAAAAAAAA"[index] & bitmask) temp += c;//Add c                 (opcode:  1      ,5    )

        if("ICAAAAAAAAAA"[index] & bitmask) temp -= c;//Subtract c            (opcode:      3,      7)

        if("/DAAAAAAAAAA"[index] & bitmask) a     = temp;//Store to a         (opcode:0,1,2,3,4,5,6,7)

        if("PAAAAAAAAAAA"[index] & bitmask) UpdateFlags(temp);//Update flags  (opcode:0,1,2,3        )

    }

因为一个BASE64是6个bit,所以用index去表示是第几个BASE64的字符,用bitmask去确定是该BASE64字符的第几位。

初看以上的代码很有道理,但是忽略了一点:类似"/DAAAAAAAAAAA"[index]取出来的值,虽然我们知道它代表了一个BASE64的字符,但是系统不知道,系统以为它就是一个char类型的。因此我们还需要把它还原成BASE64的值。

其实换个角度看,既然前面用了BASE64(通过查表)的编码,则解码过程显然也是必须的。

于是这里添加一个解码函数decode_based64(),代码变为:

 void ExecuteInstruction(unsigned opcode)

    {

        int temp;

        unsigned index = opcode / 6;

        unsigned bitmask = 1 << (opcode % 6);

        if(decode_base64("/DAAAAAAAAAA"[index]) & bitmask) temp  = a;        //(opcode:0,1,2,3,4,5,6,7)

        if(decode_base64("zAAAAAAAAAAA"[index]) & bitmask) temp += b;        //(opcode:0,1,    4,5    )

        if(decode_base64("MDAAAAAAAAAA"[index]) & bitmask) temp -= b;        //(opcode:    2,3,    6,7)

        if(decode_base64("iAAAAAAAAAAA"[index]) & bitmask) temp += c;        //(opcode:  1      ,5    )

        if(decode_base64("ICAAAAAAAAAA"[index]) & bitmask) temp -= c;        //(opcode:      3,      7)

        if(decode_base64("/DAAAAAAAAAA"[index]) & bitmask) a     = temp;     //(opcode:0,1,2,3,4,5,6,7)

        if(decode_base64("PAAAAAAAAAAA"[index]) & bitmask) UpdateFlags(temp);//(opcode:0,1,2,3        )

    }

 

 

decode_based64()这个函数因为会使用得很频繁,因此inline是必须的。

下面讨论下解码的过程:

假设value是一个char类型的数据,同时它里面存的是一个BASE64的值。还是通过查表,我们可以总结以下规律:

如果('A' <= value <= 'Z'),则其BASE64的值为value - 'A' + 0

如果('a' <= value <= 'z'),则其BASE64的值为value - 'a' + 26

如果('0' <= value <= '9'),则其BASE64的值为value - '0' + 52

如果(value = '+'),              则其BASE64的值为62 

如果(value = '/'),               则其BASE64的值为63

以上的代码当然可以用一个if(),else if(), else()这样的语句去写。

 

不过原作者显然不满与此,这里用了非常花哨的"连锁的"条件选择去写。

    constexpr unsigned decode_base64(char value)

    {

        return (value >= 'A' && value <= 'Z') ? (value - 'A' + 0)

             : (value >= 'a' && value <= 'z') ? (value - 'a' + 26)

             : (value >= '0' && value <= '9') ? (value - '0' + 52)

             : (value == '+' ? 62 : 63);

    }

仔细看应该能看明白。

这里的返回值用了constexpr关键字来修饰,这是c++ 11里的关键字。表明该函数的返回值是编译时已知的,意即:此函数内仅包含一条形如“return 常量表达式;”这样的语句。

 

 

回头再看ExecuteInstruction这个函数,里面的每个if语句有个共用的格式如下:

decode_base64("XXXXXXXXXXXX"[index]) & bitmask

于是想到可以用宏(macro),让预编译器去帮我们处理。

于是定义:

#define t(pattern) if(decode_base64(pattern[index]) & bitmask)

同时把index和bitmask的变量也顺便取消吧,把相应的操作移入宏中。

#define t(pattern) if(decode_base64(pattern[opcode / 6]) & (1 << (opcode % 6)))

好了,宏的编写完成。拿宏来替换原有的冗长的命令,看看下面的新旧版本的对比吧:

if(decode_base64("/DAAAAAAAAAA"[index]) & bitmask) temp  = a;//old version 
t("/DAAAAAAAAAA") temp  = a;//new version

 

好了,大功告成。

最后我们的代码是这样的。是不是简洁多了呢?

class SomeCPUemulator

{

    //Registers

    int a, b, c;

    bool zeroflag, negativeflag;

    

    //Utility function which updates the flags after an operation

    void UpdateFlags(int value)

    {

        zeroflag = (value == 0);

        negativeflag = (value < 0);

    }

    

    //Decode from char to Base64 value

    constexpr unsigned decode_base64(char value)

    {

        return (value >= 'A' && value <= 'Z') ? (value - 'A' + 0)

             : (value >= 'a' && value <= 'z') ? (value - 'a' + 26)

             : (value >= '0' && value <= '9') ? (value - '0' + 52)

             : (value == '+' ? 62 : 63);

    }

    

    //This function executes a single CPU instruction

    void ExecuteInstruction(unsigned opcode)

    {

        int temp;



        #define t(pattern) if(decode_base64(pattern[opcode / 6]) & (1 << (opcode % 6)))

        t("/DAAAAAAAAAA") temp  = a;//Read a                (opcode:0,1,2,3,4,5,6,7)

        t("zAAAAAAAAAAA") temp += b;//Add  b                (opcode:0,1,    4,5    )

        t("MDAAAAAAAAAA") temp -= b;//Subtract b            (opcode:    2,3,    6,7)

        t("iAAAAAAAAAAA") temp += c;//Add c                 (opcode:  1      ,5    )

        t("ICAAAAAAAAAA") temp -= c;//Subtract c            (opcode:      3,      7)

        t("/DAAAAAAAAAA") a     = temp;//Store to a         (opcode:0,1,2,3,4,5,6,7)

        t("PAAAAAAAAAAA") UpdateFlags(temp);//Update flags  (opcode:0,1,2,3        )

    }

};

 

 

 

 

后记

这样的代码是很有简洁之美的,阅读、理解它的过程也很愉悦,也开阔了我的眼界。要说缺点,也显而易见。首先,多条if语句是顺序执行,判断效率并不会比原来的switch/case要高(switch/case可以生成jump table,几乎是O(1)的时间),另外decode function也会额外带来运算量,所以论执行效率只会比传统写法更低。其次,由于代码的高度聚合性,给debug和后续改写增加了难度。总而言之,这是一段暗含一点点炫技的代码,目的在于展示如何通过最少的代码去实现复杂的功能。

你可能感兴趣的:(c/c++)