程序优化

来源:互联网 发布:linux c 网络库 编辑:程序博客网 时间:2024/06/05 02:43

一、 序言

程序优化是指利用软件开发工具对程序进行调整和改进,让程序充分利用资源,提高运行效率,缩减代码尺寸的过程。按照优化的侧重点不同,程序优化可分为运行速度优化和代码尺寸优化。

运行速度优化是指在充分掌握软硬件特性的基础上,通过应用程序结构调整等手段来降低完成指定任务所需执行的指令数。在同一个处理器上,经过速度优化的程序比未经优化的程序在完成指定任务时所需的时间更短,即前者比后者具有更高的运行效率。代码尺寸优化是指,采取措施使应用程序在能够正确完成所需功能的前提下,尽可能减少程序的代码量。

然而,在实际的程序设计过程中,程序优化的两个目标(运行速度和代码大小)通常是互相矛盾的。为了提高程序运行效率,往往要以牺牲存储空间、增加代码量为代价,例如程序设计中经常使用的以查表代替计算、循环展开等方法就容易导致程序代码量增加。而为了减少程序代码量、压缩存储器空间,可能又要以降低程序运行效率为代价。因此,在对程序实施优化之前, 应先根据实际需求确定相应的策略。在处理器资源紧张的情况下,应着重考虑运行速度优化;而在存储器资源使用受限的情况下,则应优先考虑代码尺寸的优化。在下面的讨论中,主要以速度优化为主,同时对空间进行一些讨论。

二、 优化原则

在决定优化前,需要问自己的几个问题:为什么要优化、哪些程序需要优化、优化的目标是什么、能够接受由此带来的可能的资源消耗(人力、维护、空间等)吗?

1. 如果说程序足够快,足以满足应用的需要,那还有优化的必要性吗?我认为没有必要。因为优化是一种时间与空间及代码可读性的权衡,可能带有一定的负作用,比如增加维护成本等。优化的目的是在现有程序不能满足应用的要求情况下,更加充分合理的利用现有资源去改进程序的运行效率。

2. 确定程序过程中最花费时间的地方,决定哪些程序需要优化。在进行任何优化之前,必须找到程序的瓶颈所在。按80对20的一般性原则,在程序运行过程中,80%的时间消耗在20%的程序代码中。更有甚者,比如在很多图像处理中,95%的时间消耗在内层循环中的图像数据运算;而在另一些程序中,95%的时间消耗在读写数据文件上,而在数据的处理时间却少于5%。所以,在优化过程中,首先照顾那些最常用且最消耗时间的程序,才能起到显而易见的效果。优化不常用的代码不但浪费时间,而且使得代码不清晰,难于调试。

3. 设计优化目标。程序优化是一门平衡的艺术,速度的优化往往要以牺牲程序的可读性或者增加代码长度及空间为代价,所以一定要设计合理的优化目标,将程序优化带来的资源消耗控制在合理的范围内。

在清楚了以上问题之后,可以考虑从以下三个层级进行程序优化,尝试从不同的层级思考优化方案,它们依次产生更显著的优化代码:

1. 算法优化

程序的目的是通过计算机语言来解决现实存在的问题。在程序设计过程中,首先面对现实问题,然后提出解决问题的办法,设计相应的数据结构和算法。所以,在设计时要仔细考虑是选择链表还是数组,采用线性查找还是二分查找。尝试打破一些传统规则,发掘和怀疑自己的某些假定,恢复问题的本来面目,选择和构造适合于问题的算法。

2. C/C++优化

C/C++语言优化主要根据其运行机制,使用合理的实现方式,避免不必要的时间消耗。此层次不触及算法,面向的是程序代码,而不是问题本身,所以是以一种局部的思维方式去优化程序。改变变量存储位置、参数传递优化、对像构造方式、除法优化等都属于这一级,这个级别的优化需要对C/C++的运行机制有很好理解以及掌握大量小的优化技巧和知识,需要不断的学习和积累。

3. 汇编优化

