最常见的有两种 :1. Little endian:将低序字节存储在起始地址 43212. Big endian:将高序字节存储在起始地址 1234LE little-endian 最符合人的思维的字节序,地址低位存储值的低位 ,地址高位存储值的高位,怎么讲是最符合人的思维的字节序,是因为从人的第一观感来说低位值小,就应该放在内存地址小的地方,也即内存地址低位,反之,高位值就应该放在内存地址大的地方,也即内存地址高位。BE big-endian 最直观的字节序,地址低位存储值的高位 ,地址高位存储值的低位,为什么说直观,不要考虑对应关系,只需要把内存地址从左到右按照由低到高的顺序写出,把值按照通常的高位到低位的顺序写出两者对照,一个字节一个字节的填充进去 。例子:如果我们将0x1234abcd写入到以0x0000开始的内存中,则结果为
内存地址 big-endian little-endian0x0000 0x12 0xcd
0x0001 0x34 0xab
0x0002 0xab 0x34
0x0003 0xcd 0x12网络字节序
网络字节序是TCP/IP协议栈中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。网络字节顺序采用big endian排序方式。
为了进行转换 bsd socket提供了转换的函数 有下面四个:linux的源代码(/include/netinet/in.h)# if __BYTE_ORDER == __BIG_ENDIAN
/* The host byte order is the same as network byte order,
so these functions are all just identity. */
# define ntohl(x) (x)
# define ntohs(x) (x)
# define htonl(x) (x)
# define htons(x) (x)
# else
# if __BYTE_ORDER == __LITTLE_ENDIAN
# define ntohl(x) __bswap_32 (x)
# define ntohs(x) __bswap_16 (x)
# define htonl(x) __bswap_32 (x)
# define htons(x) __bswap_16 (x)
# endif
# endif
htons 把unsigned short类型从主机序转换到网络序
htonl 把unsigned long类型从主机序转换到网络序
ntohs 把unsigned short类型从网络序转换到主机序
ntohl 把unsigned long类型从网络序转换到主机序
在使用little endian的系统中这些函数会把字节序进行转换, 在使用big endian类型的系统中这些函数会定义成空,什么都不做。htonl(x)我简化为下: #define htonl(x) \ //连接符,连接下一行 ((unsigned long ) \( \(((unsigned long)(x)&0x000000ff<<24)| \(((unsigned long)(x)&0x0000ff00)<<8)| \(((unsigned long)(x)&0x00ff0000)>>8)| \(((unsigned long)(x)&0xff000000)>>24)\))一般c语言编写程序的字节序都是系统相关的,叫主机序,即指系统处理器本身所采用的字节序,java的字节码是big-endian 和网络字节序一样,所以他和网络通信不需要关心字节序问题,如果要和其他平台进行通信,都要进行字节序转换,一般采用标准化的网络序进行传输:发送端:主机序->网络序接收端: 网络序->主机序不管哪种字节序,目的都是大家对01二进制串解释都一样, 不然两方的解释不一样就可能会产生严重的问题。对于IP头定义里面ihl和version字段需要考虑主机字节序;__be 表示big endian网络上流传一个测试自己系统是什么字节序函数代码:byte_type get_sys_byte_order(){ union { int b; char a[4]; }U; U.b = 0x01; if(0x01 == U.a[0] ) { return little_endian_type; }else { return big_endian_type; }
} 注:不同的CPU上运行不同的操作系统,字节序也是不同的,参见下表:
处理器 | 操作系统 | 字节序 |
Alpha | 全部 | Little endian |
ARM | 全部 | Little endian |
Intelx86 | 全部 | Little endian |
AMD | 全部 | Little endian |
MIPS | NT | Little endian |
MIPS | UNIX | Big endian |
PowerPC | NT | Little endian |
PowerPC | 非NT | Big endian |
为什么不统一字节序?
1. 计算都是从低位开始的,因此计算机内部处理采用小端序,效率较高。2. 对于大端序,由于符号位在高位,因此对于数据正负或大小的判断也就方便许多;其次,大端序也更符合人类的阅读习惯。3. 由于大小端各有优劣,各个芯片厂商的坚持自己设计,字节序的问题也就一直没有统一。字节序总结-
不同处理器之间采用的字节序可能不同。
-
有些处理器的字节序是确定的,有些处理器的字节序是可配置的。
-
网络序一般统一为大端序。
-
数据从本地传输到网络,需要转换为网络序,接收到的网络数据需要转换为本地序后使用。
-
C提供了一组接口用于整型数据在本地序和网络序之间的转换。
-
多字节数据对象才需要转字节序,例如int,short等,而char不需要。
-
由于处理器是按照IEEE标准处理float和double的,因此也不需要转字节序。
-
由于Java虚拟机的存在,Java不需要考虑大小端的问题。
为什么要字节对齐
原因一: CPU读取内存效率各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。比如有些架构的CPU在访问一个没有进行对齐的变量的时候会发生错误(比如高通平台,一般的手机平台都采用美国高通公司开发平台,对于无线上网卡来说,现在都是多核,要么是arm9+arm11,要么是arm9+2个Qdsp(Q是表示高通的dsp处理器)等处理器架构,然而在Qdsp中,如果访问了非对齐的内存,就会直接发生错误,直接把系统crush掉)那么在这种架构下编程必须保证字节对齐。其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出这32bit。而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该32bit数据。显然在读取效率上下降很多。 原因二 : Cache亲和性结构的数据跨越两个cache line,就意味着两次load或者两次store。如果数据结构是cache line对齐的, 就有可能减少一次读写。掌握字节对齐的估算方法学会估算结构大小,可以帮助我们更好地设计程序的数据结构,比如合理地节约内存,提供数据访问效率等。1. 对于基本数据类型对齐要求:Char 类型一个字节对齐,可以从任何内存地址读取;Short2个字节,要求是从偶内存地址读取;Int 4个字节(32位系统),要求是变量的内存地址必须被4整除;Long和int一样(在32位系统中)Double 8个字节,要求是变量的内存地址必须被8整除。2. 对于struct和union对齐要求是:在c语言中存在struct和union结构体类型,属于复杂类型,成员中自身对齐值最大的那个值 。注:结构的总大小为结构的字节边界数(即该结构中占用最大空间的变量的类型所占用的字节数)的倍数, 对于结构体最终大小,还要参考,指定对齐值n。以下部分属于具体计算方法(有很多公式,但这些公式你可能从来没有看到过),不想看可以跳过这一节。structs表示结构体,假使结构体有m个成员, 定义A(x)表示第x个成员的对齐值, X(i)表示其第i个成员(按顺序从上往下)所占的大小, H(x)表示前面x个成员最终占内存大小, 则结构体的大小可以通过下面公式估算出来:初始值--基本类型type对齐值: A(char) = 1; A(short) = 2; A(int) = 4; A(long) = 4; A(float) = 4; A(double) = 8;基本类型成员对齐值,type为基本类型成员x对应的基本类型:第x个成员对齐值:A(x) = min(A(type),n ); 公式1整个结构对齐值:A(s) = min(max(A(1),A(2),...,A(m)),n) 公式2则struct结构体前x+1个成员和前x成员之间计算公式:If(H(x)%A(x+1) == 0) H(x+1)= H(x)+X(x+1);Else H(x+1)= H(x)+A(x+1)-H(x)%A(x+1)+X(x+1); 其中 H(1) = X(1),A(1)= X(1); 公式3则结构体最终结构体大小X(s)为:If(H(m)%A(s) == 0) X(s) = H(m);Else X(s) = H(m)+A(s)-H(m)%A(s); 公式4 unionU表示这个结构体,X(i)表示其第i个成员(按顺序从上往下)所占的大小;假使结构体有m个成员;结构体的最终对齐值 A(u) = min(max(A(1),A(2),...,A(m)),n) 公式5定义H(x)表示前面x个成员实际占内存大小;A(x)表示第x个成员的对齐值,可以有公式1给出, Union结构体最终占内存大小为X(u), 则:则union结构体前x+1个成员和前x成员之间计算公式:H(x+1)= max(X(x+1), H(x));其中 H(1) = X(1); 公式6则最终union结构体大小X(u)为:If(H(m)%A(u) == 0) X(u) = H(m);Else X(u) = H(m)+A(u)-H(m)%A(u); 公式7 公式4和公式7一般实现:指定对齐值VC/VS编译器如果我们想指定对齐值,可以在VC IDE中,可以这样修改:[Project]|[Settings],c/c++选项卡Category的Code Generation选项的Struct Member Alignment中修改,默认是8字节,针对全部变量,如果想动态改变部分,在vc中可以用宏命令 #pragma pack (n)时的指定对齐值n#pragma pack()取消,之间的数据都是指定为n,但不一定为对齐n。最终的数据成员对齐值为: 自身对齐值和指定对齐值中小的那个值。GCC编译器__attribute__((aligned(n))) 表示所定义的变量为n字节对齐。__attribute__((__packed__)) 表示结构体内不填充多余的字节,一般用于通信协议结构体定义;__attribute__((__aligned__(SMP_CACHE_BYTES)))表示结构按cache对齐,提高数据访问效率。结构中带有结构不必考虑整个子结构,只考虑子结构的基本类型并参照前面的规则来分配空间。空结构(即不带任何的方法和数据)占用的1字节的空间。枚举中(enum)枚举始终占用4字节的空间。结构中成员结构的静态成员不对结构的大小产生影响,因为静态变量的存储位置与结构的实例地址无关,要理解上面的对齐规则,最好是分析一些典型的对齐例子:例子1:struct MyStruct {
char dda;
double dda1;
int type }; 默认指定对齐值n = 8(vc),其他自己查看,则n = 8;由公式1 得到此结构体的最终对齐值为 A(s) = 8有上面公式2 ,3 可以得到 X(MyStruct)(=sizeof(MyStruct)) :H(1) = 1, H(2) =(H(1)) 1+7+8 = 16; H(3) =(H(2)) 16+4 = 20;X(s) = (H(3))20 + (A(s)-H(3)%A(s)) 4 = 24;所以sizeof(MyStruct) = 24;例子2:#pragma pack(2)struct MyStruct {
char dda; A(1) = 1
double dda1; A(2) = 2
int type ; A(3) = 2
}; #pragma pack()由公式1,2得到此结构体的最终对齐值为 A(s) = 2有上面公式3 ,4 可以得到 X(MyStruct)(sizeof(MyStruct)) :H(1) = 1, H(2) =(H(1)) 1+ (A(2)-H(1)%A(2)) 1+(X2)8 = 10; 因为H(2)%A(3)==0;所以 H(3) =(H(2)) 10+4 = 14;因为H(3)%A(s) == 0;所以X(s) = (H(3)) 14;所以sizeof(MyStruct) = 14;例子3:这里有个结构体嵌套例子,对于结构体中的结构体成员,不要认为它的对齐方式就是他的大小,看下面的例子:struct s1{
char a[8];
};
struct s2{
double d;
};
struct s3{
s1 s;
char a;
};
struct s4{
s2 s;
char a;
};
默认指定对齐值n = 8;A(s1) = 1;A(s2) =min(min(A(double), 8),8) =8 ; A(s3) = min(max(A(x1)=min(A(s1) = 1,8) = 1,A(x2) = 1),8) = 1;A(s4) = min(max(A(x1)=min(A(s2) = 8,8) = 8,A(x2) = 1),8) = 8;X(s1) = 8;X(s2) = 8;X(s3) = 9;X(s4) = 16;以上只是一些测试例子,真实的结构体都比较庞大,一般用sizeof就可以了,但心里要清楚,每个成员的偏移量和填充的字节,这些都可以由上面的公式推出来(平时最应该注意的),我这里就暂时不推导了。字节对齐利弊1. 对齐意味着可能有内存浪费(特别是数组这样连续分配的数据结构),所以需要在空间和时间两方面权衡。2. 跨平台通信场景,每个平台程序字节对齐不一样,可能会导致内存空间解析出错,所以一般采用1字节对齐,中间不含填充数据,但这样会导致一些属性访问性能下降,需要综合考虑。字节对齐总结
-
结构体成员合理安排位置,节省空间,提高性能
-
跨平台数据结构可考虑1字节对齐,节省空间,解析安全,影响访问效率
-
跨平台数据结构进行结构优化(对齐填充),提高访问效率,解析风险,不节省空间
-
本地数据采用chace对齐,提高访问效率
-
32位与64位默认对齐数不一样
高通芯片平台的memcpy函数:技术: 内存对齐 循环展开 (局部性原理) 批量copyglibc库函数:技术: 内存对齐 批量copy DPDK库函数:技术: 指令预取 局部性原理 向量指令 cache对齐