从 C 开始
边界对齐
看到下列结构体,使用 sizeof
可得该结构体大小为 24 个字节
typedef struct {
char a; // 1 个字节
double c; // 8 个字节
int b; // 4 个字节
} AType;
那为什么需要对齐呢?原因也很简单,即一次读写就能把整个数给拿出来(众所周知内存的读写对于 CPU 来说开销是蛮大的)
对于 x86 对齐只是建议而不是强制,而对于 MIPS 必须强制对齐
优化结构体存储大小
由于对齐规则的存在,所以可以调整结构体内字段顺序来优化存储大小。如下面优化过的结构体大小为 16 个字节
typedef struct {
double c; // 8 个字节
int b; // 4 个字节
char a; // 1 个字节
} BType;
此时我心里便有了一个小问号,调整字段顺序这么简单的事情应该交由编译器做,而不是交给程序员处理。
那为什么没有交由编译器做呢?
内存中按序存放
结构体中的字段在内存中是按序存放的
#include <stdio.h>
typedef struct {
double c; // 8 个字节
int b; // 4 个字节
char a; // 1 个字节
} BType;
typedef char* ptr;
void main() {
BType b = {1.0, 2, 'a'};
printf("%d\n", *(p + 8)); // 输出 2
}
看回 Java
字段重新排列
public class AlignDemo {
public static void main(String[] args) {
AClass o = new AClass();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
public static class AClass {
char a;
double b;
int c;
}
}
AlignDemo$AClass object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 int AClass.c 0
16 8 double AClass.b 0.0
24 2 char AClass.a
26 6 (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 6 bytes external = 6 bytes total
最终整个对象大小要补足为 8 的整数倍
可见字段顺序和类中字段顺序是不一致的,因为编译器做了优化用以压缩空间
那为什么 Java 却能这样优化呢?Java 对数据的访问做了严格的限制,指针都没有,更别提上文的骚操作了
又见 Redis
负数索引
以下代码摘自 Redis 的 sds.h
typedef char *sds;
struct sdshdr64 {
uint64_t len;
uint64_t alloc;
unsigned char flags;
char buf[];
};
static inline size_t sdsavail(const sds s) {
unsigned char flags = s[-1]; // 这是什么骚操作?
...
}
因为 C 数组是没有边界检查的,所以负数索引不会报错。sds 其实是一个 char 指针,上面说到 C 结构体在内存中是按序存放的,不妨把他想象成一个数组。往前走一个 char 的宽度,便能取得 flags。