汇编语言最接近机器语言,每条汇编指令对应一条机器指令。在汇编语言中可以直接操作寄存器,调整指令执行顺序。由于汇编语言直接面对硬件平台,而不同的硬件平台的指令集及指令周期均有较大差异。所以针对不同平台,可以选择寄存器优化、指令调整、循环展开等优化方法。

在上面三个层次中,有时候层次之间的界限并不明显,在哪一个层次产生更显著的优化效果,需要依据具体的问题来分析。但有一点是确定的,在优化时一定要按上面的顺序来依次考虑问题,首先考虑在算法层面进行优化,如果在算法层次没有优化的空间,再考虑在C/C++层次的优化,最后才是依据硬件平台进行汇编优化。

最后,给出优化过程中必须遵循以下原则:

1. 程序必须正确,然后才有优化;

2. 优化是有方向和侧重点的,不只是单纯的速度优化,它是时间与空间的平衡;

3. 优化算法设计优先。充分优化的笨拙算法实现始终比不上一个更好算法的普通实现;

4. 优化不只是实践,它是一种理论与实践的结合。优化需要掌握很多软件和硬件平台优化的技巧,但是更重的是C/C++运行机制的理解及对数据结构和算法的把握;

5. 代码的优化是永无止境的,要避免过犹不及;

6. 程序优化不是在软件编程结束后才进行,而是融入在程序编写过程中;

7. 如果确信程序不需要优化,那就根本不进行优化;

8. 清晰简洁的代码,很多时候就是最好的优化;

三、 算法优化

优化的第一步是构造适合问题的最佳算法。在程序设计过程,我们常常使用经典算法和传统模式去解决问题,一般情况下都保证程序的正确性。而很多经典算法都对问题作了一些假设,在面对实际问题时,为了获得更高的程序效率,我们需要重新检视这些假设,并尝试不同的角度思考问题,寻求有针对性于问题的新算法。

算法设计中应该熟悉算法,知道各种算法的优缺点,针对不同的问题选择不同的算法。在某些情况下,需要对多种不同的算法进行测试,从中找到最适合问题的算法。将比较慢的顺序查找法用较快的二分查找法代替,插入排序或冒泡排序法用快速排序、堆排序代替,都可以大大提高程序执行的效率。选择一种合适的数据结构也很重要,比如你在一堆随机存放的数中使用了大量的插入和删除操作,那使用链表要快得多。如果需要较多随机存取表中元素,则最好采用线性表作为数据存储结构。

另外,不要对一个简单的问题,使用复杂的算法。比如说,如果使用二分查找,甚至线性查找也是足够快,就没有必要使用Hash表来处理小数据量表,即“杀鸡焉用牛刀”。

四、 C/C++优化

此层次优化是以局部的思维方式看程序,不触及算法层级,它面向的是代码,而不是问题。语句调整,循环优化、参数传递优化等都属于这一级,这个级别的优化需要对C/C++的运行机制有很好理解以及掌握大量小的优化技巧和知识,需要不断的积累。与此层次对应的是编译器,即当前C语言将编译成哪些汇编指令。如今,多数编译器都支持对程序速度和程序大小的优化,有些编译器还允许用户选择可供优化的内容及优化的程度。简单的语句调整、公共表达式提取、废代码删除等,当前的很多编译器也能做到了。然而,大部分编译器必须是保守的,除非用户指定了采用特定的优化方式,否则编译器只能依据最可靠的方法将C/C++语言,翻译机器语言,所以需要了解一些编译器的优化能力,以使自己的代码配合编译器做好优化。

以下文中,所有生成汇编代码,均是由GUN ARM编译器生成,优化级别为O3(生成汇编方法,可以参看附录)。

1. C/C++变量优化

C/C++语言中变量依据其定义的方式不同,其存放位置可以分为寄存器、栈区、堆区和静态存储区三种区域。

(1)寄存器上分配。当函数中定义的局部变量不多,且没有对局部变量的取地址操作时,则会将该变量会分配在寄存器中。当进行运算时,直接读寄存器,速度非常快。

