Posts 浅谈字节序和字节对齐
Post
Cancel

浅谈字节序和字节对齐

在做智能硬件App开发的过程中,手机端和蓝牙模块之间需要进行传输数据。蓝牙4.0低功耗模式数据传输速度很慢,以我们现在使用的蓝牙模块为例,传输速度大概只有大约1K/s,比起网络传输动辄几百K/s甚至上兆的速度真的是相差甚远。所以为了加快数据的传输速度唯一的办法就是减少要传输的数据的大小了。既然要减少数据量,那我们在网络请求中很常用的xml和json格式就不能用了。因为它里面有很多冗余的数据比如“{}[]”这一类的符号、字段的名称啊之类的信息。因此使用结构体作为数据的载体就是比较符合需求的方式了。

结构体和NSData互转

iOS的CoreBluetooth框架发送和接收数据都需要使用NSData对象,其实NSData对象和结构体之间很容易进行转换。

1
2
3
4
5
6
7
// 结构体 -> NSData
TestStruct test = {1, 2, 3, 4, 5};
NSData *data = [NSData dataWithBytes:&test length:sizeof(TestStruct)];

// NSDta -> 结构体
TestStruct test;
[data getBytes:&test length:sizeof(TestStruct)];

实际使用的过程中你会发现有时候数据并不对,这就涉及到下面要说的字节序和字节对齐的问题了。如果要保证结构体和NSData的互相转换能够成功,就要确保互相通信的各个平台的字节序和字节对齐方式要统一。

字节序

在几乎所有的机器上,多字节对象都被存储为连续的字节序列。例如在C语言中,一个类型为int的变量x地址为0x100,那么其对应地址表达式&x的值为0x100。且x的四个字节将被存储在存储器的0x100, 0x101, 0x102, 0x103位置。

而存储地址内的排列则有两个通用规则。一个多位的整数将按照其存储地址的最低或最高字节排列。如果最低有效位在最高有效位的前面,则称小端序(Little Endian);反之则称大端序(Big Endian)。在网络应用中,字节序是一个必须被考虑的因素,因为不同机器类型可能采用不同标准的字节序,所以均按照网络标准转化。

例如假设上述变量x类型为int,位于地址0x100处,它的十六进制为0x01234567,地址范围为0x100~0x103字节,其内部排列顺序依赖于机器的类型。大端法从首位开始将是:0x100: 01, 0x101: 23,..。而小端法将是:0x100: 67, 0x101: 45,..。

1
2
uint32_t a = 0x12345678;
NSLog(@"%@", [NSData dataWithBytes:&a length:sizeof(uint32_t)]);

由于Mac和iOS都是采用的小端序,所以下面的代码的输出结果为<78563412>

字节对齐

先来看一段代码

1
2
3
4
5
6
7
8
9
10
11
typedef struct {
    uint64_t a;
    uint8_t  b;
    uint32_t c;
    uint8_t  d;
    uint16_t e;
} TestStruct;

TestStruct test = {1, 2, 3, 4, 5};
NSUInteger length = sizeof(TestStruct);
NSLog(@"%ld %@", length, [NSData dataWithBytes:&test length:length]);

乍一看length不应该是8+1+4+1+2=16个字节吗?但是实际的输出的结果是这样的

24 <01000000 00000000 02000000 03000000 04000500 00000000>

这是为什么呢?这就不得不说一说字节对齐了。

为什么要进行字节对齐

在计算机中数据存储和传输以位(bit)为单位,每8个位bit组成1个字节(Byte)。32位计算机的字长为32位,即4个字节;对应的,64位计算机的字长为64位,即8个字节。计算机系统对基本类型数据在内存中存放的位置有限制,要求这些数据的起始地址的值是某个数k的倍数,这就是所谓的内存对齐,而这个k则被称为该数据类型的对齐模数(alignment modulus)。

各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。比如有些架构的CPU在访问 一个没有进行对齐的变量的时候会发生错误,那么在这种架构下编程必须保证字节对齐.其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对 数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那 么一个读周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该32bit数 据。显然在读取效率上下降很多。

如何对齐
  1. 结构体的起始位置的偏移量必须是能够被该结构体中最大的数据类型所整除。
  2. 每个数据成员存储的起始位置的偏移量是自身大小的整数倍(比如int在32位机为4字节,则int型成员要从4的整数倍地址开始存储)。
  3. 结构体总大小(也就是sizeof的结果),必须是该结构体成员中最大的对齐模数的整数倍。若不满足,会根据需要自动填充空缺的字节。
  4. 结构体包含另一个结构体成员,则被包含的结构体成员要从其原始结构体内部最大对齐模数的整数倍地址开始存储。(比如struct a里存有struct b,b里有char,int,double等元素,那b应该从8的整数倍开始存储。)
  5. 结构体包含数组成员,比如char a[3],它的对齐方式和分别写3个char是一样的,也就是说它还是按一个字节对齐。如果写:typedef char Array[3],Array这种类型的对齐方式还是按一个字节对齐,而不是按它的长度3对齐。
  6. 结构体包含共用体成员,则该共用体成员要从其原始共用体内部最大对齐模数的整数倍地址开始存储。

根据上面的对齐规则,成员a的起始位置偏移量为0,长度为8个字节;成员b的偏移量为8,长度为1个字节;成员c的偏移量为8+1=9,9无法整除成员c的长度4,所以在b和c之间自动填充空间对齐c的偏移量到12;成员d的偏移量为12+4=16,长度为一个字节;成员e的偏移量为16+1=17,17无法整除e的长度2,自动对齐e的偏移量至18;整个结构体的长度为18+2=20个字节,无法整除结构体中最大的数据类型的长度8,因此自动填充空间至24个字节。

前面说过,字节对齐对CPU读取内存的效率会有很大的提升。但是不同平台,不同的编译器可能会有不同的对齐方式。如果对齐方式不同,我们使用结构体进行数据的传输就会出现问题。统一对齐方式的最简单的方法就是采用1字节对齐。1字节对齐其实就相当于是不进行对齐,这样做就降低了CPU读取内存的效率,不过相对于数据传输的便利,这点损耗无足轻重。那么怎么让编译器不进行对齐操作呢?只需要在结构体声明的时候加一个__attribute__((packed)) 例如下面的代码

1
2
3
4
5
6
7
8
9
10
11
 typedef struct {
     uint64_t a;
     uint8_t  b;
     uint32_t c;
     uint8_t  d;
     uint16_t e;
 } __attribute__((packed)) TestStruct;

 TestStruct test = {1, 2, 3, 4, 5};
 NSUInteger length = sizeof(TestStruct);
 NSLog(@"%ld %@", length, [NSData dataWithBytes:&test length:length]);

加上这个之后输出结果就变成了

16 <01000000 00000000 02030000 00040500>

这下就跟我们预想的一致了。

参考资料:

维基百科:字节序 iOS开发关系结构体分配空间 C++结构体字节对齐

This post is licensed under CC BY 4.0 by the author.

忙碌的六月

再战黑苹果

Comments powered by Disqus.