ARM高效C编程和优化--系统架构,C代码规范

来源:互联网 发布:知乎 北京 皮肤科 编辑:程序博客网 时间:2024/06/07 19:43

本节主要介绍在资源受限的ARM设备上,在各种类型的操作系统上的选择,在C语言编程角度,如何构建代码才能更好的指导编译器compiler进行优化,诸如数据对齐data alignment,数据类型data type的选择,C语言函数调用的参数传递方式,以及编译器对结构体和数组的基本处理方式,下节则主要介绍编译器的使用规则,如何指导编译器进行合理的优化,以及系统级的优化,从cache使用到系统功耗控制等。

关键字:ARM Cache 系统 优化 C语言 效率 功耗控制 系统架构 编译器 efficient

Overview

产业界的迅速变化可以轻松地让去年的热点话题在今年变得无足轻重,或者这些热点话题被业界解决或者这些问题变得更加突出。然而效率问题确实业界一直以来都很关注的问题,即便是现在的处理器性能还能按照摩尔定律来增长的情况下。

在早期,如果要想在RAM资源非常稀缺的8-bit的微控制器下开发网络应用程序,需要手工的研究出聪明的方法来进行存储空间的优化,减少存储或者压缩数据空间,重用数据或者按照一定的方式来对数据结构进行编码以让数据结构占用空间更少。后来,比如在触摸屏的收音台控制器上,仍是存在代码空间不足的情况,在32KB的限制代码空间实现控制算法着实不易,因为客户需要的功能会更多,而代码空间始终需要限制在微控制器的有效代码空间内。因而随着功能的增加,需要研究更加精细的方法来找出更好的方法来解决存储不足的问题,如重用code,从汇编的角度来重新排序函数来让代码编的更小,重新分割C标准库来去除不用的sections

以上都是需要对代码存储空间进行效率规划的例子,同理,还会有需要更高的响应速度的系统,比如在时分复用的蜂窝通信系统里对于中断响应延时和算法处理时间的要求。虽然现在的处理系统都能提供容量更大的RAM/ROM资源以及更强大的处理能力,但是业界仍然需要power-efficiency或者更准确的说是energy-efficiency,尤其在现在的移动电子设备或者需要电池的电子设备上。我们需要我们的移动设备一次充电能工作一周以上,而且随着时间演进,我们还需要这个待机时间也能延长。硬件工程师已经在做一些事情来节约能量,同样对于软件工程师,也是需要考虑在已有的硬件功能上来尽力的实现高效代码以节省能量。

A double-sided coin

近年来业界的一个显著变化就是在嵌入式世界中使用平台操作系统。比如大部分的ARM-based的系统,无论是大系统还是出奇的小系统,都会选择众多商用的实时操作系统中的一种或者自己设计类似的实时系统。而通常情况下,在操作系统上开发应用程序的人和开发底层操作系统的并不是相同的一拨人,但是这两类人都需要关注如何变得energy-efficient。这里有一些二者之间的共同点,都可以指导如何开发应用和操作系统,但关注点可能略有不同。 (如下 1).

 

1. The two sides of efficiency

Some basic stuff
首先列出一些基本的优化概念,虽然大部分已经广为人知。关于在嵌入式ARM平台如何构建高效的代码,有专门的ARM优化教程。在为期3天的该workshop里从系统和代码的角度阐述了如何进行一个视频codec的优化并最终在具体的硬件上实现。一些优化能显著提高性能,而C代码优化最终累加的性能提升在200%左右。我们从这个教程里可以发现最大的性能提升来源于对数据和代码Cache的有效配置,这个简单的配置让性能提升5000%。现在的ARM core都是专门设计和优化的以让代码和数据尽可能的利用cache。所以忽略这个简单的事实会让你的系统性能惨不忍睹。后面我们会提到如何写cache-friendly的代码。但第一个基本的观点是记得把cache打开。

Memory use is key
Cache
如此的重要主要是因为存储的访问尤其是对外存的访问代价从时间还是从功耗的角度来说都非常的昂贵。几乎所有的系统都有如图1所示的层次的存储结构,简单的可以是包括一些片上on-chipscratch RAM和一些off-chip的外部RAM,更复杂的则是在core和外部存储系统间存在2级或者3级的cache

 

