点击上方“大鱼机器人”,选择“置顶/星标公众号”
福利干货,第一时间送达!
译文链接:http://www.codeceo.com/article/c-high-performance-coding.html
英文原文:https://www.codeproject.com/Articles/6154/Writing-Efficient-C-and-C-Code-Optimization)
翻译作者:码农网– gunner
在本篇文章中,我收集了很多经验和方法。应用这些经验和方法,可以帮助我们从执行速度和内存使用等方面来优化C语言代码。
简介在最近的一个项目中,我们需要开发一个运行在移动设备上但不保证图像高质量的轻量级JPEG库。期间,我总结了一些让程序运行更快的方法。
在本篇文章中,我收集了一些经验和方法。应用这些经验和方法,可以帮助我们从执行速度和内存使用等方面来优化C语言代码。
尽管在C代码优化方面有很多的指南,但是关于编译和你使用的编程机器方面的优化知识却很少。
通常,为了让你的程序运行的更快,程序的代码量可能需要增加。代码量的增加又可能会对程序的复杂度和可读性带来不利的影响。
这对于在手机、PDA等对于内存使用有很多限制的小型设备上编写程序时是不被允许的。因此,在代码优化时,我们的座右铭应该是确保内存使用和执行速度两方面都得到优化。
声明实际上,在我的项目中,我使用了很多优化ARM编程的方法(该项目是基于ARM平台的),也使用了很多互联网上面的方法。但并不是所有文章提到的方法都能起到很好的作用。
所以,我对有用的和高效的方法进行了总结收集。同时,我还修改了其中的一些方法,使他们适用于所有的编程环境,而不是局限于ARM环境。
哪里需要使用这些方法?没有这一点,所有的讨论都无从谈起。程序优化最重要的就是找出待优化的地方,也就是找出程序的哪些部分或者哪些模块运行缓慢亦或消耗大量的内存。只有程序的各部分经过了优化,程序才能执行的更快。
程序中运行最多的部分,特别是那些被程序内部循环重复调用的方法最该被优化。
对于一个有经验的码农,发现程序中最需要被优化的部分往往很简单。此外,还有很多工具可以帮助我们找出需要优化的部分。我使用过Visual C++内置的性能工具profiler来找出程序中消耗最多内存的地方。
另一个我使用过的工具是英特尔的Vtune,它也能很好的检测出程序中运行最慢的部分。根据我的经验,内部或嵌套循环,调用第三方库的方法通常是导致程序运行缓慢的最主要的起因。
整形数如果我们确定整数非负,就应该使用unsigned int而不是int。有些处理器处理无符号unsigned 整形数的效率远远高于有符号signed整形数(这是一种很好的做法,也有利于代码具体类型的自解释)。
因此,在一个紧密循环中,声明一个int整形变量的最好方法是:
register unsigned int variable_name;
记住,整形in的运算速度高浮点型float,并且可以被处理器直接完成运算,而不需要借助于FPU(浮点运算单元)或者浮点型运算库。
尽管这不保证编译器一定会使用到寄存器存储变量,也不能保证处理器处理能更高效处理unsigned整型,但这对于所有的编译器是通用的。
例如在一个计算包中,如果需要结果精确到小数点后两位,我们可以将其乘以100,然后尽可能晚的把它转换为浮点型数字。
除法和取余数在标准处理器中,对于分子和分母,一个32位的除法需要使用20至140次循环操作。除法函数消耗的时间包括一个常量时间加上每一位除法消耗的时间。
Time (numerator / denominator) = C0 + C1* log2 (numerator / denominator)
= C0 + C1 * (log2 (numerator) - log2 (denominator)).
对于ARM处理器,这个版本需要20+4.3N次循环。这是一个消耗很大的操作,应该尽可能的避免执行。有时,可以通过乘法表达式来替代除法。
例如,假如我们知道b是正数并且bc是个整数,那么(a/b)>c可以改写为a>(cb)。如果确定操作数是无符号unsigned的,使用无符号unsigned除法更好一些,因为它比有符号signed除法效率高。
合并除法和取余数在一些场景中,同时需要除法(x/y)和取余数(x%y)操作。这种情况下,编译器可以通过调用一次除法操作返回除法的结果和余数。如果既需要除法的结果又需要余数,我们可以将它们写在一起,如下所示:
int func_div_and_mod (int a, int b)
{
return (a / b) + (a % b);
}
通过2的幂次进行除法和取余数
如果除法中的除数是2的幂次,我们可以更好的优化除法。编译器使用移位操作来执行除法。因此,我们需要尽可能的设置除数为2的幂次(例如64而不是66)。并且依然记住,无符号unsigned整数除法执行效率高于有符号signed整形出发。
typedef unsigned int uint;
uint div32u (uint a)
{
return a / 32;
}
int div32s (int a)
{
return a / 32;
}
上面两种除法都避免直接调用除法函数,并且无符号unsigned的除法使用更少的计算机指令。由于需要移位到0和负数,有符号signed的除法需要更多的时间执行。
取模的一种替代方法我们使用取余数操作符来提供算数取模。但有时可以结合使用if语句进行取模操作。考虑如下两个例子:
uint modulo_func1 (uint count)
{
return (++count % 60);
}
uint modulo_func2 (uint count)
{
if (++count >= 60)
count = 0;
return (count);
}
优先使用if语句,而不是取余数运算符,因为if语句的执行速度更快。这里注意新版本函数只有在我们知道输入的count结余0至59时在能正确的工作。
使用数组下标如果你想给一个变量设置一个代表某种意思的字符值,你可能会这样做:
switch ( queue )
{
case 0 : letter = 'W';
break;
case 1 : letter = 'S';
break;
case 2 : letter = 'U';
break;
}
或者这样做:
if ( queue == 0 )
letter = 'W';
else if ( queue == 1 )
letter = 'S';
else letter = 'U';
一种更简洁、更快的方法是使用数组下标获取字符数组的值。如下:
static char *classes="WSU";
letter = classes[queue];
全局变量
全局变量绝不会位于寄存器中。使用指针或者函数调用,可以直接修改全局变量的值。因此,编译器不能将全局变量的值缓存在寄存器中,但这在使用全局变量时便需要额外的(常常是不必要的)读取和存储。所以,在重要的循环中我们不建议使用全局变量。
如果函数过多的使用全局变量,比较好的做法是拷贝全局变量的值到局部变量,这样它才可以存放在寄存器。这种方法仅仅适用于全局变量不会被我们调用的任意函数使用。例子如下:
int f(void);
int g(void);
int errs;
void test1(void)
{
errs += f();
errs += g();
}
void test2(void)
{
int localerrs = errs;
localerrs += f();
localerrs += g();
errs = localerrs;
}
注意,test1必须在每次增加操作时加载并存储全局变量errs的值,而test2存储localerrs于寄存器并且只需要一个计算机指令。
使用别名考虑如下的例子:
void func1( int *data )
{
int i;
for(i=0; ix = 0;
p->pos->y = 0;
p->pos->z = 0;
}
然而,这种的代码在每次操作时必须重复调用p->pos,因为编译器不知道p->pos->x与p->pos是相同的。一种更好的方法是缓存p->pos到一个局部变量:
void InitPos2(Object *p)
{
Point3 *pos = p->pos;
pos->x = 0;
pos->y = 0;
pos->z = 0;
}
另一种方法是在Object结构中直接包含Point3类型的数据,这能完全消除对Point3使用指针操作。
条件执行条件执行语句大多在if语句中使用,也在使用关系运算符(等)或者布尔值表达式(&&,!等)计算复杂表达式时使用。对于包含函数调用的代码片段,由于函数返回值会被销毁,因此条件执行是无效的。
因此,保持if和else语句尽可能简单是十分有益处的,因为这样编译器可以集中处理它们。关系表达式应该写在一起。
下面的例子展示编译器如何使用条件执行:
int g(int a, int b, int c, int d)
{
if (a > 0 && b > 0 && c xmin && p.x xmax &&
p.y >= r->ymin && p.y ymax);
}
这里有一种更快的方法:x>min && xymin) ymax); } 布尔表达式和零值比较
处理器的标志位在比较指令操作后被设置。标志位同样可以被诸如MOV、ADD、AND、MUL等基本算术和裸机指令改写。如果数据指令设置了标志位,N和Z标志位也将与结果与0比较一样进行设置。N标志表示结果是否是负值,Z标志表示结果是否是0。
C语言中,处理器中的N和Z标志位与下面的指令联系在一起:有符号关系运算x=0,x==0,x!=0;无符号关系运算x==0,x!=0(或者x>0)。
C代码中每次关系运算符的调用,编译器都会发出一个比较指令。如果操作符是上面提到的,编译器便会优化掉比较指令。例如:
int aFunction(int x, int y)
{
if (x + y
关注
打赏
最近更新
- 深拷贝和浅拷贝的区别(重点)
- 【Vue】走进Vue框架世界
- 【云服务器】项目部署—搭建网站—vue电商后台管理系统
- 【React介绍】 一文带你深入React
- 【React】React组件实例的三大属性之state,props,refs(你学废了吗)
- 【脚手架VueCLI】从零开始,创建一个VUE项目
- 【React】深入理解React组件生命周期----图文详解(含代码)
- 【React】DOM的Diffing算法是什么?以及DOM中key的作用----经典面试题
- 【React】1_使用React脚手架创建项目步骤--------详解(含项目结构说明)
- 【React】2_如何使用react脚手架写一个简单的页面?


微信扫码登录