关于自然对齐
1. 首先什么是cache line? cache line就是处理器从RAM load/store数据到CPU cache所使用的数据线。
2. 什么是对齐:有些人说,address of data % sizeof(data type)==0 就是对齐,其实这不准确,全面的理解它必须从CPU为什么需要对齐,不对齐就会造成数据访问花费额外的时钟周期,和额外的指令(编译器或OS附加的),并且数据经过更长的路径,比如pipeline才能到达CPU(从RAM)。这就是对齐问题。这里的重点是 数据的起始地址 & 数据的大小。
P4极其之后的处理器,cache line(缓存线)宽度是64字节,
1. 内存的访问如果没有按照64字节边界对齐(例如:每次访问 -读/写在Addr%64!=0就是没对齐),就可能会造成两次cache line split内存访问,和多个upos的执行。
所以一个原则就是,总是对齐数据在操作数大小的地址边界(按照操作数自身的大小进行对齐),如果内存被访问通过矢量指令(解指针引用的内存访问,如, mov ecx, DWORD PTR [ebx], 或 st4 [r8]=r9 (这是IA64指令)。这样的访问不会超过16字节(80位浮点数,或128位long long,在64位系统),那么在16字节边界对齐总是没问题的。是否所有长度的数据都要对齐在16字节,如果,char 和char连着排放,那么岂不是要加15个padding字节?太过浪费,所以,用数学的办法,只要能保证对齐在16字节边界就行。
所以引入自然对齐的概念:(对于下面的,可以这样来理解:按照这些规则进行对齐的话,就可以保证内存的一次访问都在64字节的cache line的宽度之内)
a. 1字节数据可以在任何位置开始访问(对齐在任何地址)
b. 2字节数据的开始地址必须包含在一个对齐的4字节之内。
c. 4字节数据的开始地址是4的倍数,(按4字节对齐)
d. 8字节数据的开始地址是8的倍数,(按8字节对齐)
e. 10字节浮点数据的开始地址是16的倍数,(按16字节对齐)
f. 16字节数据的开始地址是16的倍数,(按16字节对齐)
整数的访问不能跨越8字节边界,就是说整数必须是<=8字节进行对齐(1,2,4,8)
任何数据类型访问不能跨越16字节边界,就是说必须是<=16字节进行对齐(1,2,4,8,16)
一个64字节或更大的数据结构或数组的开始地址应该按照64字节边界对齐,数据成员按照每个成员的size从大到小排列,是一个有助于自然对齐的启发性方法。只要按照上面的自然对齐,就可以容易的保证一次数据访问不穿越16/64字节边界。反过来,只要能保证不穿越16/64字节边界,就不需要使用自然对齐。(通常我们没法保证访问数据不穿越16/64字节边界,内存地址分配不由我们控制)
2. 一次访问如果穿越64字节边界可能极大降低性能,pipelines(管线)越长,代价越大。
3. 双精度浮点数作为操作数时,8字节对齐的访问比非8字节对齐有更好的性能。
Itanium 2的cache组织:
处理器高速缓存被组织为一个表,行对应于cache line中的数据,内存中的数据将被存储于哪一行由内存单元地址的最末6位决定(处理器简单的将这6位归零后,作为行索引),为什么是6位呢?因为Itanium 2 L1D 的cache line的宽度是64字节,也就是说一个cache line一次可以访问(load/store)64字节的内存单位,对于地址就是低6位不变,第7位+1的地址范围的数据。每64字节数据,按照同一列,新行顺序存放,这个表的列叫做"associativity"。一般来说,因为cache不足以存放所有的数据,新数据请求将会导致覆盖旧数据,为了高效管理cache,每一列的所有行组成"associative"集。列数越多,替换的准确性越高(可以理解为有更少的可能性替换当前正在使用的数据),越有效。Itanium 2 L1有4-Way associative set, 每way 64字节line宽度,所以行数就是(cache size=16KB)/(4*64)=64行,所以,内存地址的从低到高的第7位开始,到第12位,用于索引行,用于访问数据。所以Itanium 2的每way(associative set,每列)有64个cache line,并且涵盖4K字节数据,所以访问不穿越4K边界的数据,将会保证在一个associative set中,将会极大提高内存访问效率。见下图:
不得不说,处理器的这种设计,4Kbytes/1way,不正好符合OS虚拟内存管理中,页大小为4K。这样OS的每次虚拟内存访问,和处理器对内存的访问正好是一致的
,RAM<->Cache 都是4K。
关于Itanium2的数据对齐要求
Itanium2也遵循上面的自然对齐方式,如果(Processor Status Register)PSR.ac=1,所有的未对齐的内存访问将会引发UDRf(Unaligned Data Reference fault),Unix(如linux ia64, and HPUX ia64),将会表现为sig 10, bus error。IA32模式将会表现为IA_32_Exception(AlignmentCheck) fault,对齐的内存访问则不会。 如果PSR.ac=0, 则访问未对齐的数据,可能会也可能不会引发UDRf。这是执行的自定行为。(注:这里的执行表示OS)
1. 如果数据单元访问跨越4K边界,那么一定会引发UDRf。这是必须行为。
2. 某些执行将会在跨越cache line边界(64字节)时引发UDRf。这是执行的自定行为。
3. 执行可以自定在某种未自然对齐的情况下,引发UDRf。
4. 如果ia32模式,PSR.ac=0,并且Alignment Check被禁用,那么任何未对齐都会被忽略。不会有UDRf。
semaphore指令从不考虑PSR.ac标志,只要未对齐引用发生,就会引发UDRf。对于cmp8xchg16指令,必须是8字节对齐
HPUX如何处理对齐问题:
(一般编译器会默认按照 min(8字节,数据类型),即自然对齐的方式,处理对齐问题,分配到指定的内存边界)
1. 编译器指令:#pragma align N (N=1,2,4,8,16)
该指令影响随后的一条定义语句,如char a; 将导致a 分配在在N字节边界地址
2. #pragma pack N 将影响范围扩大到从它开始到文件结束。再用一次不带参数#pragma pack,将复位到默认对齐。
3. #pragma unalign N将影响随后的一个typedef,如 typedef int int_align_n; 将导致之后用int_align_n定义的数据对齐在N字节边界。
4. 取某数据地址,然后运行时cast后付给一个指针变量,将会导致未对齐的访问。因为编译器没有为这种情况使用特定的非对齐访问指令。
前面这些指令都会导致数据的存储变成非自然对齐,访问时就可能会引发UDRf或Alignment check failed. 可能会导致程序终止用bus error。视OS平台/执行不同而不同(看上面这段有描述)。
如何避免非对齐的数据访问crash呢?
1. 不要使用非自然对齐的与编译指令,或指针dereference.
2. HPUX aCC编译器+unum(如+u1)可以让编译器产生非对齐,1字节进行dereference load/store的指令,避免UDRf或Alignment check failed错。但会显著降低程序效率。
3. HPUX aCC链接器-lunalign (ia64架构)选项或-lhppa (pa-risc架构)。它会链接一段代码,包含一个函数allow_unaligned_data_access(),调用这个函数将安装一个sig handler,处理sig 10(其实安全的忽略它,访问数据没有问题),降低效率是必然的。
自然对齐的方式最容易被碰到,在HPUX IA64平台。莫名其妙你的程序会bus error 然后终止,就是说你该考虑一下你的数据对齐问题了。:-)