1. 典型的系统存储架构

一个简单的规则是如果L1 cache的一次访问需要一个时钟周期,可能L2 cache的访问需要10个周期,而一个外部存储需要的时间在100个量级,同样功耗也是这样逐级增长的。因而在写高效代码时,需要时刻牢记这种逐级递增的存储访问消耗。图2是一个简单的对存储访问消耗的评估。所以第二条基本的原则是在cache打开后,需要尽力保证它被充分的利用,即减少对外存的访问。

 

2. 内存访问代价估计

Instructions count, too

从能耗和时间角度考虑,指令执行需要在存储访问之后。一个简单的规则就是你执行越少的指令,你需要的功耗越少,你执行的时间也越少。这里假设所有的指令都是从cache执行的,即每一条指令的执行需要相同的时钟周期,这个假设也符合现在通用的处理平台,如果cache配置正确的话。所以第三条基本的准则是优化代码以更快的执行"optimize for speed"。高效来源于两个方面,第一,更少的执行指令;第二,更快的执行完任务就能尽快的转入休眠状态来节省功耗。后续我们在操作系统层面会提到动态和镜头的功耗管理,但现在我们可以直接的说,更快的代码,更有效率的功耗。

另外就是指令的执行和存储的访问比代价更小,一个简单的结论是算法更倾向于计算(data processing)而不是进行数据通信(data movement)。也可以说CPU-bound的程序要比memory-bound的程序更为高效,当然这些都是相对而言,很多时候在速度优化时,我们会用到用存储空间来换取执行速度的做法。还有更为复杂的情况,针对代码size编译优化的可能会比针对执行速度speed优化的代码更为高效,这是考虑到代码size更小,则更为cache友好。这种情况下,针对speed优化的代码会因为cache性能的恶化而变得效率极低。

Good coding practice

这里说的一个好的准则是"Make things Match.",意思是说让你的数据类型跟你的处理平台架构吻合,让你的代码和处理器的指令集吻合,让存储空间的使用和平台的存储配置吻合,让代码风格和可用工具如编译器吻合。如果在写代码时,就很清楚这代码将怎样被编译器编译优化,怎样在处理器上执行。如你清楚程序调用和全局变量在ARM处理器上如何实现的,你就能更好的让你的程序高效。很多情况下,让你的代码符合工具的风格将远远优于让工具来根据你的风格工作。

Data type and size
ARM
核是32位的,即它的寄存器是32-bitALU也是32-bit,内部的数据通路也是32-bit的。所以32-bit的长度的数据将得到更好的编译处理。如下面的ALU运算,使用32-bit的运算就避免了16-bit运算的符号扩展以及将32-bit数据截断为16-bit数据的额外开销。ARM核有时还可以避免这些多余的操作,比如把32-bit的截断为16-bit的操作可以通过往内存存储16-bit数据的方式来完成。后续的ARM架构还提供SIMD的指令,可以提高这种16-bit操作的低效率,但是通常这样的操作用C语言实现编译器很难完成。另外记住,对于局部变量,无论真实大小,都是占用整个32-bit的寄存器或者占用堆栈的32位字空间的。因而,在ARM平台上使用低于32-bit的数据类型往往没有什么优势,但是这并不完全如此尤其是在考虑SIMD的针对半字等数据类型操作的指令。

 

Data alignment
ARM
核一直以来对代码和数据的对齐方式都有非常严格的要求。直到现在还是很多的core并不支持非对齐的指令,虽然最新的从ARMv6往上的核已经有了非对齐的数据访问。这些最新的core支持非对齐的字和半字数据访问,虽然意图是去除在写代码时对数据对齐的考虑,让硬件支持非对齐访问,从而可以将所有的数据都声明为非对齐的。但这种方法通常都是没有效率的,比如加载一个非对齐字需要1个周期,而在早期不支持非对齐读取的core上可能需要3~4个周期,但是非对齐的读取还是需要硬件的特殊支持,这也带来了性能的损失。这些硬件的特殊支持和转换还是需要一定的时间的,从而带来性能的损失。

Structures and arrays