(2)在栈上分配。用户在函数体中定义了较多局部变量后,或对变量进行取地址操作,通过结构体返回值,则相应的变量会放在栈空间。函数执行结束后,这些存储单元自动被释放。一般的情况,由于栈区中数据在函数中都会被重复用到,加载时都能够Cache命中,一个周期内完成,效率很高,但是其分配的内存容量有限。

(3)从静态存储区域分配。在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量、static变量。在ARM9平台,从静态区加载数据到寄存器,一般需要3个周期。如果在循环中,无序访问数据造成Cache不能命中,那么每次都需要3个周期加载,则比较费时。所以尽量让数据顺序访问,提高Cache命中率和访问速度。

(4)从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请的内存,程序员自己负责用free或delete释放内存,其访问速度和静态区相同。

clip_image001

建议1:把重复使用的指针型参数拷贝到本地局部变量。

参看下面的代码:

clip_image002

比较左边的C代码,差别在于将step指向的值赋给局部变量temp。程序员一般会认为,A框中列出的 C代码,节约了一个整形变量的存储空间,而且直接用指针进行运算,少了一条赋值语句,速度应该更快。其实不然,将两部分C代码汇编后,再比较右边汇编指令,并没有节约存储空间(原因是变量分配在寄存器中),而且b中汇编代码到在计算每二个表达式时,少用了一条LDR指令。为什么呢?因为编译器不能断言step指针,是否和t1指向同一个地址,即在计算第一个表达式后,其指向值是否发生了改变。为了保证程序正确性,在第二次引用时,只能再次从内存加载step,增加了一个LDR指令周期数。对于这种指针型参数,编译器不能做到优化,如果step处于循环中,情况则更加糟糕。所以只能由程序员自己控制,配合编译器做好优化工作。

建议2:尽量选用32位的数据类型变量。

ARM 指令集支持有符号/无符号的8 位、16 位、32位整型变量。恰当的使用变量类型,不仅可以节省目标代码指令,并且可以提高代码运行效率。在程序编写过程中,应该尽可能地避免使用char、short 型的局部变量,因为操作8位/16位局部变量往往比操作32位变量需要更多指令, 请对比下列函数和它们的汇编代码。

clip_image003

首先比较左边的C代码,唯一的差别是计数器变量i的定义不同。一般认为,采用short类型变量比int节约了两个字节。其实并没有,因为ARM是32位运算(除非处于Thumb模式),寄存器也是32位,并不存在节约了两个字节。在i定义为32整型时,在对应的汇编代码中可以说编译器已经做到极致优化,其将循环结束条件都进行了优化。而在计数器定义为16位整形时,编译器则不但没有优化循环条件,而且还将计数器每次加1之后,利用移位操作对其进行宽度调整,将32位整形其转化为16位整型,增加了两条MOV指令。所以,在变量定义中,尽量使用32位宽度的数据类型,小于32位宽度的变量不但没有节约内存,还增加了很多无用的指令操作。在定义局部变量时,选用32位的数据有利于编译器优化。

建议3:尽量在需要使用一个对象时才声明,并用初始化的方法赋初值。

只要你定义了一个变量而且其类型带有一个构造函数或析构函数,那么当程序运行到这个变量定义式时,你就得承担构造成本;当这个变量离开作用域时,你得承担析构成本。即使这个变量最终没有被使用,仍需要耗费这些成本,所以要尽量避免这种情形。

看如下面代码,定义一个CString类,其中有构造函数,析构函数及拷贝构造和赋值函数和一个获得字符串长度的函数。

clip_image004

下面利用CString定义如下两段基本相同的代码,只是局部变量str定义的位置不同。

clip_image005

对两种情况,进行一次调用函数对比(除去GetLength操作),如下。

代码A:pStr->GetLength()==0时,1个构造函数 + 1个析构函数;

pStr->GetLength()!=0时,1个构造函数 + 1个析构函数 + 1个赋值函数;

代码B:pStr->GetLength()==0时,没有函数调用;

pStr->GetLength()!=0时,1个拷贝构造函数 + 1个析构函数;

