Sharpmark's Personal Home Page

数据对齐

看《编程卓越之道Write Great Code》的时候,在看数据总线的时候,明白了一些内存访问的基本原理,而最近在做SIMPLE,正好可以利用内存访问的一个技巧来优化一下我的程序。这 个技巧就是”数据对齐”。顺便拿出来分享一下。我会简单说一下数据对齐的由来,原因和应用方法。

首先说数据对其这种说法怎么来的。我以使 用16位(16-bit)数据总线的处理器来举例。32位的原理相同。处理器将16位的数据总线分为两部分d0-d7为低字节,d8-d15为高字节。相 应将内存分为对应两列。偶地址的接在d0-d7数据线上,奇地址的接在d8-d15位上。这样处理器就可以任意访问一个偶地址的数据,或一个奇地址的数 据,或者一个以偶地址开头的字(Word, 1word == 2byte)。
然而,例如在80×86系列的16位处理器上,它允许从任意地址读 取一个字。处理器从指定的地址取得低端字节,而从下一个地址获得高端字节。如果低端地址是偶地址的话,没有什么问题。但是,如果是奇地址的话,那么数据的 高端字节就需要到下一个字去获得了。例如希望从地址21获取一个字。这个数据位置就是在21, 22两个字节。但是要注意,处理只能将偶地址作为低位放入地址总线,所以处理器首先通过访问地址为20, 21的字节,获得一个字,然后再访问下一个字,获得地址为22, 23的两个字节。并在内部将地址21和地址22的数据交换位置,通过数据总线传递给处理器。
这在大规模访问连续数据的时候就会产生问题(当然其他 时候也可能会有)。例如,一个图像数据顺序保存在内存中,假设每个象素是两字节,数据的起始字节是奇数。并假设处理器没有做任何优化,没有缓存,那么每个 象素的读取都会有两次的内存访问,一次的高低字节数据交换。一个800*600的图像就会有480000次的多余内存访问。这个效率会差很多。所以要想办 法解决。

这里我介绍两种需要手动设置数据的对齐的情况。
1. 在一个大的数据块的开始,如果数据没有对齐,那么后面的数据都回相应错位。设置数据开始部分对齐的方法如下:(从SIMPLE中摘录,为了便于阅读,有改 动,另由于blogger对尖括号的支持不够好,所以下面的代码中可能使用全角的尖括号代替半角尖括号,使用时请自行替换。)

