随着DSP处理器性能的提升以及编译器最佳化技术的进步,曾经红极一时、仅用汇编语言编写DSP应用程序的作法已逐渐被淘汰。今天,几乎每个DSP应用程序都使用C语言程序代码和汇编程序码混合的方式。对于一些性能需求极高的关键功能,DSP工程师会继续使用高度最佳化的汇编程序码;而一些次要的功能现在也使用C语言编写,使程序代码更容易维护和移植。对于C语言和汇编程序码的结合,每位DSP工程师都需要掌握特殊的工具和方法。
众所皆知,汇编语言编码具有更高的性能优势,而用C语言编码则较容易且速度也更快。为了解其中原因,以下我们进一步比较汇编语言编码与C语言编码的优缺点:
汇编语言编码的优点:
‧汇编语言编码可以充分利用处理器的独特指令以及各种专门的硬件资源。而C语言程序代码是通用型程序代码,必须支持各种硬件平台,因此很难支持特殊平台程序代码。
‧汇编程序设计人员通常对应用程序非常熟悉,可以作出编译器无法作出的假设。
‧汇编程序设计人员可以发挥人类的创造性;而再先进的编译器也只是一个自动化的程序。
汇编语言编码的缺点:
‧汇编程序设计人员必须解决耗时的机器级问题,如缓存器分配和指令排程。若使用C语言程序代码,这些问题可以由编译器解决。
‧使用汇编语言编码的程序设计人员必须了解DSP架构及其指令集的专业知识;而C语言编码只需要掌握相当普及的C语言知识。
‧若使用汇编语言,将应用程序从一个平台移植到另一个平台非常困难也相当耗时;而C语言应用程序的移植相对而言非常容易。
图1显示了如何利用专用硬件机制来获得高度最佳化的汇编程序码。左边的C语言编码利用模块算法设计出一个循环缓冲区P1;右边高度最佳化的汇编程序码中,等效的缓冲区是利用CEVA-TeakLite-III DSP核心的模块运算机制(Modulo Mechanism)设计产生的。只要缓冲区指标(本例中的r0)有更新,模块运算机制便会自动执行模块运算。这种运算与指针更新在同一个周期发生,因此汇编程序码比C语言程序代码更加高效,可为模块运算产生独立的指令。
图1:右边的CEVA-TeakLite-III汇编程序码可以建置成左边的C语言程序代码。
为DSP应用选择C/汇编程序码
混合使用的问题就在于该如何划分C语言程序代码和汇编程序码的界限,而答案取决于剖析器提供的性能分析结果。然而在使用剖析器之前,DSP工程师需要为应用程序定义清晰的对象,一些典型的对象包括循环数、程序代码大小和数据大小。一旦这些对象确定后,所有应用程序都应该先以C语言编写和制作,随后使用剖析器来分析性能。
在一些极端情况下,如控制应用,用C语言层级的编码就足够了;但大多数情况下,原始C语言层级应用程序版本不会遵从一个或多个对象,这也意味着需要使用一些汇编程序码来完成。在求助于汇编语言编码之前,C语言编码可提供许多方法来提高性能,但这些方法不属于本文讨论的范畴。假设所有C语言级的方法全用完了,并且准备使用汇编语言编码,这时强烈建议将原始C语言程序代码保存起来。这样不仅方便除错,而且一旦条件许可(比如转移到更强大的平台)还可以回复原始的C语言。
程序代码中的汇编语言部份应尽可能维持在最少,这样便能分析从剖析器得到的性能结果,并定义应用程序中的关键函数。关键函数会占用大部份执行时间,必须用汇编语言重写才能满足性能对象。当两到三个最关键的函数重写后,需要重新进行性能测量,若应用程序仍然不能满足对象需求,那么必须使用汇编语言定义并重写额外的关键函数,这个过程需要不断地重复直到满足性能对象需求为止。
汇编语言设计师的编译器考虑
在编写会与C语言程序代码结合的汇编程序码时,汇编程序设计人员必须了解编译器的惯例和假设。其中有个重要的编译器惯例是函数呼叫惯例,也称为函数参数传递惯例。这个惯例描述了编译器如何在一个函数呼叫另一个函数时传递参数。为了使汇编语言函数能被C语言函数成功呼叫;反之亦然;汇编语言函数必须截取参数,然后将参数发送到由函数呼叫惯例定义的硬件资源上,通常为缓存器或堆栈内存。
汇编程序设计人员还必须了解编译器的缓存器使用惯例。这些惯例将硬件缓存器分成被呼叫者保存(callee-saved;或呼叫者使用,caller-used);以及被呼叫者使用(callee-used;或呼叫者保存,caller-saved)缓存器。编译器假设被呼叫者保存缓存器在函数呼叫过程中保持不变的值,若汇编程序设计人员希望使用这种缓存器,他们必须先将缓存器的值备份,然后在返回到C语言程序代码之前恢复这些缓存器的内容;相反的,被呼叫者使用缓存器被认为在函式呼叫过程中不会保持一定的值。这意味着汇编程序设计人员使用这些缓存器之前无需进行备份,不过他们必须记住,当汇编语言函数呼叫C语言函数时,被呼叫者可以对这些缓存器进行覆写。
图2为一个从CEVA-X1641 DSP核心FFT实作中截取的汇编程序码案例。其中以黄色标示的add指令遵循CEVA-X1641编译器的呼叫惯例,在r0地址缓存器中传递指针参数。标为蓝色的pushd指令用于备份,随后函数会使用的被呼叫者保存缓存器。
图2:从CEVA-X1641手写FFT算法汇编语言实作摘录的一段程序代码。
除了呼叫惯例和缓存器使用惯例(针对每个编译器下定义),一些编译器在人工编写的汇编程序码方面可能会有一些额外的假设。这些假设通常是针对编译器,因此编译器供货商应该提供完善的数据和说明。例如,一些DSP架构会有内存存取对齐限制,用于这些DSP的编译器通常假设堆栈指针以某个宽度(如32位)对齐,这允许编译器最佳化堆栈的读写作业,并使用机器的全部内存频宽;此外亦要求汇编程序设计人员在呼叫C语言函数前确保堆栈对齐,否则会发生对齐错误的存取。
编译器假设的另外一个例子与人工编写的汇编程序码中特殊指令的位置有关。例如,CEVA-X1641编译器假设一个mov acX, rN指令(将累加器移动到地址缓存器)永远不会作为汇编语言函数的第一条指令。当填充呼叫指令(呼叫一个函数)的延迟槽时,这个假设可提供更佳的指令排程。像这样特殊的假设通常可以用专用编译功能覆盖。
连接C/汇编语言的延伸功能
大多数用于嵌入式平台的编译器,特别是用于DSP程序设计上,都具有丰富的C语言和汇编语言连接功能。其中绝大部份功能不属于标准C语言,因此被称为C语言延伸功能。以下列出的是一些对DSP程序设计更有用的功能。
内嵌汇编语言。这个功能可让程序设计人员将汇编语言指令插入C语言程序代码,当必需使用如装置驱动程序等低阶C语言程序代码直接存取机器资源时,会常使用到该功能。由于在大多数使用该功能的实作中,编译器对所要插入的指令信息所知有限,因此对它们的特性会作出最坏的假设,这种假设可能会妨碍许多编译器最佳化作业。例如,在支持某些指令(并非全部指令)平行处理的架构中,编译器不会将插入指令与其它指令作平行处理,因为这种作法很可能会导致非法指令封包。
将硬件缓存器连结到C变量。将一个硬件缓存器连结到一个C变量时,C语言程序代码中的变量值即反映出硬件缓存器的值;反之亦然。每当C变量被读写时,硬件缓存器也相对地被读写。这个功能在低阶程序代码中很常见,时常与汇编语言指令内嵌功能结合在一起,允许内嵌汇编程序码存取C语言层级的变量。图3的例子显示了内嵌汇编语言功能(标示为橙色)和硬件缓存器连结功能(标示为紫色)的常见组合。
图3:结合内嵌汇编语言和硬件缓存器连结的程序代码片段。
内存扇区属性。预设状态下,编译器将全域C变量和函数分配到标准的预定义内存扇区,该扇区属性允许程序设计人员将上述变量和函数分配到特殊的使用者定义内存扇区。在随后的连结阶段,这些内存扇区可以被映像到具体的内存地址。该功能可让程序设计人员将C语言层级单元分配到实际的内存位置,这对DSP应用程序来说至关重要。
使用者定义呼叫惯例。如上所述,编译器有一个汇编程序设计人员必须遵守的预定义呼叫惯例;然而在某些情况下,汇编语言函数可利用不同的呼叫惯例获得更佳的最佳化效果。例如,编译器理论上会在累加器中传递参数。若执行延伸地址计算的函数能接收地址缓存器中的参数,那么它的效率会更高。该功能会依靠附加在函数原型的专用语法,并通知修正后的呼叫惯例编译器。
编译器内嵌函数。意指能够用专用的宏或函数呼叫,触发内建编译器功能的总称。例如,CEVA-X和CEVA-TeakLite-III编译器可为语音编码器中常见的ETSI/ITU基本DSP作业,提供编译器内嵌函数。针对这些作业,编译器可利用其等效高度最佳化汇编语言序列,取代每个基本作业。
相反地,没有内嵌函数支持的编译器必须呼叫使用者定义的函数,这样做会导致两大性能缺陷:首先,使用者定义函数可能会在一个回路里产生函数呼叫和返回(如图4),因此产生了巨额的开销;其次,使用者定义函数将如同其它C语言程序代码一样被编译,这意味着使用者定义函数可能会获得次要的最佳化性能。而另一方面,具有内嵌函数的编译器已经内建了最佳化的实作。
图4:H.264编码器——一个关键的函数性能案例。
图4说明了这个功能的重要性。在图4中,左边的C语言程序代码使用ETSI的mult_r(循环相乘)基本作业,CEVA-TeakLite-III编译器产成了如右边的高效实作结果。mult_r作业在左边的C语言程序代码和右边的汇编程序码中以紫色标示。
汇编语言内嵌函数。汇编语言内嵌函数是将汇编程序码内嵌到C语言程序代码的一种先进方法,详细介绍如下。
汇编语言内嵌函数——将汇编语言指令当作C语句一样编写
上述内嵌汇编语言功能具有显著的缺点:
1. 它会破坏各种编译器的最佳化作业,由于编译器不了解内嵌程序代码的内容,因此会使用最坏的假设。
2. 它可能迫使程序设计人员处理低阶问题,如缓存器分配和指令排程。
汇编语言内嵌函数功能可以帮助程序设计人员实现内嵌汇编程序码,且不会产生上述缺点。从程序设计人员的角度来看,汇编语言内嵌函数就像是C语言宏或函数,它们会在呈现一个单一汇编语言指令时,接收C语言层级变量并返回C语言输出结果。由于涉及该功能的所有程序代码都在C语言层级,因此程序设计人员不必担心缓存器分配、指令排程和其它低级语言问题。汇编语言内嵌函数不仅不会妨碍编译器最佳化作业,还会参与最佳化过程,就好像是编译器固定产生的汇编语言指令一样,这些特性造就了强大的汇编语言内嵌函数功能。
利用汇编语言内嵌函数,程序设计人员可以从特殊汇编语言指令中受益,这些指令不太可能从编译器中产生,且通常是为特定算法量身定做的。在适当的位置采用这些指令可以大幅提高性能;例如,CEVA-X1641的bitrev指令就是为FFT等算法定制的。由于编译器不太可能把一个程序看作FFT而使用bitrev指令,因此程序设计人员只需将bitrev汇编语言内嵌函数嵌入到C语言程序代码中。
结合程序设计人员对应用程序的专业知识,汇编语言内嵌函数功能也相继提升。利用这种专业知识,程序设计人员可将精密的汇编语言序列内嵌函数用在C应用程序中的关键性能区域里。这样一来,程序设计人员便能确保编译器产生的汇编程序码效率就如同手动编写的程序代码一样高。
图5是使用汇编语言内嵌函数与CEVA-X1641编译器的例子。左边的C语言函数使用st(储存,以红色标示)和msu(乘法和减法,以紫色标示)汇编语言内嵌函数。st内嵌函数参与判断(标示为蓝色)和延迟时隙填充(标示为绿色);msu内嵌函数则参与回路解开(标示为橙色)和Quad-Mac(标示为紫色)。汇编语言内嵌函数还受益于由CEVA-X1641编译器处理的所有机械相关问题,如缓存器分配、指令排程和硬件单元分配。
图5:CEVA-X1641编译器支持汇编语言内嵌函数的使用。
同时使用C/汇编语言进行除错
汇编程序码的除错并非一件小事,它需要对如延迟和内存对齐限制等架构和机械层级问题有深入的了解。单纯地结合C语言程序代码与汇编程序码会使问题变得更加棘手,因为这样一来程序设计人员便得对C语言程序代码和汇编程序码间的连结进行除错。
进行混合应用程序除错的第一步就是隔离问题。假设汇编程序码的C语言层级实作维持不变,且C语言层级实作能正常作业,那么将汇编语言函数转换成C语言实作并重新测试应用程序就变得相当容易。为了迅速检测问题,程序设计人员可以采用交互作业程序:每一个步骤都将一半的可疑函数转换为相应的C语言实作,这样一来程序设计人员在进行每一步时都只需测试前一步中一半的函数。
一旦有问题的汇编语言函数被确定,就应该同时调查单独汇编语言问题和C语言与汇编语言的连接问题。单独汇编语言问题的除错对汇编程序设计人员来说十分简单明了,但C语言与汇编语言的连接问题就有点麻烦。不同于单独的汇编语言问题,在查看汇编语言函数本身时,无法看见C语言与汇编语言的连接问题;为了找出这些问题,程序设计人员必须检查编译器的惯例,例如呼叫惯例和缓存器使用惯例。
程序设计人员还必须检查编译器假设,例如汇编语言指令的行踪(重复前面提到的例子,CEVA-X1641编译器假设mov acX, rN指令绝不会作为汇编语言函数的第一条指令)。为了节省除错时间,程序设计人员应该在第一次实作汇编语言函数时验证是否所有的编译器惯例和假设都有遵循惯例。
H.264视讯编码器和AMR-NB
本文讨论的技术和方法已被CEVA公司用于各种应用程序中,包括视讯编译码器、音讯编译码器、语音编码器和装置驱动器。此外,本文所述的功能无论用在何种案例,均能显著地提升性能。
H.264视讯编码器是一个很好的研究案例。它在处理能力(通常以MHz衡量)及其它资源方面都有强烈需求,特别是在与音讯编译码器等其它类型的编译码器比较上。
CEVA公司利用其高阶CEVA-X16xx DSP核心系列及其MM2000多媒体平台,提供这种编码器所需的处理能力。
CEVA公司利用先进剖析技术确定这种编码器的关键函数,然后对它进行最佳化。编码器的关键函数最佳化过程是逐步完成的。首先,利用如汇编语言内嵌函数这样的先进功能全面地将函数最佳化成C语言;然后进一步将编译器提供的汇编程序码最佳化成汇编语言层级。
图6显示出透过对这种编码器关键函数进行最佳化过程所获得的性能改善。只有最后一个最佳化阶段涉及到全部汇编程序码范围;所有其它阶段都基于具有汇编语言内嵌函数的C语言程序代码。这些汇编语言内嵌函数主要用于SIMD(单指令多数据)作业,如avg_acW_acX_acZ_4b。这条指令对8个输入字节执行字节平均,进而产生4字节。这种SIMD作业对执行大量字节层级运算的视讯编译码器而言相当实用(这也是为何CEVA-X16xx架构为字节层级的SIMD作业提供广泛支持的原因)。
图6:CEVA-TeakLite-III编译器内建ETSI基本作业支持。
AMR-NB(自适应多码率——窄频)是广泛应用在无线通讯应用的语音编译码器。CEVA已为其所有DSP核心建置该语音编码器;但为遵循本文主旨,我们在此只讨论CEVA-X1620建置。将这种语音编码器完全建置到汇编语言的情况相当常见,倘若使用本文提到的各种功能,C语言实作和CEVA-X1620编译器可达到与汇编语言实作竞争的结果。其中提升CEVA-X1620编译器性能的关键功能就是支持ETSI内嵌函数的功能。
图7显示了整个AMR-NB应用经过最佳化过程后在MCPS(每秒百万循环)上所获得的性能改善。只有最后的最佳化阶段涉及了全范围的汇编语言编码,所有其它阶段都基于具有ETSI内嵌函数和汇编语言内嵌函数等的C语言程序代码。
图7:对ARM-NB进行各种最佳化方法所获得的MCPS改善。
总之,H.264编码器和AMR-NB的案例清楚地显示了汇编语言实作的性能优势,但也显示出纯汇编语言实作并非首选的最佳化方法。利用高质量软件开发工具链提供的各种C语言和汇编语言功能,DSP程序设计人员不必用汇编语言建置整个应用程序也能达到令人满意的性能结果。正如本文所述,编写C语言和汇编语言混合程序代码不是一件简单的工作;不过,本文讨论的各种功能都有助于DSP工程师更轻松地完成这项任务。