可以看出对于A代码,无论什么情况,至少有1次构造和析构函数调用。最好情况,也比B情况下,多一次赋值函数调用。所以尽可能的滞后局部变量定义,最好延期到能够给它赋初值为止,可以提高程序速度。

下面再看看循环体中局部变量定义,请看如下代码:

clip_image006

对循环体两种情况,进行一次调用函数对比,如下。

代码A: 1个构造函数 + 1个析构函数 + n个赋值函数;

代码B: n个拷贝构造函数 + n个析构函数;

对于这两种情况就,需要考虑每个函数调用花费是多大了,然后再决定采用哪种写法。

对于基本数据类型…(未完)

2. 参数传递优化

ARM在函数调用时,如果参数少于四个,则通过R0-R3传递参数。如果多于四个参数,则会将参数从右向左的顺序入栈。所以,在函数设计时,尽量限制函数的参数,不要超过4个,这样可以省去参数入栈操作,提高函数调用效率。

另外,在函数调用过程中,如果通过传值的形式传递结构体参数和返回值时,则可能造成拷贝构造函数的调用。对于此类传递,尽可能选用指针或引用传递,不会增加额外的函数调用负担。看下面的函数中参数的传递方式。

C.常量传递

A.传值方式

void fun_2(CString *pStr)

{

...

return;

}

B.指针传递

void fun_1(CString pStr)

{

...

return;

}

clip_image007

void fun_3(const CString *pStr)

{

...

return;

}

函数func_1以传值的方式传递参数,必然会调用CString的拷贝构造函数和析构函数,函数运行过程中不会改变调用者的内容。

函数func_2以指针的方式传递参数,不会调用CString的拷贝构造函数和析构函数,但函数运行过程中,可以对调用者参数所指向的内容进行改变,会造成不安全性。

为了保证调用者的值不会被改变,同时也不调用拷贝函数,那么就可以func_3方式传递参数,在类型前加上const关键字。

3. 合理使用内联函数

在编译内联函数时,编译器首先对参数和返回值进行检查,确认正确后,内联函数的代码就会直接替换函数调用。这样,就可以省去函数调用的开销,提高函数的执行效率。但是,每一内联函数的调用处都会有其一份拷贝,无疑增大了代码体积,程序加载后消耗更多的内存空间。所以,内联函数是典型的“以空间换时间”的优化策略。如果函数体内代码的执行时间远大于函数调用开销,那么内联的意义就不大了。

所以,使用内联函数时注意以下事项:

(1)将大多数内联函数限制在小型、被频繁调用的函数身上。

(2)内联会导致目标代码体积变大,在空间有限的情况下,不宜使用内联函数。

现在的编译器,都会拒绝将过于复杂的函数内联,自动地取消不值得内联的函数。

4. 分支优化

条件分支(if语句、switch语句)是编程中经常使用的基本操作,然而在某些时候(如循环)它可能带来严重的性能问题。在ARM上,分支是通过跳转指令B来实现。在ARM7和ARM9中,跳转指令B需要3个指令周期数,是占用指令周期数比较长的一条指令(原因是会清空流水线)。所以,如果在循环中大量使用条件分支,则严重影响程序效率。在XScale中,ARM处理器能对条件分支做出预测。如果分支预测正确,那么跳转指令B只需要花费1个指令周期,而如果预测错误,那么需要增加4个指令周期。这就是分支预测错误的惩罚。

下面将讨论条件分支的一些有效优化方法。

1.把条件分支移动到循环外

先看如下代码,

clip_image008

对于A代码中奇偶模式的随机分支,即使当前最好的CPU也不可能做出好的预测,对于这种情况就要改写成两个for循环,一个处理偶数,一个处理奇数,如代码B。

2.单独处理条件分支。

图像处理算法,经常需要判断边界像素点,进行特殊处理;可以考虑的优化方案是把边界区域和内部区域分开处理;或者条件允许的话,扩大原图像的边界,形成"哨兵"数据,这样访问像素的时候就不用考虑越界的问题了。