如果存储空间不需要特别的考虑,那么仍然还有充分的理由来选择数组元素的大小。首先使各数组元素的长度是2的幂可以简化计算每个单元访问。这是因为ARM指令集允许移位操作和ALU结合来作为寻址模式,如一个数组的元素大小为12,基址急促前r3、访问元素索引r1的访问将是如下形式:

ADD r1, r1, r1, LSL #1 ; r1 = 3 * r1
LDR r0, [r3, r1, LSL #2] ; r0 = *(r1 + 4 * r1)

如果数组元素大小为16,则访问可以简化为如下形式:

LDR r0, [r3, r1, LSL #4] ; r0 = *(r3 + 16 * r1)

虽然后续我们会考虑对cache友好的数据访问,但仍然需要提让数据起始在一个cache line对齐,会让数据的访问更在cache更新时更快速。

Efficient parameter passing参数传递
每个不到4字节大小的函数参数也是通过一个32位的寄存器或一个32位的stack来传递的。这个信息被ARM架构的一个叫做程序调用的规格标准(AAPCS)。这也是ARM二进制应用程序接口一部分 (ARM ABI)。最新版本的可以在ARM网站上获得,一般ABI文档是对工具链开发者参考的,而AAPCS的文档是为应用程序开发者准备的。根据AAPCS,有四个可用的寄存器用于传递函数参数。因此这四个参数都可以在简单的函数调用之前通过非常有效的装载到寄存器。同样,作为返回值,一个单一的word-sized(或更小)返回值可以通过一个返回寄存器,一个双字的值在两个寄存器中返回。显而易见,如果需要传递超过四个的参数,则需要把其余的参数放在堆栈空间,在使用时从内存空间加载,这显然需要额外的指令和时间。所以在编程时需要牢记的一个很简单的规则就是"保持四个或更少的参数"。进一步的,双字的参数需要使用奇偶一对寄存器来传递参数,如r0r1一对寄存器或者r2/r3一对寄存器。

以下的函数调用用r0传递参数 'a'r2r3来传 'b',而 'c'则在堆栈上,r1则因为双字要求的对齐方式而无法使用。

fx(int a, double b, int c)

而是用如下的声明函数则可以充分利用寄存器资源:

fx(int a, int c, double b)

下节内容主要介绍编译器的使用规则,如何指导编译器进行合理的优化,以及系统级的优化,从cache使用到系统功耗控制等。

程序员需要做什么
在多核系统中,硬件的高性能也许让我们决定一切都交给操作系统把,然而在写代码和配置操作系统时如果能考虑如下因素是非常重要的。

1)系统效率(System efficiency):智能和动态的任务优先级调度;负载平衡;
2)
计算效率(Computation efficiency):数据,任务和函数级别的并行;减少同步开销overhead
3)
数据效率(Data efficiency有效利用存储系统特性,谨慎维护cache一致性以避免cache颠簸和错误的core间共享。

总结

 1)  合理配置工具和硬件平台;2) 仔细写代码和合理配置配置cache以尽可能减少外部内存访问;3) 速度优化以及合理利用NEON等运算加速器以减少指令执行数;


References:
[1] Reducing Energy Consumption by Balancing Caches and Scratchpads on ARM Cores, Mario Mattei, University of Siena

[2] Wattch: A framework for Architectural-Level Power Analysis and Optimizations, Brooks et al, Princeton University

[3]Evaluating the performance of multi-core processors – Part 1, Max Domeika, Embedded. com

http://www.eetimes.com/design/embedded/4210470/Efficient-C-Code-for-ARM-Devices

http://www.blog.163.com/houh-1984/

关键字

ARM Cache 系统 优化 C语言 效率 功耗控制 系统架构 编译器

本节主要介绍在资源受限的ARM设备上,在各种类型的操作系统上的选择,在C语言编程角度,如何构建代码才能更好的指导编译器compiler进行优化,诸如数据对齐data alignment,数据类型data type的选择,C语言函数调用的参数传递方式,以及编译器对结构体和数组的基本处理方式,下节则主要介绍编译器的使用规则,如何指导编译器进行合理的优化,以及系统级的优化,从cache使用到系统功耗控制等。

原创粉丝点击