微波EDA网,见证研发工程师的成长! 2025年04月02日 星期三
首页 > 硬件设计 > MCU和DSP > Blackfin C语言优化

Blackfin C语言优化

时间:02-28 来源:互联网 点击:
3. 编译器,睡在上铺的兄弟
这一刻,你不是一个人在战斗…,这话听起来好像有点耳熟。如果把C语言优化比作是程序员在进行的一场战斗的话,程序员并不孤独,因为我们有一个隐形的战友,就是编译器,而编译器的优化功能就是我们最有力的武器。以VisualDSP++为例,通常新建的工程C语言优化缺省是不打开的,程序员可以按照程序运行的需要打开优化。这个从不优化到优化的过程实际上反映了VisualDSP++编译器在处理C语言程序过程中的两步走。
在优化开关没有打开的情况下,编译器对C代码的处理是一一对应的直译,就是把C代码一句一句按照先后顺序翻译为相应的一条或者多条汇编语句。在直译的同时,编译器也会注意到对中间变量和中间结果的保护——不管他们接下来会不会被用到,他们都会被写入存储器,尽管这样做会增加很多冗余。经过这样的直译,一段C代码对应的汇编代码可能是多一个数量级的。一个典型的例子是,只有两条乘累加指令的for循环代码对应的汇编代码是几十条之多。可想而知,这样不经优化的代码执行速度是很慢的。一个参考数据是打开优化开关以后的代码运行速度平均可以提高20倍。也就是说,一个600MHz的芯片,不打开优化,相当于主频降到30MHz。所以绝大多数情况下我们要打开编译器的优化开关。
编译器的第二步走,就是对直译产生的代码进行优化,这个过程就是充分利用DSP的硬件实现指令和事件最大可能并行的过程。这里的并行既有运算单元本身的并行也有运算单元和其他功能单元的并行。以Blackfin为例,每一个core里都有两个乘法器和加法器。编译器在优化的时候第一个层次的并行是运算的并行,就是尽可能同时使用两个运算单元,做乘法就尽可能做到两个乘法器同时运算,做加法就尽可能做到两个加法器同时运算。接下来一个层次的并行是指令的并行,就是运算单元和memory存取、或者其他功能单元之间的并行,仍以Blackfin为例,在同一个cycle中,可以有两个乘累加和两个数据的存或取并发执行。这些并行都是DSP硬件本身支持的,编译器优化的工作就是充分利用DSP的硬件能力。
循环是编译器在第二步走的过程中重点处理的对象。这比较好理解,因为那些大量消耗cycle的代码往往是在循环当中的。下面我就结合编译器对循环的处理,来看看在优化的过程中程序员要怎么和编译器并肩战斗。编译器对循环处理的目标就是希望在每一次循环中尽可能的并行。为了实现这个目标,编译器采取的措施就是不停的打开循环、降低循环次数,增加循环内的指令个数,提高指令之间并发的几率。举个例子,一个100次的循环中有一个乘累加,编译器打开循环,将循环次数降低一半,循环内每次就会出现两个乘累加,编译器就有可能安排Blackfin的两个乘累加单元同时运算,从而将执行的效率提高一倍,这个优化过程叫做矢量化(Vectorization)。如果这个循环中还有加法、减法、存数、取数,或者其他运算,编译器还会安排这些指令和乘累加并发,或者这些指令之间并发,这个优化的过程也是实现软件流水线的过程(SoftwarePipeline)——在优化后的代码中往往出现当前的运算和以往的存数或者未来的取数并行。编译器对循环的打开可能是多次的,直到编译器有足够的指令可以充分安排并发。
说到这里我们对这位睡在上铺的兄弟已经有一些了解了,那么程序员在这个优化的过程中应该做什么呢?这就要从矢量化和软件流水线受到的限制谈起。刚才提到在优化过程中编译器一个重要的操作就是打开循环,如果循环次数是2的N次方例如8,16,32…,编译器就可以很舒服的按照需要多次打开循环。但如果在上面的例子里循环次数是101,编译器是无法打开循环的,对这个循环的优化就不能有效的展开。这个时候就需要程序员做工作了:我们可以将循环里面的运算在循环外实现一次,让循环次数变为100,从而给编译器两次打开循环的机会(2x2x25)。矢量化和软件流水线对操作数的存放也是有要求的。首先,对memory中操作数读取和计算结果存放必须是顺序(地址递增或者递减)的,如果是乱序或者随机的,不管是运算的并行和是指令的并行都很难实现。我们在编写程序和对C程序进行优化的时候就要注意到尽可能安排数据访问的顺序性。其次,根据操作数的宽度,程序员还要注意保证数据的2字对齐或者4字对齐。这有助于在指令并行执行时对操作时的有效读取。程序员可以通过在定义数据(组)的时候用编译器提供的相应编译选项来实现数据的对齐。在进行矢量化和软件流水线的过程中往往要对程序执行的顺序做局部调整,这种调整对程序整体来说虽然是微调,但在某些情况下改变原始程序执行的顺序会影响到程序执行结果的正确性。最典型的情况就是运算的操作数和结果之间存在某种联系和依赖。比方说数组中靠后的成员数值取决于靠前的成员运算的结果,这意味着数组成员之间有依赖性,不独立,从而不能实现并行计算。这在for循环中经常体现为一个运算的两个操作数指针可能是指向同一个数组的不同位置。数据独立性是到目前位置我们看到影响客户C代码优化效率最严重的因素。
编译器在进行优化的时候永远都遵循一个基本原则,那就是优化不能影响程序运行的正确性。所以当编译器发现矢量化和软件流水线需要满足的那些条件不确定的时候,它的行为往往是保守的。这是一种宁可放弃性能也要保证正确性的态度,无可厚非。该出手时就出手,到了程序员帮编译器一把的时候了。因为编译器面对的这些不确定性,在程序员看来通常是确定,一定,以及肯定的。以前面数据独立性的问题为例,编译器很难判断当前for循环中两个指针pa,pb在运行的时候是不是会指向同一个数组,因为对编译器来说它们只是两个指针,对它们后面实际操作的对象毫无头绪。而程序员却可能清楚的知道这段程序处理的两个数组是定义在两段不同的物理内存上的,也就是说这两个指针不会指向同一段地址,数据的独立性是有保证的。这个时候我们就可以通过相应的编译选项通知编译器:下面这个for循环里的数据是独立的,放心大胆的优化吧。这里提到的编译选项,包括前面说的关于循环次数,数据对齐,以及存储位置等其他编译选项都可以在VisualDSP++关于C语言编译器的手册中找到。
了解了编译器的工作方式,针对矢量化和软件流水线对代码和数据存储的要求,在C语言范围内对相关代码进行调整,并通过编译选项将有利于优化的确定信息通知编译器,依托C语言编译器的能力实现代码的高效优化,就是程序员在这里要做的工作。

Copyright © 2017-2020 微波EDA网 版权所有

网站地图

Top