3.合并多个条件来减少条件分支

编写代码过程中,常常使用 if((pStr1==0)&&( pStr2==0)&&( pStr3==0)),编译器将生成3个条件跳转指令,而且使分支可预测性大大降低,可以改写为: if((pStr1|pStr2|pStr3)==0) 从而同时改进代码和分支预测准确率。经测试,在GUN ARM编译器的O1优化级别上,能够对于上述语句自动优化。对下面的语句,编译器不能做到优化(因为编译器不能断定b0,b1,b2>=0),会产生三个比较指令和三个跳转指令,这种情形就需要程序员自己优化。

if((b0>=64)||(b1>=64)||(b2>=64))  //b0,b1,b2>=0

改写为:if((b0|b1|b2)>=64)

5. 循环优化

在ARM中的循环结构中,循环的终止条件将影响其执行效率,因为ARM指令有条件执行特性,所以在书写循环时尽量以0为终止条件。这样编译器可以用一条BNE(若非零则跳转)指令代替CMP (比较)和BLE (若小于则跳转)两条指令,既减小代码尺寸,又加快了运行速度。

比较下面代码,其中R0=i,以10为终止条件比以0为终止条件的循环多了一条比较指令。

clip_image009

建议4:在循环书写过程中,如果循环体内代码与计数器i的增减顺序无关,尽量采用0作为循环终止条件。

另外,一些不需要在循环中参加运算的任务放到循环外面,包括表达式、函数的调用、指针运算、数组访问等。

6. 乘/除法优化

在ARM中没有除法指令,所有的除法操作都是调用C库函数实现除法运算,模运算(%)也是通过除法运算来实现,一般要花费20~100个周期,是最慢的基本算术运算。

建议5:在编程过程中,尽量避免除法。

一种可能减少整数除法的地方是连除,由乘法代替。这个替换的副作用是有可能在乘积时会溢出,所以只能在一定范围的除法中使用。

有些除法可用乘法代替,例如: if ( (x / y) > z)可变通为 if ( x > (y * z)) 。

在能满足精度,且存储器空间冗余的情况下,也可考虑使用查表法代替除法。

当除数为2的幂次方时,用移位操作代替除法。现在大部分编译器都能做到移位优化。如下所示:

a=a/8,(当a>0)改为:a=a>>3,(当a<0)改为:a=(a+1)>>3;

a=a%8,(当a>0)改为:a=a&7;

另外,乘以任何一个整数都可以用移位和加法来代替乘法。ARM中加法和移位可以通过一条指令来完成,且执行时间少于乘法指令。例如: i = i * 5(3个指令周期) 可以用i = (i<<2) + i (1个指令周期)来代替。

7. 浮点优化

大多数ARM处理器硬件并不支持浮点运算。这就意味着编译器要把每一个浮点运算转换成一个子程序调用,用整型运算来模拟浮点运算。在使用浮点时,可以对其进行转化整形运算,将提高运行速度。如,图像缩放过程中,需要计算原图像和目标图像的比值,那么就有小数位,如果采用浮点数运算,则会影响程序速度。那么可以用移位操作放大被除数,将浮点数转化为整数来表示,看下面代码。

clip_image010

对于上面的将浮点转化为整型运算后,一般的情况下,程序的运行速度要快2~3倍。在转化过程中,要注意防止移位溢出。比如示例4-7的代码中,要求原图像的宽度和高度都必须小于65536,否则左移16位会造成溢出。

五、 汇编优化

汇编优化相对应的是具体硬件平台。用汇编重写并不是简单把高级语言改写为汇编实现,那样的汇编很可能没有当今编译器产生的代码好,所以如果决定用汇编实现,那就应该按照汇编的角度来规划自己的实现,适当的参考编译器生成的汇编码是可取的(特别是新手)。另外,在某些领域,使用处理器的新特性和新的指令集(如MMX) 等,将产生巨大的性能收益,这些地方经常采用汇编来实现。

1. 流水线

