在Microchip dsPIC系列设备上,查找表是作为一组指令地址存储在Flash中的。执行查找需要从Flash中读取地址然后调用例程。进行调用会增加另外一些周期来推送指令指针和其他位和杂项(例如设置堆栈帧)的清理工作。
例如,在dsPIC33E512MU810上使用XC16(v1.24),查找代码如下:
lookUpTable[state]();
编译为(从MPLAB-X的反汇编窗口):
! lookUpTable[state]()
0x2D20: MOV [W14], W4
0x2D22: ADD W4, W4, W5
0x2D24: MOV #0xA238, W4
0x2D26: ADD W5, W4, W4
0x2D28: MOV [W4], W4
0x2D2A: CALL W4
每个(空的、无任何操作的)函数编译成:
!static void func1()
!{}
0x2D0A: LNK
! Function body goes here
0x2D0C: ULNK ; 1 cycle (un-link frame pointer)
0x2D0E: RETURN ; 3 cycles
这是任何情况下总共需要11个指令周期的开销,它们都需要相同的时间。(注意:如果表格或其中包含的函数不在同一个32K程序字闪存页中,则由于必须使地址生成单元从正确的页面读取或设置PC进行长调用而产生更大的开销。)
另一方面,只要整个switch语句适合某个特定大小,编译器将生成测试和相对分支的代码,每个case需要三个(或可能四个)周期,直到找到为真的那个。
例如,以下switch语句:
switch(state)
{
case FUNC1: state++; break;
case FUNC2: state
default: break;
}
编译为:
! switch(state)
0x2D2C: MOV [W14], W4 ; get state from stack-frame (not counted)
0x2D2E: SUB W4, #0x0, [W15] ; 1 cycle (compare with first case)
0x2D30: BRA Z, 0x2D38 ; 1 cycle (if branch not taken, or 2 if it is)
0x2D32: SUB W4, #0x1, [W15] ; 1 cycle (compare with second case)
0x2D34: BRA Z, 0x2D3C ; 1 cycle (if branch not taken, or 2 if it is)
! {
! case FUNC1: state++; break;
0x2D38: INC [W14], [W14] ; To stop the switch being optimised out
0x2D3A: BRA 0x2D40 ; 2 cycles (go to end of switch)
! case FUNC2: state--; break;
0x2D3C: DEC [W14], [W14] ; To stop the switch being optimised out
0x2D3E: NOP ; compiler did a fall-through (for some reason)
! default: break;
0x2D36: BRA 0x2D40 ; 2 cycles (go to end of switch)
! }
如果选择第一种情况,开销为5个周期;如果选择第二种情况,开销为7个周期,以此类推。这意味着在第四种情况时它们的成本相等。
这意味着在设计时了解数据将对长期速度产生重大影响。如果您有大量(超过4种情况)且它们发生的频率相似,则查找表在长期内将更快。如果情况的频率显着不同(例如,情况1比情况2更可能发生,情况2比情况3更可能发生等),则如果您按最可能的情况对开关进行排序,则开关在长期内将更快。对于只有少数情况的边缘情况,开关(可能)无论如何都会更快,对于大多数执行来说更易读且更少出错。
如果开关中只有几种情况,或某些情况比其他情况更常见,则测试和开关的分支所需的时间可能比使用查找表少。另一方面,如果您有超过一把手的情况发生的频率相似,则平均而言,查找表最终可能会更快。
提示:除非您知道查找表肯定会更快并且运行所需的时间很重要,否则请使用开关。
编辑:我的开关示例有点不公平,因为我忽略了原始问题并将情况的“主体”内联以突出使用开关而不是查找表的真正优势。如果开关也必须进行调用,则它仅在第一种情况下具有优势!