我编写程序至今有35年了,我做了很多关于程序执行速度方面优化的工(一个示例),我也看过其它人做的优化。我发现有两个最基本的优化技术总是被人所忽略。
注意,这两个技术并不是避免时机不成熟的优化。并不是把冒泡排序变成快速排序(算法优化)。也不是语言或是编译器的优化。也不是把 i*4写成i<<2的优化。
这两个技术是:
- 使用 一个profiler。
- 查看程序执行时的汇编码。
熟读而精思,循序而渐进,厚积而薄发。
我编写程序至今有35年了,我做了很多关于程序执行速度方面优化的工(一个示例),我也看过其它人做的优化。我发现有两个最基本的优化技术总是被人所忽略。
注意,这两个技术并不是避免时机不成熟的优化。并不是把冒泡排序变成快速排序(算法优化)。也不是语言或是编译器的优化。也不是把 i*4写成i<<2的优化。
这两个技术是:
本程序利用PIC18单片机实现了对EEPROM的读写。应用背景是”汽车里程表”,简单介绍下程序功能和流程。
单片机接受来自RA4管脚的计数脉冲信号,应用中这个脉冲信号通常由一个传感器来产生,计数脉冲被设定为上升沿触发。计数器溢出时,需要更新一个用于存储当前里程的RAM寄存器单元COUNT,可以理解为车轮转了一定圈数后,里程表的增加一定的里程数。
RB0管脚用来接收一个外部中断信号,接收到中断信号后,在中断服务程序中将COUNT的值即当前里程数保存至EEPROM的00H单元。这样可以配合外部电路实现当单片机掉电时自动保存里程表数值的功能,即汽车熄火时保存里程表,以减少对EEPROM的读写次数。
单片机启动时,首先应该到EEPROM的相应单元读取里程表的当前值,并在此基础上进行累加。
程序中为了调试方便,还将COUNT的数值同步地通过8个LED以二进制形式较直观地显示了出来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | LOOP BCF INTCON, T0IF ; 清TMR0中断标志 MOVLW TMR0B MOVWF TMR0L ; 装入计数初值 253 BSF T0CON, TMR0ON ; 启动计数器 TEST BTFSS INTCON, T0IF ; 检测TMR0是否溢出 GOTO TEST INCF COUNT, F ; 计数加一 MOVFF COUNT, PORTD ; 输出,显示 GOTO LOOP WIRT_EE ; 写EEPROM BCF EECON1, EEPGD BCF EECON1, CFGS ; 设定EECON1控制寄存器 BSF EECON1, WREN ; EEPROM写使能 BCF INTCON, GIE ; 写EEPROM时需要关闭一切中断 |
使用PIC18单片机的ADC转换模块对RA0口输入的模拟电压信号进行转换,然后通过PORTD端口输出,而这里与PORTD对应引脚相连接的是8个LED。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | list P = 18F452 ;指明单片机型号为PIC18F452 #include P18F452.INC ;包含一个头文件,其中定义了一些端口及一些特殊寄存器的地址 org 0000h ;PIC上电时从0000h单元开始执行 goto main ;跳转到主程序 ORG 0008H ;中断向量入口 BTFSS PIR1, ADIF ; AD转换完成中断 RETFIE GOTO AD_ISR ORG 0030H ;主程序定位 MAIN CLRF TRISD ;设定D口方向为输出 CLRF PORTD ;设定C口方向为输出 BSF TRISA, 0 ; 使用AN0输入 MOVLW 81H ;FOSC/32, AN0, 开启 MOVWF ADCON0 MOVLW 0EH ;左对齐,AN0为模拟输入 MOVWF ADCON1 ;VDD & VSS为参考电压 BCF INTCON, TMR0IF BCF PIR1, ADIF ;清AD中断标志位 BSF PIE1, ADIE ;开AD中断 BSF INTCON, PEIE ;开外围中断 BSF NTCON, GIE ;开总中断 MOVLW C7H ;TMR0 8位,分频比为1:256 MOVWF T0CON LOOP CALL DELAY BSF DCON0, GO ;开启 A/D转换 GOTO LOOP DELAY BTFSS NTCON, TMR0IF ; 等待延时,采样保持 GOTO DELAY BCF NTCON, TMR0IF RETURN AD_ISR ;AD转换完成时调用的中断服务程序,将转换结果输出 ORG 0200H MOVFF ADRESH, PORTD ;显示转换结果 BCF PIR1, ADIF ;清AD中断标志位 RETFIE END ;程序结束 |
1 2 3 4 5 6 7 | int main() { char * str = new char[32]; str = "Hello, Piggy!"; return 0; } |
这样是会内存泄漏的……而我一直都不知道……不过想来也自然,因为这样是允许的,char *str = “Hello, Piggy!”;,“程序中的字符串被存放在常量存储区”不要把这句话当成耳旁风,谨记。
有的东西,你自己觉得自己理解了知道了记住了,可能你真的记住了,但你真的理解了吗?Put a “Why” upon everything ever you touch.
sizeof仅仅是个运算符,但那又意味着什么呢?意味着它不是一个函数,意味着它是在编译期求值(我并不是说所有的运算符都编译期求值)的。对于这样一条语句:
1 | size_t size = sizeof(int); |
来说,仅仅对应着这样一条汇编指令:
1 | movl $4, -4(%ebp) |
这对于自定义类型class也是同样的。
PIC的汇编实在诡异,有点被颠覆的感觉,原来汇编指令还可以这么来设计,原来汇编指令怎么设计都可以。最OOXX的一条指令就一个实现短转移的指令叫做BRA,意为BRAanch,看到这条指令的时候,我都诧异了,奶罩能做什么?哇塞!居然还能跳转!?奶罩居然可以无条件跳转?!Orz……另外PIC指令把单词缩写运用的淋漓尽致,譬如指令BTFSS,是一条位测试加条件跳转指令:BTFSS = Bit + Test + FileRegister + Skip + Set,用法:BTFSS R1, 0003h, 寄存器R1的第3位为1时跳过下一条指令。真是震撼!
最后附上PIC18的中断体系硬件结构图,出自陈育斌老师的手笔:
这里面猫腻儿还真不少。
karmic到源里面没有以前到vim-full包了,取而代之到是vim包,但现在有很多问题,n”+yy和n”+p无法和系统”剪切板”里到内容关联了,不知道什么原因。
Update
一切都释然了,只要意识到,>也需要进行类型提升。那么后面到-1啦-2啦之类的,都是很大很大的数了,循环根本就进不去,更别提死循环了。这样看来四、五两段程序就没有必要列出来了。其实,我是被它们的汇编代码给迷惑住了。
这里面没有用到C库,也没有main函数,为了把这个程序编译成可执行文件,需要指定程序的入口。编译指令:
1 2 3 4 5 6 7 8 9 10 | $ gcc -c nomain.c $ ld -e nomain nomain.o -o nomain $ ./nomain $ echo $? 42 $ ls -l nomain -rwxr-xr-x 1 ivan ivan 618 2009-09-02 22:11 nomain $ strip nomain $ ls -l nomain -rwxr-xr-x 1 ivan ivan 356 2009-09-02 22:15 nomain |
解释一下,ld是linux下的一个链接器,-e选项用来指定程序的入口。编译后可执行文件的大小为618字节(一个动态链接的HelloWorld需要9KB,静态链接将近600K),strip命令可以”剥去”可执行文件中的调试信息,可进一步减小文件的大小,另外在链接时通过其他选项还可以将可执行文件中保存的编译器和系统版本信息也一并去掉……
0x4000a963 <_dl_runtime_resolve+3>: movl 0x10(%esp,1),%edx 0x4000a967 <_dl_runtime_resolve+7>: movl 0xc(%esp,1),%eax 0x4000a96b <_dl_runtime_resolve+11>: call 0x4000a740
指令xchgl %eax,(%esp,1)将printf的地址放入栈顶。最精彩的一条指令当属ret $0×8,它将栈顶元素即printf的地址弹出至程序计数器PC,作为下一条将执行的指令地址,同时,清除堆栈中的0×10和0×8049560。此时堆栈中的情形,就如同直接调用了printf函数,似乎什么都没发生过。
此外
不知道我说清楚了没有,感觉说的很乱,文字也很乱。:-)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | call ___main movl $1,-4(%ebp) ; a movl $2,-8(%ebp) ; b movl -4(%ebp),%eax ;if/else开始 cmpl -8(%ebp),%eax jge L2 movl -4(%ebp),%eax movl %eax,-12(%ebp) jmp L3 .p2align 4,,7 L2: movl -8(%ebp),%eax movl %eax,-12(%ebp) ; if/else结束 L3: movl -8(%ebp),%eax ; ? :开始 cmpl -4(%ebp),%eax jle L4 movl -4(%ebp),%eax L4: movl %eax,-12(%ebp) ; ? :结束 |
if/else用了8条指令,?:用了5条,这个差距可不算小了啊!因为,程序里面是要有循环的,多数循环里面会有分支语句,且很多情况下是二分支。
呃……又学到一个单词,ternary:三元。:-)