对齐操作和非对齐操作
操作是否对齐是一个简单而容易忽略的性能(有时是可靠性)问题。对齐主要是指读写操作不产生不必要地跨越存储设备上原生存储单元的访问,这里的存储单元说的是在访问路径上的任何设备,它可以是外存,也可以是内存,甚至是CPU附近或内建的快取缓存,等等。
在C/C++中,对于内存的访问多数是在编译时可以预测的。一般来说编译器会在编译过程中自动对数据结构进行补足(除非由于某种需要而指定了__packed),并对可能产生这类问题的冒险行为,例如将一个较短的数据类型的指针cast成一个较长数据类型的指针进行警告。因此,在希望有较高性能的硬件平台上,不对齐的内存字操作往往会导致硬件异常(Alpha、IA64、SPARC64等)。但是对于便宜的PC硬件(x86和amd64)来说,为了保证和先前硬件的兼容性,它们往往会选择默默承受这样的问题,并在CPU的microcode中将这类操作转换成两次读操作。
通过仔细地编写C代码,可以在某些情况下减少非对齐或比字长短的操作,许多C的字符串处理函数都可采用这种方法。例如,在实现 strlen(3) 函数时,可以采取下面的策略:
- 将指针ptr向前取整到整字边界bp;
- 判断bp开始的字中是否有纯0的字节,如果有,则从ptr开始扫描到bp所指下一字前的每个字节,若找到则返回值;
- 从bp下一字开始,均先判断整个字中是否有纯0字节;如果没有,直接访问下一个字。
上面只是关于 strlen(3) 采用的技巧的粗略介绍。当然,由于好的程序绝对不会将 strlen(3) 放在关键路径上,因此这个改进的现实意义并不太大。实际测试中,这个改进版本的 strlen(3) 平均比按字节比较的版本快5.2倍,而对非常短的字符串则只有最多16%左右的性能损失。
对于外围设备的操作对齐相对来说更复杂一些。例如,采用4k扇区的硬盘,或者采用RAID的磁盘阵列,其固件需要将写操作拆成和物理扇区或stripe同样的大小,将数据读出,然后再重写。这个过程比较耗时(实际测试显示,在 WD AV25 硬盘上,对齐的4k写操作和未对齐的4k写操作的IOPS可相差达60倍),对于RAID,如RAID5的情形,还可能进一步引发一致性问题。
要避免这类问题,唯一的办法是对存储格式进行合理的规划。例如,采用的记录尺寸应为块尺寸的整数倍(对于多数 Unix 文件系统来说这并不是问题),或采用日志的方式将写操作收集起来先行写入整个存储块,然后在提交阶段将写操作凑成整块来做。
不过需要注意的是,操作系统的存储驱动必须能够告诉文件系统如何对齐。FreeBSD中,这是通过g_provider对象的stripesize和stripeoffset属性来暴露给文件系统的。如果操作系统本身没有提供对齐支持,在分区时强制分区以整物理扇区,而不是整逻辑磁道(17个512 byte扇区)的位置开始,也可以在一定程度上缓解这个问题。