[code:c]< T* > alignPointer(void * raw, int align)
{
T*p = reinterpret_cast<T*>(
(reinterpret_cast<uintptr_t>(raw) + align - 1)
&~(align - 1);
return p;
}[/code]

解释一下,假设我们需要N个字节的数据,并以align对齐。(align==0,1的时候不对齐, 2的时候两字节对齐,以此类推,align为2的幂。)。void * raw是申请的空间,大小为N + align – 1个空间。多余的align – 1个空间是为了对齐而预留的。T类型是指定的每个元素的类型。

首先,通过reinterpret_cast<uintptr_t>(raw)将raw转化成uintptr_t类型,(uintptr_t是一种足够大从而可以保存指针的类型,如果没有定义uintptr_t的系统,可以使用typedef int uintptr_t来自行定义)。
结果加align – 1,相当于这个元素最后一个字节的位置。然后对~(align – 1)取与(and)运算。相当与计算出元素最后一个字节所在的内存的对齐列的第一个字节的地址。这么说有些晕,我来举个例子:例如分配n个字节,其中起始地址是7,对齐字节是4字节对齐。如果不对齐的话那么这个数据的第一个元素占用的就是7~10字节。如果对齐的话,首先7 + (4 – 1) = 10,计算出理论的最后一个字节的地址是10。然后10 & ~3。换算成二进制更好理解:1011 & 1100 = 1000,也就是十进制的8。所以起始的字节是从8开始的,那么地址是7的字节以及分配的最后两个字节会被忽略。再假设起始地址是4,那么计算后还是4,不用对齐。
2. 行对齐。在图像的内存表示中,除了开头的内存对其外,还有一个地方需要注意,就是行与行之间的对齐。例如一个RGB像素3字节,一个2*3的图像,再内存中的布局应该是:

0- 2为(0, 0) 3- 5为(1, 0) 6- 7空闲
8-10为(0, 1) 11-13为(1, 1) 14-15空闲
16-18为(0, 2) 19-21为(1, 2) 22-23空闲

图像的每一行都需要8个字节存储。不过其中仅有6个字节包含象素数据。前三个字节保存该行第一个象素的存储,其后3个字节保存下一个像素。为了另下一行从双字开始,在存储下一行的象素之前必须跳过两个字节。这里的做法跟上面的那种差不多,所以就不再举出代码了。

二进制数以及位运算

二进制数的一些特性

1、如果一个二进制数(整型)数的第零位的值是一,那么这个数就是奇数;而如果该位是零,那么这个数就是偶数。
2、如果一个二进制数的低端n位都是零,那么这个数可以被2n整除。
3、如果一个二进制数的第n位是一,而其他各位都是零,那么这个数等于2n。
4、如果一个二进制数的第零位到第n位(但不包含位n)都是一,而且其他各位都是零,那么这个数等于2n-1。
5、将一个二进制数的所有位左移移位的结果是将该数乘以二。
6、将一个无符号二进制数的所有位右移一位的结果等效于该数除以二(这对有符号数不适用)。余数会被下舍入(round down)
7、将两个n位的二进制数相成可能会需要2*n位来保存结果。
8、将两个n位的二进制数相加或者相减绝不会需要多于n+1位来保存结果。
9、将一个二进制数的所有位取反(就是将所有的一改为零,所有的零改为一)等效于将该数取负(改变符号)再将结果减一。
10、将任意给定个数的位表示的最大无符号二进制数加一的结果永远是零。
11、零递减(减一)的结果永远是某个给定个数的位表示的最大无符号二进制数。
12、n位可以表示2n个不同的组合。
13、数2年包含n位,所有位都是一。

有用的位运算

可以使用逐位与(and)运算来检测位串中的各个位是零还是一。例如:IsOdd = (ValueToTest & 1) != 0;

可以使用与运算来检测一组位是零/非零。例如IsDivisibleBy16 = (ValueToTest & 0xF) == 0;比较一个位串中的一组特定的位。(可能不连续)。

使用逻辑与创建模-n计数器(Modulo-n Counters)
cntr = (cntr + 1) & 0x3f; // 0x3f = 31. 这里实现的就是模-32计数器

主程序的异常处理结构

下面这段程序,实现了一个良好的main()函数结构。我这里解释一下他的功能和实现方法。
1、它处理整个程序的异常。整体框架被包括在一个try/catch结构里面。
2、它通过一个计数器restart,标记了之前运行得出错误数。这样在yourMain(int restart)中,就可以根据restart来判断这是第几次重新启动,并且可以做一些之前的异常情况的处理。
3、通过一个while来不断尝试重新开始程序。并通过标志位running来决定是否继续执行。
4、这里要注意几个问题,首先如果每次调用yourMain都会产生一个异常,这个结构就进入了死循环。你需要手动检查这是第几次运行,如果次数累计到一定程度,应该强制终止。
5、在捕获到异常,并重新调用yourMain的时候,全局或静态变量不会重新初始化。必须显式的重新初始化。

[code:c]
int yourMain(int restart)
{
// Your main function goes here.
}

int primaryMain()
{
int retval = 0;
bool running = true;
int restart = 0; // restart count
while(running)
{
try {
// Run the application
retval = yourMain(restart);

if (retval == 0) then running = false;
}
catch (std::exception& ex) {
// TODO: some error info ouput here.
restart++;
// Add code to decide if we should fail instead of restarting
}
catch(...) {
// TODO: some error info ouput here.
retval = 1;
running = false;
}
}

return retval;
}

int catchMain()
{
try {
// Run the application
return primaryMain();
}
catch(...) {
// TODO: some error info ouput here.
return 1;
}
return catchMain();
}

int main()
{
// TODO: set up something you need, like debug stream

return catchMain();
}[/code]