王 江
(上海贝尔股份有限公司,上海201206)
一件优秀的电子产品往往是软硬件协同设计的产物。苹果公司的成功再一次证明了软硬件结合的重要性,他们花费了大量时间和精力来保证设备中软硬件的互补。竞争对手的手机和平板电脑在测评时会出现“笨拙”、“不直观”等描述性词汇,而苹果产品则经常会得到“浑然一体”等褒奖。
由于社会化大分工越来越细。在现实的研发过程中,软件设计人员一般偏重于应用与用户界面,不甚了解承载软件运行的硬件设计,有时一味地依赖提升处理器速度,增加内存容量,加快总线速度,来达到预期的性能。这不但增大了产品的功耗,而且增加了产品成本,实为下下之策。
如果我们在软件设计时考虑一下硬件因素,因地制宜,充分发挥硬件的优点,克服其缺点,扬长避短,那么就有可能做到软硬件的完美结合,从而提升产品的整体性能。文章将从处理器、缓存、内存、外围高速总线和外围低速总线等方面介绍笔者在这方面积累的一些经验。
处理器分为单内核处理器和多内核处理器两大类。两类处理器影响软件设计的共同点,首要因素是主频,其次是缓存的命中率。软件无法改变主频——除了那些少数可以动态调整主频的处理器,而缓存的命中率是软件设计需要考虑的重要因素。
一级缓存是离处理器内核最近的存储器,它的速度基本和处理器的主频一致。因为其价格昂贵,所以容量不大,一般为几十KB的数量级。一级缓存之外可能存在二级缓存和三级缓存,视处理器架构和硬件设计而定,它们的速度依次降低,容量依次变大;而外部存储器,即通常所说的内存,是最慢但容量最大的存储器,与一级缓存相比,其速度慢至少一个数量级以上,但是容量可以高达几十GB。
因此在程序设计时,常用的参数应尽量放在一个数据结构中,每个数据结构的大小尽可能控制在一个缓存行。对于常用的函数也一样,函数的代码尽可能短小。这样,就能增大常用的数据结构和函数驻留在缓存中的概率,程序的执行速度自然就快了。
图1 内存访问时延比较
在多核处理器中,每个内核拥有独立的一级缓存,共享二级缓存和外部存储器。由于缓存容量有限,软件运行时不得不频繁地读写外部存储器,引入比较大的时延。如图1所示,未涂色部分为时延,涂色部分为内存访问操作。在单核单发射处理器中(单发射意味着一次流水线周期只能执行一条指令),因为内核只有一个指令执行单元,所以读写数据引入的时延比较大。当执行单元增加到两个,即双发射处理器,两条指令可以并发执行,读写数据引入的时延大大减小。当执行单元再加倍,即4发射处理器,相比于双发射,读写数据的时延并没有明显的改善。此时,单纯依赖增加执行单元,受益不多。而增加处理器数目(如增至4内核),却能突破瓶颈,通过多内核交叉访问的方式可大幅度降低时延。
所以,为了充分利用多内核处理器的特点,软件应该避免将一个计算量大的任务绑定到一个内核上,尽量使用对称多处理操作系统,将计算量大的任务和访问内存频繁的任务尽可能平均分布到各个内核上。
同时,建议启用内存的交织(Interleave)模式,几乎可以使其吞吐量翻番。交织模式允许一路内存正在访问的同时,另一路内存刷新。实践表明,如果所有路内存的刷新周期都是交叉排列,将会产生一种流水线效应。否则,处理器必须等待第一个数据处理结束再刷新内存,才能发起下一次读写操作。
针对大数据量搬移、内存复制和内存设置等操作,尽可能选用DMA方式。例如,优化常用的内存复制库函数memcpy和内存设置库函数memset,如果操作的数量大于某个设定值时,采用DMA方式可以大大减轻处理器的负担。
此外,有些体系结构的处理器对代码有着特殊的要求。例如MIPS架构处理器[1],在其体系结构中,正常的加载和存储必须地址对齐:半字只能从双字节的边界加载;字只能从4字节的边界加载。一个非对齐地址的加载指令会导致自陷。所以,如果访问地址是依靠程序计算所得,那么就要格外小心。还有一种情况是,处理器与另一个智能芯片通过共享内存的方式进行通信,也有可能导致非对齐地址访问。一个简便的解决方法是,程序中增加对地址合法性的检测。
目前,芯片间常用的高速互连总线包括PCI、PCI-X、PCIe、HyperTransport、RapidIO、SPI-4.2、以 太 网 (FE/GE/10GE)等。其中,PCIe和以太网是应用最广泛的两类总线,以下将分别介绍它们对程序设计的影响。
PCIe总线是一类高速串行总线,最新的 PCIe 3.0[2]标准的信号频率可达8.0GHz,PCIe 1.0标准的信号速率也可达2.5GHz。PCI的频率达到66MHz。虽然PCIe和它的先驱PCI、PCI-X在硬件上完全不同,但是因为PCIe的驱动程序向前兼容,而且具有速度更快、连线更少的优势,所以PCIe快速地取代了PCI和PCI-X。
PCIe总线对软件设计的影响主要有三点。
第一,小 端 (Little-Endian)模 式 是 PCIe/PCI-X/PCI标准规定的模式,即存储数据时,数据的低字节存放在低地址,传送数据时,数据的低字节先传送。如果PCIe两侧器件的模式不同,一侧是大端模式,另一侧是小端模式,那么,就会引入字节翻转问题。例如,一侧写入“0x11223344”,另一侧读到的却是“0x44332211”。从软件角度,解决这个问题的方法往往是写一个宏,在一侧把字节翻转过来,这样会耗费大量的CPU周期。其实,目前的大部分PCIe接口芯片都有字节顺序倒换的功能,硬件可以根据设置自动完成字节顺序倒换,从而极大提升软件的效率。
第二,负荷单元大小。PCIe标准规定TLP(协议层)报文的数据有效负载的最大值为4KB,具体器件定义了“最大负荷长度”(Max_Payload_Size)和“能支持的最大负荷长度”(Max_Payload_Size_Supported)两个参数。Max_Payload_Size_Supported由PCIe芯片的硬件逻辑决定,一般为只读;而Max_Payload_Size由软件设定(芯片的默认值通常是最小值:128字节),显然其数值不能大于 Max_Payload_Size_Supported。
PCIe器件发送数据报文时,使用 Max_Payload_Size参数决定TLP的最大有效负载。当PCIe器件所传送的数据大小超过 Max_Payload_Size参数时,这段数据将被分割为多个TLP进行发送。在接收侧,接收到的TLP的最大有效负载也不能超过接收器件的Max_Payload_Size参数。如果接收的TLP,其长度字段超过Max_Payload_Size参数,接收器件将认为该TLP非法。因此,Max_Payload_Size越大,PCIe总线的利用效率就越高。但是,收发两侧要协商,设置一个两侧都能够接受的最大值。
因为PCIe的驱动程序兼容PCI/PCI-X,所以许多PCI/PCI-X的驱动程序在新的PCIe器件上能够正常运行,但是不能充分发挥PCIe的吞吐能力。合理设置Max_Payload_Size参数,可以提高PCIe的利用效率,从而提升软件性能。
第三,共享内存的一致性。在PCIe架构中,主从设备之间大数据量的通信一般用共享内存的方式。以集成PCIe接口的网络芯片为例,它工作于从模式,处理器工作于主模式。处理器在其外部存储器中开辟了一段空间允许从设备读写,这片空间就是共享内存。当发送报文时,网络芯片会从共享内存中读取数据,组包后发送出去。当接收报文时,它会把报文数据写到共享内存中。通常把这片共享内存的属性设置为非缓存模式(Un-Cache),这样肯定能确保其一致性。然而,有些处理器能够从硬件层面确保共享内存的一致性,例如,博通公司的BCM1250、BCM1480多核处理器。此时可以将共享内存的属性设置为缓存模式(Cache),性能将大幅度提高。所以,当碰到共享内存的一致性问题时,需要仔细阅读相关的芯片资料,才能充分发挥硬件的长处。
以太网包含10兆、百兆、千兆、10千兆等接口。因为以太网是以大端方式传输数据,而本地处理器可能工作于小端模式,为了保证数据的一致性,就要把本地的数据转换成网络上使用的格式,然后发送出去。接收的时候也一样,经过转换后这些数据才能使用。一般利用基本的库函数,例如htons()、htonl()、ntohs()和ntohl()等进行字节转换。
所以,如果产品是面向以太网应用的,需要频繁处理以太网报文,那么选择处理器工作在大端模式是明智之举。
相比高速总线,低速总线对系统性能的影响更加明显。目前常用的外围低速总线包括UART(串口)、I2C总线、SMBus、SPI、MDIO等。它们的速度,低则几kHz,高则几十MHz。针对低速及各种协议的特点,软件设计需要考虑以下因素。
毫无疑问,系统越少访问低速外设,性能就越高。那么,如何减少访问低速外设的次数呢?方法有三种:一是使用缓存;二是中断方式;三是充分利用器件特性,推出个性化服务。
① 使用缓存是一种以空间换时间的方法。一些板卡信息例如生产日期、序列号等往往存储在非易失性介质(如EEPROM)中。软件只要在系统初始化时读取这些信息,保存在全局变量中,今后就不用访问低速外设了,只需访问这些全局变量即可。类似的信息还有槽位号、机箱号等,它们的共同特点是其内容在使用过程中保持不变。缓存的另一个作用是当处理器往低速外设写数据时,先写到缓存中,然后由一个优先级低的任务把缓存中的数据写到外设,这样就不会影响高优先级任务的执行。
② 中断方式。众所周知,中断方式能够将处理器从繁重的外设轮询任务中解放出来。例如,温度告警,常见的温度监测芯片,如LM75、ADT7411等,都集成I2C总线接口,都可以设置监测温度的上下门限,如果温度超过门限值,那么就触发中断。再如有些I/O扩展芯片,如PCA9555,一种具有I2C总线接口和16个I/O端口的芯片,如果I/O端口电平发生变化,则会触发中断。
③充分利用器件特性,可以使软件性能事半功倍。例如,很多具有I2C总线接口的器件,如EEPROM,读写单个数据和多个数据的操作方法不同。如图2所示,当写单个数据时,处理器先发送器件地址和写命令,接着发送寄存器地址,最后发送待写数据。如果写多个数据且它们的地址连续,那么写完第1个数据后,可以紧接着发送第2个数据、第3个数据……直到全部写完,寄存器地址会自动增加。不了解芯片特性的程序员往往在写单个数据的函数的基础上,多次调用该函数,来实现多个数据的写操作,事倍功半。读操作的情况类似,如图3所示。由此可见,用连续读写多个数据的方式,效率几乎可以翻倍。
图2 I 2 C总线器件写数据操作
图3 I 2 C总线器件读数据操作
访问低速总线连接的外设通常不是原子操作,即一次访问需要耗费多条机器码,例如SPI总线、SMBus总线、图2~3所示的I2C总线[3]操作等。它们的特点是每次访问过程禁止被中断,或在同一条总线上交叉进行另一次操作,有些总线甚至有超时规定,例如SMBus有25ms的超时限制。在多任务抢占式操作系统中,意外难免发生,低优先级的任务可能被高优先级任务抢占,正在进行的读/写操作可能突然被一个中断破坏,然后插入另一个读/写操作等。
为了保证单次读/写操作的可靠性,必须对读/写函数设立临界区——在任何给定时间只有一个线程可以执行的代码,用互斥锁等机制保护起来;同时,避免在中断服务程序中访问低速外设,建议采用延后访问或者工作队列的方式。
对于低速外设,建立良好的软件模型可以帮助提升产品整体性能,反之则成为系统前行的绊脚石。特别在事件驱动的系统中,千万记得不能让处理器停下来等待事件的发生。例如,IPMI(智能平台管理系统)可以运行在UART、SMBus等多种总线上。如下代码是初学者易犯的错误之一,编程思路简单,但执行效率低。它首先等待输入,根据输入执行相关操作,再等待操作的结果执行下一步操作,等待过程中浪费了大量的处理器周期,降低了整个系统性能。
根据低速外设的特点改进软件模型可以提升产品性能,下面的代码,建立专门的线程、任务或中断服务程序来接收外部数据,根据注册函数的不同,调用对应的数据处理程序,再根据数据处理的结果调用相应的处理函数。上一个软件模型相当于硬件中的同步系统,而改进的模型相当于异步系统,更符合事件驱动的特点。
当前的电子产品、通信设备大部分是软硬件结合的产物,此乃大势所趋。未来最成功的公司将把优秀的软件镶嵌在独特的硬件上,达到浑然一体的效果。文章从处理器、存储器、高速总线、低速总线等方面介绍了笔者在开发过程中积累的一点经验,而要真正做到软硬件的完美结合,除了经验之外,还需投入大量时间和精力,分析用户需求,根据需求选择最合适的硬件,深刻理解硬件芯片的特性,建立高效的软件模型,以及编写优秀的代码。
[1]MIPS Technologies.MIPS Architecture For Programmers Revision 2.50,2005.
[2]PCISIG.PCI Express Base Specification Revision 3.0,2008.
[3]PHILIPS.The I2C-Bus Specification Version 2.1,2000.