为了加快CPU的处理频率,现代CPU都设计了多级流水线,ARM处理器也不例外。ARM7采用3级流水线,ARM9采用5级流水线。下面以ARM9简单说明。

clip_image011

l 取指 在PC地址处取出指令。指令被加载到内核,然后进入流水线。

l 译码 对前一个周期中取到的指令进行译码。

l 运算 执行译码操作所得指令。

l 加载1 通过LDR指令从内存加载特定数据。如果不是加载指令,那么这个步骤无效。

l 加载2 通过LDRB、LDRH加载字节或半字数据。如果不是加载指令,那么这个步骤无效。

2. 流水线阻塞

如果在运算中一条指令需要前一条指令的执行结果,而这时结果还没有出来,那么处理器就会等待,这称为流水线数据相关。

如果在运算中,一条指令运算需要前一条指令的运算部件,而前一条指令运算没有结束,那么处理器会等待,这称为流水线部件相关。典型是乘法部件。

clip_image012

在上面的汇编代码A中,LDR指令需要2个指令周期,其后紧跟的ADD指令需要使用LDR的结果,这时数据还未加载成功,相应产生了数据相关。那么,处理器只能等待加载成功,然后再进行ADD指令,流水线产生了1个指令周期的间隙。

在汇编代码B中,第一条MUL指令在运算中占用了乘法部件,需要2个指令周期才能结束运算。其后的第二条指条也需要使用乘法部件,造成了部件相关,处理器等待第一个乘法结束后再进行第二个乘法,也产生了1个指令周期的间隙。

3. 流水线优化

由前面叙述可知,ARM指令执行是在指令流水线中进行,一条指令执行的时间会受其相邻指令的影响。流水线延迟或阻断会对处理器的性能造成影响,因此应该尽量保持流水线畅通。优化过程中可以通过调整代码中的指令序列,在指令间隙之间插入不存在相关性的指令,以使流水线满负荷运转。另外,跳转指令B将清空流水线重新加载指令,也严重影响流水线畅通。所以,要尽可能避免。对上面产生相关的指令序列进行调整如下。将序列中不相关的CMP指令插入到相关指令之间,填满流水线,去掉了一个指令周期的间隙,提高了流水线的吞吐率。

clip_image013

4. 寄存器分配优化

CPU 对寄存器的存取要比对内存的存取快得多, 因此为变量分配一个寄存器,将有助于代码的优化和运行效率的提高。整型、指针等类型的变量都可以分配寄存器;一个结构的部分或者全部也可以分配寄存器。给循环体中需要频繁访问的变量分配寄存器也能在一定程度上提高程序效率。寄存器优化的基本原则是最大限度的使用寄存器,最少次数访问存储器。

5. 合理使用条件指令

ARM指令集的一个重要特征就是大多数的指令均可包含一个可选的条件码。当程序状态寄存器CPSR中的条件标志满足指令条件时,带条件码的指令才会执行。默认情况下,ARM指令并不会更新ARM寄存器CPSR中的N,Z,C,V标志。对大多数的指令,若要更新这些标志,则需要对指令助记符加后缀S。例外的是不写入目标寄存器的CMP指令,直接更新CPSR的标志位,因此不需要S后缀。利用条件执行通常可以省去单独的判断指令,因而可以减小代码尺寸并提高程序效率。

例1:

下面以一个if分支判断为例说明,

clip_image014

R0 = a, R1 = b使用条件指令的代码,相对未用条件代码,少了一条CMP指令。

例2:

clip_image015

其中:R1 = a, R2 = x, R3 = y。

在未使用条件指令的情况下,如果分支L1,则需要5个周期数,如果分支L2,则需要8个周期数。如果分支的情况各占50%,则未用条件指令平均需要(5*0.5+8*0.5)=6.5周期数。而使用条件指令,则恒定需要3个周期数。平均少用了3.5个周期。

注意:

对于示例5-5中,如果分支中所需要较多的操作,则不适合选用条件指令。因为,条件指令无论是否执行,都至少占用一个周期。

6. 循环展开:

