文章目录
1us的误差,足矣改变这个世界
————CSDN根号3
01 - 为什么整数位移比乘除法高效
首先,整数位运算要比乘除法要高效。如果学过计算机组成原理的,相信在初次接触二进制乘除法运算的时候,对补码一位乘法、原码恢复余数法、补码不恢复余数法等都一脸茫然,因为过程实在是太复杂,如果你想不起来,看一看当时老师(可能是你的)课件PPT:
应当问一句:为什么这么复杂?答:因为CPU只会加法!应当说,这个世界只有加法,那些乘除法、开根号、微积分等等,都是加法的深度组合,而CPU现实加法的底层硬件之一就是位移器!相关内容在数字逻辑学中,再看一张课件PPT:
所以,程序中使用位移比使用乘除法要高效,但不是任何时候,因为程序中存在一个2-8定律,对程序性能效率起作用的只有那20%的代码,因此如果去优化那其余的80%代码是徒劳的。这里所说的高效,是在优化那20%代码的基础上
02 - 位移和乘除法对比
现在很多嵌入式编译器很强大,当识别到除数或者乘法因子(一般是常量)是2n倍,会自动编译成位移运算,比如大多数的ARM编辑器,就ARM Gcc 6.4而言,C语言和对应的汇编如下:
重点看asr r3, r3, #3,意思是把r3寄存器右移3位再把值存入r3,右移3位相当于除以23=8,所以编译器内部也会对乘除法进行优化,尽量用位移代替。当除数或者乘法因子是变量或者不是2n倍的时候,就会调用乘除法子程序,或者乘除法指令
2.1 - 汇编代码对比
这里依然使用ARM gcc 6.4编译器,如果是整数变量进行乘除法运算,一般会调用乘除法子程序
int ret = 128;
int value = 8;
/* C语言 */ /* 核心汇编 */
ret /= value; ------------ bl __aeabi_idiv
ret = ret >> 3; ---------- asr r3, r3, #3
2.2 - 编写复杂度对比
有若干种情况:
① 当除数或者乘法因子是2n倍时,乘除法很容易转变为位移运算:二进制左移1位 == 十进制乘以2,二进制右移一位 == 十进制除以2
②当除数或者乘法因子不是2n倍时:
1. 乘法运算也可以转变为位移运算,就是拆分成一个2t + K的数:
10 * 9 = 10 * (8+1) = (10 * 8) + (10 * 1) = 10<<3 + 10
1024 * 127 = 1024 * (128 - 1) = (1024 * 128) - (1024 * 1) = 1024 << 8 - 1024
2. 除法运算不能转换转换,因为除法没有分配率
当表达式很复杂的时候,整个转换过程也随之复杂
2.3 - 速度对比
分别使用MinGw编译器和ARM Gcc 6.4编译器进行对比:
MinGw编译器代码
#include <stdio.h>
#include <windows.h>
#include <time.h>
#define RTY_MAX 10000
int main(void) {
int a = 1024;
int b = 8;
int c,i;
double run_time;
LARGE_INTEGER time_start;
LARGE_INTEGER time_over;
double dqFreq;
LARGE_INTEGER f;
QueryPerformanceFrequency(&f);
dqFreq=(double)f.QuadPart;
QueryPerformanceCounter(&time_start);
for(i = 0; i<RTY_MAX; ++i)
//c = a>>3;
c = a/8;
QueryPerformanceCounter(&time_over);
run_time=1000000*(time_over.QuadPart-time_start.QuadPart)/dqFreq;
printf("\nrun_time:%fus\n",run_time);
return 0;
}
MinGw编译器运行结果
/* c = a>>3 */ /* c = a/8 */
run_time:25.659256us run_time:31.724171us
ARM Gcc 6.4编译器代码(裸机编程,与实际硬件有关)
#define RTY_MAX 10000
extern uint32_t timer_count;
void cal(void)
{
uint8_t a = 128;
uint8_t b = 8;
uint8_t c,i;
timer_start();
for(i = 0; i<RTY_MAX; ++i)
c = a/b;
//c = a>>3;
timer_stop();
xprintf("%lu tick\r\n",timer_count);
}
ARM Gcc 6.4编译器运行结果
/* c = a>>3 */ /* c = a/8 */
1254876 tick 3865468 tick
可以看到,乘除法的时间要比位移运算长,并且随着运算次数越多,他们时间差越大,在影响系统的20%代码内,1us的差距都将发生不可逆转的灾害
03 - 实际应用
3.1 - 单片机时钟重载值
大家都写过:
TH0 = (65536 - 1000) / 256; TL0 = (65536 - 1000) % 256
一般而言,单片机时钟会发生ms级中断,如果编译器比较强大,那么上述公式会自动转变为位移运算,如果编译器不支持,那么每次计算重载值都会花费一点时间,随着不断的积累,时间差就会很明显,如果这段代码存在那20%代码之内,为了那若干us的误差补偿,正确的写法应当是:
TH0 = (max_65536 - value_1000) >> 8 ; TL0 = (max_65536 - value_1000) & 0x00FF ;
当然,更好的方法是先计算好,得出一个常量,直接赋值,但是这样难以维护,要适当取舍。在其余代码中也经常看见位移运算代替乘除法,如果仔细阅读过STM32的HAL库函数源代码,你会发现位移运算随处可见
04 - 总结
- 整数位移运算比乘除法高效
- 优化代码要优化2-8定律中的2
- 整数乘法任何时候都可以转换成位移运算
- 整数除法、求余运算的除数如果是2n倍时,就可以转换成位移运算