弹载计算机程序优化研究

2015-02-10 02:43李洋张振东
航空兵器 2014年5期

李洋+张振东

摘 要:随着嵌入式处理器性能的不断提高,处理器性能已经不是影响弹载计算机系统整体性能的主要因素。系统升级越来越多地注重程序的执行效率、编译器优化能力、程序并行设计等方向。本文从一般性的程序运行切入,分析了影响程序执行效率的因素、编译器优化的局限性,从程序定义、减少函数调用、提高循环效率、减少不必要内存访问等角度介绍了一些提高程序执行效率的程序设计方法。

关键词:弹载计算机;程序优化;编译器;代码移动

中图分类号:TP311 文献标识码:A 文章编号:1673-5048(2014)05-0037-04

0 引 言

在嵌入式领域,实时性一直是衡量系统性能的一项重要指标,它主要取决于系统硬件性能和软件设计两个方面,以往由于硬件核心处理单元性能的限制,致使整个嵌入式系统的运行环境、适用范围、代码体积、系统性能受到了很大的局限。然而随着超大规模集成电路的快速发展,德州仪器、高通、ARM等公司嵌入式处理器产品线的成熟和广泛应用,核心处理单元已经不是影响系统性能的最主要因素,人们开始越来越多地关注程序的执行效率、编译器的优化能力、程序并行设计等问题。

作为嵌入式应用的一个典型代表,弹载计算机也面临着类似问题,美国AIM-120中程空空导弹经历了数次改进,每次除了硬件性能有部分升级外,还包含大量的算法改进、代码优化等软件升级。我国空空导弹目前已经发展到第四代,算法改进、代码优化、并行设计也是在进行软件升级时所采取的有效手段。

1 影响程序运行效率的因素

高效的计算机程序主要取决于两方面的因素:①针对问题选择最适宜的算法和数据结构。一个算法的评价主要从时间复杂度和空间复杂度来考虑,对于弹载计算机宝贵的硬件资源和严格的强实时性要求而言,选择使用的算法必须在保证实时性的前提下尽可能少地占用弹载计算机资源。数据结构则是对算法的支撑,程序涉及的数据都属于某种数据类型,类型明显或隐含地规定了数据的取值范围、存储方式以及允许进行的运算,选择与算法相适宜的数据结构可以带来更高的运行或存储效率;②有足够智能化的编译器,能够充分理解程序源代码的意图,进而把源代码转化为高效的可执行代码,同时能够避免由于优化而带来的诸多问题。然而,就编译器本身而言具有多方面的局限性。

2 编译器的局限性

编译器利用极为复杂的算法来确定程序中的变量是如何参与运算以及被引用的,因此编译器可以尽量简化源程序的计算方法,比如用移位操作代替源程序中的乘法运算。然而,编译器对程序进行优化时仍受到三个方面的限制:①不能改变源程序的计算结果;②编译器的算法通常不能完全理解源程序的思想,以及变量使用的环境;③整个编译过程的时间是可接受的。

2.1 存储器别名使用

voidaliase2(intpt1,intpt2){

pt1+=2pt2;}

两段程序似乎具有相同的功能,它们都是把指针pt2指向变量值的2倍加到指针pt1指向的变量上,但是由于aliase2进行了3次访问内存的操作,而aliase1却访问了6次。因此,编译器在对aliase1进行优化时如果按照aliase2那样生成代码,程序将拥有更高的执行效率。然而,如果当pt1和pt2同时指向同一内存单元时,那么程序aliase1将执行如下操作:

pt1+=pt1;pt1+=pt1;

执行后的结果是pt1指向内存单元的内容是执行前的4倍,而aliase2则进行另一种操作:

pt1+=2pt1;

很显然,执行后的结果是pt1指向内存单元的内容是执行前的3倍。实际中,编译器不会知道aliase1是如何被调用的、两个参数的关系是什么,因此编译器必须假设pt1和pt2是可能相等的,从而它不能按照aliase2那样生成代码。

上述这种现象称为存储器别名使用,编译器必须假设程序中的不同指针在初始化后或在运行过程中是可能指向同一地址单元的,这种假设极大地限制了编译器对程序的优化能力。

2.2 函数调用的副作用

限制编译器优化能力的另一个因素来自于函数调用,先看如下两个函数:

intfun(int);intfun1(x){

returnf(x)+f(x)+f(x)+f(x);}

两个函数看来似乎有相同的计算结果,fun2调用fun一次,而fun1调用fun四次,对于函数的调用,计算机要进行压栈等一系列复杂的操作,因此fun1比fun2具有更大的开销,在运算结果相同的前提下,编译器应该按照fun2去优化代码,但是函数fun如果是如下的结构:

intcounter=0;intfun(intx){

returncounter++;}函数fun就改变了一个全局变量的值,全局变量的值会随着函数fun被调用的次数不断改变。再考虑fun1和fun2,fun1返回的值是0+1+2+3=6,而调用fun2返回的值是40=0。

