简洁之美,一段精彩的模拟器代码分析(c/c++)
by Yiran Xie
*如要转载请包含本文链接
背景
某日在Youtube上闲逛,看到一个名为 “Bisqwit”的家伙上传的视频,宣称他把编写NES模拟器的coding全过程记录了下来。抱着好奇的心态点进去一看,顿时惊为天人。这家伙仅用了几十分钟时间就编写出了一个非常精简的版本,简直不可思议。视频分为三部分:
-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和后续改写增加了难度。总而言之,这是一段暗含一点点炫技的代码,目的在于展示如何通过最少的代码去实现复杂的功能。