【嵌入式底层知识修炼】整数乘除法与位运算的效率对比


1us的误差,足矣改变这个世界

————CSDN根号3


01 - 为什么整数位移比乘除法高效

  首先,整数位运算要比乘除法要高效。如果学过计算机组成原理的,相信在初次接触二进制乘除法运算的时候,对补码一位乘法、原码恢复余数法、补码不恢复余数法等都一脸茫然,因为过程实在是太复杂,如果你想不起来,看一看当时老师(可能是你的)课件PPT:
Alt

  应当问一句:为什么这么复杂?答:因为CPU只会加法!应当说,这个世界只有加法,那些乘除法开根号微积分等等,都是加法的深度组合,而CPU现实加法的底层硬件之一就是位移器!相关内容在数字逻辑学中,再看一张课件PPT:
Alt

  所以,程序中使用位移比使用乘除法要高效,但不是任何时候,因为程序中存在一个2-8定律对程序性能效率起作用的只有那20%的代码,因此如果去优化那其余的80%代码是徒劳的。这里所说的高效,是在优化那20%代码的基础上

02 - 位移和乘除法对比

  现在很多嵌入式编译器很强大,当识别到除数或者乘法因子(一般是常量)是2n倍,会自动编译成位移运算,比如大多数的ARM编辑器,就ARM Gcc 6.4而言,C语言和对应的汇编如下:
Alt
  重点看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倍时,就可以转换成位移运算

猜你喜欢

转载自blog.csdn.net/Hxj_CSDN/article/details/89389247