流水线阻断的情况可通过循环拆解等方法加以改善。一个循环可以考虑拆解以减小跳转指令在循环过程中所占的比重,进而提高代码效率。以while循环为例生成的汇编指令

clip_image016

其中,R0 = i。

这个循环开销包括一条设置条件标志的减法指令SUBS和跳转指令BGT.在ARM9平台上,减法指令SUBS占用1个周期数,跳转指令B占用3个周期数,也就是说循环一次的最小成本为4个指令周期数。如果在do something所占用4个指令周期,则有效指令周期数和逻辑控制指令周期数各占用50%。如果循环体内代码,一次处理四个对像(如3中代码),则有效指令周期数占一次循环指令总周期数的比重为80%,提高的代码执效率。

注意:

1. 对于较简单循环体函数,有很好的效果,如果循环体很大,则循环展开的意义不大。

2. 循环展开后,程序的代码量将会增大,所以空间有限的情况下,要在时间与空间上权衡,避免过度展开。

3. 循环展开后,要合理的处理计数器,避免访问越界。计数器最好以0为终点,这样可以省去与终点的比较指令。

7. 有效使用地址模式

LDR/STR指令中的自动索引(auto-indexing)功能时可以完成在加载过程中,往基址寄存器上加一个偏移量的操作,供后面的指令使用。

例如:指令LDR R1, [R2], #4 完成R1= (*R2)及 R2 += 4两个操作,是后索引(post-indexing)的例子;而指令 LDR R1, [R2, #4]! 完成 R1 = *(R2 + 4) 和 R2 +=4 两个操作,是前索引(pre-indexing)的例子。上述指令将加载和地址变化合二为一,减少了一条指令,特别适合于数组处理中。

8. SIMD单指令多数据

在不带MMX指令的ARM处理器(ARM7和ARM9TDMI等)上,一般是不支持SIMD单指令多数据处理。但是合理的安排数据,还是可以进行并行操作的。比如两幅图像按alpha(0--255)合成中,合成公式为

dst = (src1 * alpha + src2 * (255- alpha) ) / 256 (式1)

= ((src1–src2) * alpha + src2*255) / 256。 (式2)

图像中每Pixel中的A、R、G、B四个通道各占8位,对式2中,如果按A、G和R、B分解,用一个32位寄存器来保存,然后进行减法、乘法运算和移位操作,那么就可每次进行两个通道的并行计算。参看下面的代码和运算过程:

clip_image017

在计算R、B之后,再计算A、G两个通道,最后再将两次计算的结果进行合并,完成一个像素的混合。在上述多路并行运算中也要注意防止溢出,此示例中由于alpha的值处于0到255之间,所以不会溢出。

9. MMX优化

...(未写)


附录:

1. 在Elastos 的GNU ARM环境使用GCC产生汇编代码的方法

命令:gcc –O3 –S test.c

参数说明:

–On表示编译器对程序的优化级别,共有O1、O2、O3三个级别。如果不带此参数说明,则不进行编译器优化;

–S 表示生成.S汇编文件;

test.c 表示需要生汇编的C代码;

2. GNU ARM 汇编和armasm宏汇编的格式差别

每种编译器对其汇编指令格式都有不同的规定,下面主要区别GUN ARM和armasm宏汇编的区别。

GNU Arm编译器对于函数调用采用以global开头,后面给出一个加下划线的外部联结标号,标号通过后面有一个冒号来识别。注释采用”@”符号。

.global _ARGB32Blit2ARGB32

_ARGB32Blit2ARGB32:

STMFD SP!, {R4-R12,LR} @注释

...

LDMFD SP, {R4-R12, PC}

Armasm编译器对要求在任何ARM指令或Thumb指令出现以前,用一个AREA保留字定义一个范围。所有汇编文件必须用END保留字表示结束。标号的定义必须在一行的顶格书写。注释采用”;”符号。

AREA sample, CODE, READONLY

EXPORT ARGB32Blit2ARGB32

ARGB32Blit2ARGB32

STMFD SP!, {R4-R12,LR} ;注释

...

0 0
原创粉丝点击