这种现象称为函数调用的副作用,编译器在进行编译时不会去判断一个函数的调用是否具有副作用,也不会去把原函数试着替换为另一个拥

2.3 编译器设计的权衡

除了上述两种情况,某些编程语言由于自身的特性,往往比其他编程语言难于优化,例如C语言允许指针运算和强制类型转换操作,这些特性会影响编译器对代码的理解和优化。同时,在进行程序优化时,通常还要考虑一些其他因素,比如程序是如何运行,或者在可读性与高效率之间进行权衡等等。

由此看来,由于编译器优化的局限性,一个足够智能的编译器设计起来异常复杂,很可能需要针对目标程序的不同应用而专门设计。编译器在优化目标代码时很可能会改变原有的程序结构和代码次序,因此这种编译器在设计时也需要考虑足够多的条件以避免优化带来的错误,需要耗费人们大量的时间和精力,且不能通用。从理论上来说由于过于复杂,每次编译过程也需要相当长的时间,因此,智能编译器从目前来看是不切实际的想法。

换个角度考虑,由于代码中细小的改动都可能使编译器对代码的优化起到很大的帮助,因此除了依赖编译器对目标程序的优化,程序员如果在写程序时更加注重代码的效率,也可以提高程序执行的效率。

3 高效的程序设计

程序运行时间从理论上讲主要取决于它的输入规模,在实际中还受具体处理器性能、I/O、方法调用等条件的影响,当输入规模一定时,针对后者对程序代码做一些优化,将提升代码的执行效率。

3.1 程序定义

为了说明代码优化对程序执行效率的影响,这里用处理向量数据结构的程序段为例。首先定义结构体:

typedefstruct{

intlength;intdata;

}vec_rec,vec_ptr;

如图1建立一个定长的向量结构,作为程序的输入规模。定义工作函数,作为优化目标函数:voidtest1(vec_ptrv,intdest){

inti;

for(i=0;i

intval;

get_vec_element(v,i,&val);dest=(dest)val;

}}

其中vec_length(v)用来取得向量v的长度,get_vec_element(v,i,&val)用来把向量v中第i个值赋给val,两个函数都用C语言实现,这里省略。

在默认环境下,使用某型处理器,通过编译执行,与使用GCC编译时“-O2”优化选项相较。通过代码分析软件得出优化前后两个程序的CPE(cyclesperelement,每元素周期数)分别是41.86和33.25,很明显,如果执行优化编译,可以提升代码效率。

3.2 消除循环中的低效率

通过分析test1发现,函数vec_length会随着for循环的进行每次迭代都被调用,而向量v的长度是一定的,不会随着每次循环而改变,因此可以在循环前只计算一次向量的长度,并把它赋给一个局部变量length,由此得到改进后的程序test2:

voidtest2(vec_ptrv,intdest){

inti;

intlength=vec_length(v);

for(i=0;i

intval;

get_vec_element(v,i,&val);dest=(dest)val;

}

}

test2的CPE是21.25,这种方法也叫代码移动,对于代码中一些每次计算都得到相同值的部分,可以把它提前到程序的某一行,避免重复计算带来的开销。鉴于之前提到的编译器局限性,在进行代码移动时,编译器不能检测被移动的部分是否具有函数调用的副作用,目前最复杂的编译器依然无法对所有程序做出这种检测,因此在这种情况下只有通过程序员自己的判断来进行优化。这个例子很好地证明了在程序中看似简单的部分通常隐藏着与输入规模成正比的低效率。

3.3 减少函数调用

函数调用时涉及压栈等一系列操作,隐含着通过上述方法,程序的执行效率得到了提高,当然这是在输入规模达到一定程度的情况下得到的结果,特别是对某些计算密集型程序而言。然而,在实际中并不是所有程序都拥有这种模型,这就需要其他的专业知识,比如对所用处理器的特性有深层次的理解,熟练掌握对应的汇编语言,从而对编译器如何生成、执行代码有充分的认识,在进行程序优化时才能更好地去实现最初的预想。

换个角度考虑,由于代码中细小的改动都可能使编译器对代码的优化起到很大的帮助,因此除了依赖编译器对目标程序的优化,程序员如果在写程序时更加注重代码的效率,也可以提高程序执行的效率。

3 高效的程序设计

程序运行时间从理论上讲主要取决于它的输入规模,在实际中还受具体处理器性能、I/O、方法调用等条件的影响,当输入规模一定时,针对后者对程序代码做一些优化,将提升代码的执行效率。

3.1 程序定义

为了说明代码优化对程序执行效率的影响,这里用处理向量数据结构的程序段为例。首先定义结构体:

typedefstruct{

intlength;intdata;

}vec_rec,vec_ptr;

如图1建立一个定长的向量结构,作为程序的输入规模。定义工作函数,作为优化目标函数:voidtest1(vec_ptrv,intdest){

inti;

for(i=0;i

intval;

get_vec_element(v,i,&val);dest=(dest)val;

}}

