充分利用片上存储器
从我们的能量图可以清楚地看出,TCM是到目前为止系统具有的最高效存储器类型。不是所有的系统均具有A R M称为TCM的存储器(通过专用和优化的接口连接到内核),但是大部分系统至少具有某种片上快速存储器类型。为了便于讨论,我们指的是常见的片上存储器(SPM)。假定S PM单次存取能耗大约是外部R A M存取能耗的1/170,充分利用这种SPM存储器应该是首选。
图3:SPM的能量优势
图3中的图表显示了简单的“多类”基准,甚至128个字节的S PM区域都可以减少大约一半的功耗。1k字节的存储器最大可减少70%的功耗。本次研究(Mar wedel, 2004)中采用的方法是从外部R AM到S PM动态重定位代码和数据片段。甚至在按需移动各项开销方面,不仅降低了能耗,性能也提高了大约60%。
很显然,我们在某个点上正在损失回报。这种情况下,S PM超过1k时,性能提升幅度变缓,系统总能耗也会稍微升高。在这里,我们实际上在承担这种特定应用无法使用的S P M能耗,因为这种应用程序本身并不够大。
您还可以注意到,在结合了所使用的分配算法时,这种特定的应用无法使用小于64字节的S P M区域,因为没有足够小的可用片段与之相配。本次研究中还展示了一个更加成熟的算法,可以在最佳状况下节省能耗可以超过80%。
永远做缓存友好的事
分析缓存优点有时可能比分析S PM优点更加复杂。一方面,缓存基本上是自我管理的。另一方面,缓存不是对单个存储位置进行操作,而是对固定大小的“线路”进行操作。因此,访问单个可缓存的存储位置可能加载整条线路,从而造成突发的内存存取。如果该附加的数据从未被访问过,则所消耗的能量就浪费了。
另一个不利之处是缓存所需的其他逻辑成本(就硅片面积和功耗而言)。
图4:缓存的能量优势
图4摘自普林斯顿(Brooks,2000)一份论文,显示了针对某简单应用基准的三套数据。针对不同的缓存大小,这些条块分别代表性能IP C(单位周期指令数)、功耗和功耗时间积(ED P)。总的来说,性能会随着缓存大小的增加而提升。但是,系统的功耗也会增加,因为增大缓存单元会相应增加功耗。功耗时间积允许我们在性能和缓存大小之间取得平衡。在这个例子里,存在一个最佳点,即缓存大小为64k时,此时的功耗时间积最小。
最大限度减少数据内存存取
A RM架构的一个特性是其常量是不确定的,特别是,不可能用单条指令把一个任意32位常量放到一个寄存器中。实际上,所有内存存取必须按寄存器中的地址操作,这就意味着程序需要把这些地址和其他常量频繁地放到寄存器中,而这一点很难做到。解决此问题的标准方法是把常量作为文字数据嵌入到代码段中,在运行时使用PC相关的加载进行加载。
因此,这种最大限度减少常量影响的方法很实用。确保在编译时这些常量是已知的,如果可能,最好能把这些常量嵌入到单条ARM指令中。为了存取全局变量,尽可能减少加载基址指针的需求。这就需要确保全局变量在运行时都在内存中,这样才能使用单个指针存取多个变量。实现这个目标最简单的方式是将全局变量放到一个结构中。
尽管A R M的堆栈访问相对高效(堆栈访问可较好地加载和存储多条指令),但是程序员还可以通过很多方式来减少堆栈访问:减少活动变量、避免占用本地变量地址、可能时充分利用尾部调用优化、将传递到函数的参数数量减少到四个以下、允许编译器主动内联函数等。
递归情形和避免递归情形的做法更加复杂。通常编译器可以对归函数很好地进行尾部优化。实际上将所有数据存储到堆栈中可以比其他做法获得更好的局部性。或许建议可能最好表达为“除非其他做法让数据局部性更糟或您确信编译器可以对递归调用进行尾部优化,否则不要使用递归算法”。应编写异常处理程序,增加尾部连锁的机会,进而避免堆栈环境内不必要的保存和恢复。
现在我们把注意力转到这个问题的第二头大象,即指令执行。
最大限度减少指令数目
事实上,减少指令执行次数本质上与性能优化是相同的,执行的指令数越少,能耗就越低。另外,还要增加一些明显的指针。
首先,正确地配置工具。在编译器和链接器完全了解目标平台,甚至无法实施一些基本的优化。
编写代码时要保持敏锐,才能避免不必要的操作。对于A R M架构,32位数据类型是高效的:一般8位和16位数据类型,尽管占用的存储空间较少,但是处理效率也较低。在v6和v7架构中,打包和接包指令以及S IM D操作一定程序上对此有些帮助,但是要注意,在主程序中无法从C访问这些指令。
编写循环时要当心
可以按照以下一些简单的规则来编写循环:使用无符号的整数计数器,向下倒数,并把是否等于零作为终止条件。这可以让循环更短,速度更快,使用的寄存器更少。还要记住,要采用矢量化来编写循环。即使在尝试展开和矢量化最简单的循环时,有关控制结构和数据声明的一些简单规则都可以让编译器的作业变得更简单。
图5:循环展开
图5显示了与一个特定循环优化有关的一些数据,这个循环优化就是循环展开(Brooks,2000)。按照预期,随着展开因子的增加,执行时间和指令数目会减少。我们看到了减少循环开销和减少地址计算的效果。功率结果更加有趣,但不太明显。因为预测器可用来训练其行为的分支更少且针对循环结束失败的最终错误预测比例大增,所以随着循环进一步展开,分支预测器的准确性出现下降。但是,因为顺序取指的连续数据流不经常被中断,所以取指阶段的效率可以提升。组合的结果是减少了每条指令的净能耗。
因此尽管执行时间基本上低于展开因子4,但是因为功耗持续降低,所以所有重要的功耗时间积也随之降低。因此有能耗意识的编译器或开发人员与只考虑执行时间的编译器或开发人员相比,会更倾向于展开循环。
精度满足需求即可
还必须考虑输出要求的精度。即使有浮点硬件可用,定点实现的计算通常比浮点实现的计算更有效率。如果您正在渲染一个供屏幕查看的图像,可能并不需要完全符合标准,您只需要渲染出可以接受的图像。
对标准M P E G- 4解码函数进行递进优化的一项研究(S h i n,2002)已经表明,把软浮点切换为定点二进制可以把能耗降低72%。精度损失意味着该结果不再符合标准,但是在所研究的系统上仍然足以满足渲染用途。
关于Thumb
T humb指令集专门设计用于改进代码密度,还可以提升窄内存系统的性能。但是,在代码密度确实改进的同时,指令数也同时增加了。这是因为,与A R M指令相比,减少了个别Thumb指令的功能。因此Thumb重新编译会造成能耗增加,这看起来是合理的,而我们看到的事实也的确是这样。
上述研究表明,如果代码大小减少4%,指令执行数增加38%,而能耗增加28%。为了找到第三头大象,我们需要走出处理器及其内存的领域,着眼于范围更大的系统。我们这些天使用的系统已经被我们的硬件设计同事组合到了一起,这个系统提供了大量节能选项。
充分利用片上存储器
从我们的能量图可以清楚地看出,TCM是到目前为止系统具有的最高效存储器类型。不是所有的系统均具有A R M称为TCM的存储器(通过专用和优化的接口连接到内核),但是大部分系统至少具有某种片上快速存储器类型。为了便于讨论,我们指的是常见的片上存储器(SPM)。假定S PM单次存取能耗大约是外部R A M存取能耗的1/170,充分利用这种SPM存储器应该是首选。
图3:SPM的能量优势
图3中的图表显示了简单的“多类”基准,甚至128个字节的S PM区域都可以减少大约一半的功耗。1k字节的存储器最大可减少70%的功耗。本次研究(Mar wedel, 2004)中采用的方法是从外部R AM到S PM动态重定位代码和数据片段。甚至在按需移动各项开销方面,不仅降低了能耗,性能也提高了大约60%。
很显然,我们在某个点上正在损失回报。这种情况下,S PM超过1k时,性能提升幅度变缓,系统总能耗也会稍微升高。在这里,我们实际上在承担这种特定应用无法使用的S P M能耗,因为这种应用程序本身并不够大。
您还可以注意到,在结合了所使用的分配算法时,这种特定的应用无法使用小于64字节的S P M区域,因为没有足够小的可用片段与之相配。本次研究中还展示了一个更加成熟的算法,可以在最佳状况下节省能耗可以超过80%。
永远做缓存友好的事
分析缓存优点有时可能比分析S PM优点更加复杂。一方面,缓存基本上是自我管理的。另一方面,缓存不是对单个存储位置进行操作,而是对固定大小的“线路”进行操作。因此,访问单个可缓存的存储位置可能加载整条线路,从而造成突发的内存存取。如果该附加的数据从未被访问过,则所消耗的能量就浪费了。
另一个不利之处是缓存所需的其他逻辑成本(就硅片面积和功耗而言)。
图4:缓存的能量优势
图4摘自普林斯顿(Brooks,2000)一份论文,显示了针对某简单应用基准的三套数据。针对不同的缓存大小,这些条块分别代表性能IP C(单位周期指令数)、功耗和功耗时间积(ED P)。总的来说,性能会随着缓存大小的增加而提升。但是,系统的功耗也会增加,因为增大缓存单元会相应增加功耗。功耗时间积允许我们在性能和缓存大小之间取得平衡。在这个例子里,存在一个最佳点,即缓存大小为64k时,此时的功耗时间积最小。
最大限度减少数据内存存取
A RM架构的一个特性是其常量是不确定的,特别是,不可能用单条指令把一个任意32位常量放到一个寄存器中。实际上,所有内存存取必须按寄存器中的地址操作,这就意味着程序需要把这些地址和其他常量频繁地放到寄存器中,而这一点很难做到。解决此问题的标准方法是把常量作为文字数据嵌入到代码段中,在运行时使用PC相关的加载进行加载。
因此,这种最大限度减少常量影响的方法很实用。确保在编译时这些常量是已知的,如果可能,最好能把这些常量嵌入到单条ARM指令中。为了存取全局变量,尽可能减少加载基址指针的需求。这就需要确保全局变量在运行时都在内存中,这样才能使用单个指针存取多个变量。实现这个目标最简单的方式是将全局变量放到一个结构中。
尽管A R M的堆栈访问相对高效(堆栈访问可较好地加载和存储多条指令),但是程序员还可以通过很多方式来减少堆栈访问:减少活动变量、避免占用本地变量地址、可能时充分利用尾部调用优化、将传递到函数的参数数量减少到四个以下、允许编译器主动内联函数等。
递归情形和避免递归情形的做法更加复杂。通常编译器可以对归函数很好地进行尾部优化。实际上将所有数据存储到堆栈中可以比其他做法获得更好的局部性。或许建议可能最好表达为“除非其他做法让数据局部性更糟或您确信编译器可以对递归调用进行尾部优化,否则不要使用递归算法”。应编写异常处理程序,增加尾部连锁的机会,进而避免堆栈环境内不必要的保存和恢复。
现在我们把注意力转到这个问题的第二头大象,即指令执行。
最大限度减少指令数目
事实上,减少指令执行次数本质上与性能优化是相同的,执行的指令数越少,能耗就越低。另外,还要增加一些明显的指针。
首先,正确地配置工具。在编译器和链接器完全了解目标平台,甚至无法实施一些基本的优化。
编写代码时要保持敏锐,才能避免不必要的操作。对于A R M架构,32位数据类型是高效的:一般8位和16位数据类型,尽管占用的存储空间较少,但是处理效率也较低。在v6和v7架构中,打包和接包指令以及S IM D操作一定程序上对此有些帮助,但是要注意,在主程序中无法从C访问这些指令。
编写循环时要当心
可以按照以下一些简单的规则来编写循环:使用无符号的整数计数器,向下倒数,并把是否等于零作为终止条件。这可以让循环更短,速度更快,使用的寄存器更少。还要记住,要采用矢量化来编写循环。即使在尝试展开和矢量化最简单的循环时,有关控制结构和数据声明的一些简单规则都可以让编译器的作业变得更简单。
图5:循环展开
图5显示了与一个特定循环优化有关的一些数据,这个循环优化就是循环展开(Brooks,2000)。按照预期,随着展开因子的增加,执行时间和指令数目会减少。我们看到了减少循环开销和减少地址计算的效果。功率结果更加有趣,但不太明显。因为预测器可用来训练其行为的分支更少且针对循环结束失败的最终错误预测比例大增,所以随着循环进一步展开,分支预测器的准确性出现下降。但是,因为顺序取指的连续数据流不经常被中断,所以取指阶段的效率可以提升。组合的结果是减少了每条指令的净能耗。
因此尽管执行时间基本上低于展开因子4,但是因为功耗持续降低,所以所有重要的功耗时间积也随之降低。因此有能耗意识的编译器或开发人员与只考虑执行时间的编译器或开发人员相比,会更倾向于展开循环。
精度满足需求即可
还必须考虑输出要求的精度。即使有浮点硬件可用,定点实现的计算通常比浮点实现的计算更有效率。如果您正在渲染一个供屏幕查看的图像,可能并不需要完全符合标准,您只需要渲染出可以接受的图像。
对标准M P E G- 4解码函数进行递进优化的一项研究(S h i n,2002)已经表明,把软浮点切换为定点二进制可以把能耗降低72%。精度损失意味着该结果不再符合标准,但是在所研究的系统上仍然足以满足渲染用途。
关于Thumb
T humb指令集专门设计用于改进代码密度,还可以提升窄内存系统的性能。但是,在代码密度确实改进的同时,指令数也同时增加了。这是因为,与A R M指令相比,减少了个别Thumb指令的功能。因此Thumb重新编译会造成能耗增加,这看起来是合理的,而我们看到的事实也的确是这样。
上述研究表明,如果代码大小减少4%,指令执行数增加38%,而能耗增加28%。为了找到第三头大象,我们需要走出处理器及其内存的领域,着眼于范围更大的系统。我们这些天使用的系统已经被我们的硬件设计同事组合到了一起,这个系统提供了大量节能选项。
更广系统中的节能
显而易见,没有使用的组件应尽可能置于低功耗状态。这也是所有敏锐的设计系统不可分割的组成部分,这些组件应包括内存和缓存系统、甚至是处理器本身。在多核系统中,我们必须考虑在处理要求相对低时中止一个或多个内核运行的可能性。
首先,一个很小但值得考虑的问题是:处理外设时,要始终尝试使用中断机制,而不是轮询机制。轮询循环只会耗用能量而无任何目的。几乎所有架构均包括了某种等待中断的指令,可以把这种情况下的系统置于待机状态。对于A R M系统,内核通常带有时钟门控,只保留静态漏电。
通过设计中断架构来增加拖尾连锁,一般可以避免不必要的睡眠唤醒循环。ARM Cortex-M3架构可以自动实现这一点。
对于个别计算单元,选择一种关机方案是很容易的。对于可以预测需要与否的单元,在不需要时通过应用程序或操作系统就可以停止运行这些单元。对于不可预测需要与否的单元,可以通过按需通电启动系统,也可以在空闲超过某段时间之后再自动断电。子系统断电的时标可以通过以下两方面来得出:通电但处于空闲状态时的功耗以及睡眠唤醒循环的能耗。从根本上讲,这取决于应用场合。但是,动力循环代码的简单循环计数将是最明显的起点。
测量数据表明,N e o n引擎相比C o r tex-A9等内核的运行功率要高大约10%。但是,对于传统的信号处理算法,其性能提升了40% - 150%。在任务期间启用NeON而在不需要时切断Neon的好处很明显。很常见的是,不但N e o n引擎可以在任务完成时切断,而且整个处理器系统可以节省更多功耗。
通常一个比较难的选择是启用计算组件提前完成计算(且因此切断时间更长)还是完成计算时及时降低处理器速度以减少功耗。图6显示了每次迭代的能耗数据,这是简单的基准(Domeika, 2009)。采用不同的指令缓存和浮点协处理器组合,对于两个时钟速度,每个时钟速度均要运行此基准四次。有两个清晰的重点。首先,尽管指令缓存和浮点单元均减少了能耗,但是浮点单元比指令缓存的表现更优异。
图6:系统组件电源利用
其次,对于所有配置而言,每次迭代的能耗从本质上讲是相同的,与时钟速度无关。因此,为了更快地完成任务而启用所有功能并全速运行要比调慢时钟速度更加高效。
多重处理
众所周知,与调高单核的功率相比,采用多核可以获得更高的性能和更好的能效。使用多核系统时,我们必须考虑在不需要时选择中止一个或多个内核。A R M的研究表明,S M PLinux系统中单核循环的成本是50000个周期(大部分周期用于清除一级缓存),这意味着此操作将在几百毫秒内完成,而不是更短的时间内完成,否则其能耗成本将超过其具有的优势。
ARM是主动研究型架构,其包含两个内核,一个高性能的内核用于全功能操作,一个较小的配套内核则以较低的性能完成低功率操作。需要较高的处理功率时,系统运行较大的内核。任务完成时,系统可以把所有信息传递给小内核并关闭大内核。需要逆向信息移动时,再切换回大内核。如果这两个内核相连成为相关系统,则切换的能耗成本可降到最低。
关于操作系统
不巧的是,在操作系统上运行时,应用程序员无法这样灵活处理。缓存配置、S PM使用与否、组件的电源周期等很大程度上都是操作系统架构和设备驱动程序来专门决定的。但是,应用程序员仍然有很多东西需要考虑。
研究已经表明,设计不良的进程间通信(I P C)会大大增加系统的能耗。一个简单的技术称为“矢量化”进程间通信,这种技术批量处理小的消息并把大量小的消息作为一个大的消息来发送,这样通常可以减少上下文的切换开销。另外,减少进程数量可明显降低进程间通信的需求。需要频繁通信的进程可以合并成一个进程。
在嵌入式Linux中运行的最近研究(Tan, 2003)表明,分析和合理设计进程间通信2 可以潜在改进能耗多达60%。
结论
尽管我已经强调了许多领域仍然属于学术研究范围,但我们现在还是可以做许多工作。结果相对比较简单:减少外部内存存取,减少指令执行,并在不使用某些单元时关掉它们。
在做出这个结论的同时,我想起了2 0 0 9年中期在一次培训课上与客户的一次谈话。这些客户关心如何在包含了N e o n的C or tex-A8平台上实现信号处理算法且想要知道个别指令的确切能耗。我解释说,实际上很多这种信息是未知的,而且不管在什么情况下,很难使用当前的工具得出这些信息。回顾上文,我们已经认识到在捕猎大象的这个长期任务中,这些信息都是无关紧要的。实际上,客户要捕猎的大象与房间中的其他大象相比,非常的小。不管是经过分析还是持续跟踪数据,都可以得出更好的建议,那就是估算每次实现中涉及的数据存取次数和类型。这样,再结合指令计数,可以做出更加明智的选择。与内存存取布置不良相比,个别指令的功耗几乎无关紧要。
我们这些软件开发人员要继续对学术和工具供应商施加压力,让他们在下一代工具中构建出这些功能。这不容易但将会实现。
最后,我必须提醒大家所有这些取决于您所用的系统、平台、应用程序、操作系统、电池和用户。就像俗话说的“优势各有不同”。
更广系统中的节能
显而易见,没有使用的组件应尽可能置于低功耗状态。这也是所有敏锐的设计系统不可分割的组成部分,这些组件应包括内存和缓存系统、甚至是处理器本身。在多核系统中,我们必须考虑在处理要求相对低时中止一个或多个内核运行的可能性。
首先,一个很小但值得考虑的问题是:处理外设时,要始终尝试使用中断机制,而不是轮询机制。轮询循环只会耗用能量而无任何目的。几乎所有架构均包括了某种等待中断的指令,可以把这种情况下的系统置于待机状态。对于A R M系统,内核通常带有时钟门控,只保留静态漏电。
通过设计中断架构来增加拖尾连锁,一般可以避免不必要的睡眠唤醒循环。ARM Cortex-M3架构可以自动实现这一点。
对于个别计算单元,选择一种关机方案是很容易的。对于可以预测需要与否的单元,在不需要时通过应用程序或操作系统就可以停止运行这些单元。对于不可预测需要与否的单元,可以通过按需通电启动系统,也可以在空闲超过某段时间之后再自动断电。子系统断电的时标可以通过以下两方面来得出:通电但处于空闲状态时的功耗以及睡眠唤醒循环的能耗。从根本上讲,这取决于应用场合。但是,动力循环代码的简单循环计数将是最明显的起点。
测量数据表明,N e o n引擎相比C o r tex-A9等内核的运行功率要高大约10%。但是,对于传统的信号处理算法,其性能提升了40% - 150%。在任务期间启用NeON而在不需要时切断Neon的好处很明显。很常见的是,不但N e o n引擎可以在任务完成时切断,而且整个处理器系统可以节省更多功耗。
通常一个比较难的选择是启用计算组件提前完成计算(且因此切断时间更长)还是完成计算时及时降低处理器速度以减少功耗。图6显示了每次迭代的能耗数据,这是简单的基准(Domeika, 2009)。采用不同的指令缓存和浮点协处理器组合,对于两个时钟速度,每个时钟速度均要运行此基准四次。有两个清晰的重点。首先,尽管指令缓存和浮点单元均减少了能耗,但是浮点单元比指令缓存的表现更优异。
图6:系统组件电源利用
其次,对于所有配置而言,每次迭代的能耗从本质上讲是相同的,与时钟速度无关。因此,为了更快地完成任务而启用所有功能并全速运行要比调慢时钟速度更加高效。
多重处理
众所周知,与调高单核的功率相比,采用多核可以获得更高的性能和更好的能效。使用多核系统时,我们必须考虑在不需要时选择中止一个或多个内核。A R M的研究表明,S M PLinux系统中单核循环的成本是50000个周期(大部分周期用于清除一级缓存),这意味着此操作将在几百毫秒内完成,而不是更短的时间内完成,否则其能耗成本将超过其具有的优势。
ARM是主动研究型架构,其包含两个内核,一个高性能的内核用于全功能操作,一个较小的配套内核则以较低的性能完成低功率操作。需要较高的处理功率时,系统运行较大的内核。任务完成时,系统可以把所有信息传递给小内核并关闭大内核。需要逆向信息移动时,再切换回大内核。如果这两个内核相连成为相关系统,则切换的能耗成本可降到最低。
关于操作系统
不巧的是,在操作系统上运行时,应用程序员无法这样灵活处理。缓存配置、S PM使用与否、组件的电源周期等很大程度上都是操作系统架构和设备驱动程序来专门决定的。但是,应用程序员仍然有很多东西需要考虑。
研究已经表明,设计不良的进程间通信(I P C)会大大增加系统的能耗。一个简单的技术称为“矢量化”进程间通信,这种技术批量处理小的消息并把大量小的消息作为一个大的消息来发送,这样通常可以减少上下文的切换开销。另外,减少进程数量可明显降低进程间通信的需求。需要频繁通信的进程可以合并成一个进程。
在嵌入式Linux中运行的最近研究(Tan, 2003)表明,分析和合理设计进程间通信2 可以潜在改进能耗多达60%。
结论
尽管我已经强调了许多领域仍然属于学术研究范围,但我们现在还是可以做许多工作。结果相对比较简单:减少外部内存存取,减少指令执行,并在不使用某些单元时关掉它们。
在做出这个结论的同时,我想起了2 0 0 9年中期在一次培训课上与客户的一次谈话。这些客户关心如何在包含了N e o n的C or tex-A8平台上实现信号处理算法且想要知道个别指令的确切能耗。我解释说,实际上很多这种信息是未知的,而且不管在什么情况下,很难使用当前的工具得出这些信息。回顾上文,我们已经认识到在捕猎大象的这个长期任务中,这些信息都是无关紧要的。实际上,客户要捕猎的大象与房间中的其他大象相比,非常的小。不管是经过分析还是持续跟踪数据,都可以得出更好的建议,那就是估算每次实现中涉及的数据存取次数和类型。这样,再结合指令计数,可以做出更加明智的选择。与内存存取布置不良相比,个别指令的功耗几乎无关紧要。
我们这些软件开发人员要继续对学术和工具供应商施加压力,让他们在下一代工具中构建出这些功能。这不容易但将会实现。
最后,我必须提醒大家所有这些取决于您所用的系统、平台、应用程序、操作系统、电池和用户。就像俗话说的“优势各有不同”。