其中vec_length(v)用来取得向量v的长度,get_vec_element(v,i,&val)用来把向量v中第i个值赋给val,两个函数都用C语言实现,这里省略。

在默认环境下,使用某型处理器,通过编译执行,与使用GCC编译时“-O2”优化选项相较。通过代码分析软件得出优化前后两个程序的CPE(cyclesperelement,每元素周期数)分别是41.86和33.25,很明显,如果执行优化编译,可以提升代码效率。

3.2 消除循环中的低效率

通过分析test1发现,函数vec_length会随着for循环的进行每次迭代都被调用,而向量v的长度是一定的,不会随着每次循环而改变,因此可以在循环前只计算一次向量的长度,并把它赋给一个局部变量length,由此得到改进后的程序test2:

voidtest2(vec_ptrv,intdest){

inti;

intlength=vec_length(v);

for(i=0;i

intval;

get_vec_element(v,i,&val);dest=(dest)val;

}

}

test2的CPE是21.25,这种方法也叫代码移动,对于代码中一些每次计算都得到相同值的部分,可以把它提前到程序的某一行,避免重复计算带来的开销。鉴于之前提到的编译器局限性,在进行代码移动时,编译器不能检测被移动的部分是否具有函数调用的副作用,目前最复杂的编译器依然无法对所有程序做出这种检测,因此在这种情况下只有通过程序员自己的判断来进行优化。这个例子很好地证明了在程序中看似简单的部分通常隐藏着与输入规模成正比的低效率。

3.3 减少函数调用

函数调用时涉及压栈等一系列操作,隐含着通过上述方法,程序的执行效率得到了提高,当然这是在输入规模达到一定程度的情况下得到的结果,特别是对某些计算密集型程序而言。然而,在实际中并不是所有程序都拥有这种模型,这就需要其他的专业知识,比如对所用处理器的特性有深层次的理解,熟练掌握对应的汇编语言,从而对编译器如何生成、执行代码有充分的认识,在进行程序优化时才能更好地去实现最初的预想。

换个角度考虑,由于代码中细小的改动都可能使编译器对代码的优化起到很大的帮助,因此除了依赖编译器对目标程序的优化,程序员如果在写程序时更加注重代码的效率,也可以提高程序执行的效率。

3 高效的程序设计

程序运行时间从理论上讲主要取决于它的输入规模,在实际中还受具体处理器性能、I/O、方法调用等条件的影响,当输入规模一定时,针对后者对程序代码做一些优化,将提升代码的执行效率。

3.1 程序定义

为了说明代码优化对程序执行效率的影响,这里用处理向量数据结构的程序段为例。首先定义结构体:

typedefstruct{

intlength;intdata;

}vec_rec,vec_ptr;

如图1建立一个定长的向量结构,作为程序的输入规模。定义工作函数,作为优化目标函数:voidtest1(vec_ptrv,intdest){

inti;

for(i=0;i

intval;

get_vec_element(v,i,&val);dest=(dest)val;

}}

其中vec_length(v)用来取得向量v的长度,get_vec_element(v,i,&val)用来把向量v中第i个值赋给val,两个函数都用C语言实现,这里省略。

在默认环境下,使用某型处理器,通过编译执行,与使用GCC编译时“-O2”优化选项相较。通过代码分析软件得出优化前后两个程序的CPE(cyclesperelement,每元素周期数)分别是41.86和33.25,很明显,如果执行优化编译,可以提升代码效率。

3.2 消除循环中的低效率

通过分析test1发现,函数vec_length会随着for循环的进行每次迭代都被调用,而向量v的长度是一定的,不会随着每次循环而改变,因此可以在循环前只计算一次向量的长度,并把它赋给一个局部变量length,由此得到改进后的程序test2:

voidtest2(vec_ptrv,intdest){

inti;

intlength=vec_length(v);

for(i=0;i

intval;

get_vec_element(v,i,&val);dest=(dest)val;

}

}

test2的CPE是21.25,这种方法也叫代码移动,对于代码中一些每次计算都得到相同值的部分,可以把它提前到程序的某一行,避免重复计算带来的开销。鉴于之前提到的编译器局限性,在进行代码移动时,编译器不能检测被移动的部分是否具有函数调用的副作用,目前最复杂的编译器依然无法对所有程序做出这种检测,因此在这种情况下只有通过程序员自己的判断来进行优化。这个例子很好地证明了在程序中看似简单的部分通常隐藏着与输入规模成正比的低效率。

3.3 减少函数调用

函数调用时涉及压栈等一系列操作,隐含着通过上述方法,程序的执行效率得到了提高,当然这是在输入规模达到一定程度的情况下得到的结果,特别是对某些计算密集型程序而言。然而,在实际中并不是所有程序都拥有这种模型,这就需要其他的专业知识,比如对所用处理器的特性有深层次的理解,熟练掌握对应的汇编语言,从而对编译器如何生成、执行代码有充分的认识,在进行程序优化时才能更好地去实现最初的预想。