故事
面试官:一面就问点cpp基础哈。
我:(心中一喜)
面试官:...(内存对齐相关)、...(malloc,free分配内存相关)、...(复制内存和处理重叠)
我:(哭)
cpp内存对齐
有两个原因:
-
内存逻辑上是线性的,实际是并行的
比如64位:
0 64 ...
1 65 ...
2 66 ...
... ... ...
如果要读8个字节,那么8的倍数的地址上可以一次读完,不然要读两次。
然后读操作会涉及到缓存之类的,会浪费时间。
-
早年间的一些 CPU 是不支持没有对齐的内存访问的。
对齐规则:
1、分配内存的顺序是按照声明的顺序。
2、每个变量相对于起始位置的偏移量必须是该变量类型大小的整数倍,不是整数倍空出内存,直到偏移量是整数倍为止。
3、最后整个结构体的大小必须是里面变量类型最大值的整数倍。
对齐样例:
struct rec1 { char *a; short b; double c; char d; float e; char f; long g; int h; }; /* rec1 Size: 56 Offsets, Sizes and Padding: Offset of a: 0, Size: 8, Padding: 0 Offset of b: 8, Size: 2, Padding: 6 Offset of c: 16, Size: 8, Padding: 0 Offset of d: 24, Size: 1, Padding: 3 Offset of e: 28, Size: 4, Padding: 0 Offset of f: 32, Size: 1, Padding: 7 Offset of g: 40, Size: 8, Padding: 0 Offset of h: 48, Size: 4, Padding: 4 */
合理的布局可以节约内存:
struct rec3 { char *a; double c; long g; float e; int h; short b; char d; char f; }; /* rec3 Size: 40 Offsets, Sizes and Padding: Offset of a: 0, Size: 8, Padding: 0 Offset of c: 8, Size: 8, Padding: 0 Offset of g: 16, Size: 8, Padding: 0 Offset of e: 24, Size: 4, Padding: 0 Offset of h: 28, Size: 4, Padding: 0 Offset of b: 32, Size: 2, Padding: 0 Offset of d: 34, Size: 1, Padding: 0 Offset of f: 35, Size: 1, Padding: 4 */
可以手动取消内存对齐:
// 早年间的一些 CPU 是不支持没有对齐的内存访问的,但是在现代 CPU 上已经没有了这个问题,编译器也提供了一些选项可以让我们明确指出不需要内存对齐。 struct __attribute__((packed)) rec2{ // 去掉所有对齐的优化 // 让类的空间密集排布一定会影响内存的读取效率,但不大 char *a; short b; double c; char d; float e; char f; long g; int h; }; /* rec2 Size: 36 Offsets, Sizes and Padding: Offset of a: 0, Size: 8, Padding: 0 Offset of b: 8, Size: 2, Padding: 0 Offset of c: 10, Size: 8, Padding: 0 Offset of d: 18, Size: 1, Padding: 0 Offset of e: 19, Size: 4, Padding: 0 Offset of f: 23, Size: 1, Padding: 0 Offset of g: 24, Size: 8, Padding: 0 Offset of h: 32, Size: 4, Padding: 0 */
测试代码:
#include <bits/stdc++.h>
struct rec1 {
char *a;
short b;
double c;
char d;
float e;
char f;
long g;
int h;
};
/*
rec1
Size: 56
Offsets, Sizes and Padding:
Offset of a: 0, Size: 8, Padding: 0
Offset of b: 8, Size: 2, Padding: 0
Offset of c: 16, Size: 8, Padding: 6
Offset of d: 24, Size: 1, Padding: 0
Offset of e: 28, Size: 4, Padding: 3
Offset of f: 32, Size: 1, Padding: 0
Offset of g: 40, Size: 8, Padding: 7
Offset of h: 48, Size: 4, Padding: 0
*/
// 早年间的一些 CPU 是不支持没有对齐的内存访问的,但是在现代 CPU 上已经没有了这个问题,编译器也提供了一些选项可以让我们明确指出不需要内存对齐。
struct __attribute__((packed)) rec2{
// 去掉所有对齐的优化
// 让类的空间密集排布一定会影响内存的读取效率
char *a;
short b;
double c;
char d;
float e;
char f;
long g;
int h;
};
/*
rec2
Size: 36
Offsets, Sizes and Padding:
Offset of a: 0, Size: 8, Padding: 0
Offset of b: 8, Size: 2, Padding: 0
Offset of c: 10, Size: 8, Padding: 0
Offset of d: 18, Size: 1, Padding: 0
Offset of e: 19, Size: 4, Padding: 0
Offset of f: 23, Size: 1, Padding: 0
Offset of g: 24, Size: 8, Padding: 0
Offset of h: 32, Size: 4, Padding: 0
*/
struct rec3 {
char *a;
double c;
long g;
float e;
int h;
short b;
char d;
char f;
};
/*
rec3
Size: 40
Offsets, Sizes and Padding:
Offset of a: 0, Size: 8, Padding: 0
Offset of c: 8, Size: 8, Padding: 0
Offset of g: 16, Size: 8, Padding: 0
Offset of e: 24, Size: 4, Padding: 0
Offset of h: 28, Size: 4, Padding: 0
Offset of b: 32, Size: 2, Padding: 0
Offset of d: 34, Size: 1, Padding: 0
Offset of f: 35, Size: 1, Padding: 4
*/
struct __attribute__((aligned(4))) rec4 {
char *a;
short b;
double c;
char d;
float e;
char f;
long g;
int h;
};
struct __attribute__((aligned(8))) rec8 {
char *a;
short b;
double c;
char d;
float e;
char f;
long g;
int h;
};
struct __attribute__((aligned(16))) rec16 {
char *a;
short b;
double c;
char d;
float e;
char f;
long g;
int h;
};
template <typename T>
void printOffsetsAndSizes() {
std::cout << "Size: " << sizeof(T) << std::endl;
struct MemberInfo {
const char* name;
size_t offset;
size_t size;
};
// C++ 并没有内置的方法来反射或自动获取结构体的成员。所以手动定义成员信息
std::vector<MemberInfo> members =
{
{"a", offsetof(T, a), sizeof(((T*)nullptr)->a)},
{"b", offsetof(T, b), sizeof(((T*)nullptr)->b)},
{"c", offsetof(T, c), sizeof(((T*)nullptr)->c)},
{"d", offsetof(T, d), sizeof(((T*)nullptr)->d)},
{"e", offsetof(T, e), sizeof(((T*)nullptr)->e)},
{"f", offsetof(T, f), sizeof(((T*)nullptr)->f)},
{"g", offsetof(T, g), sizeof(((T*)nullptr)->g)},
{"h", offsetof(T, h), sizeof(((T*)nullptr)->h)}
};
// 按偏移排序一下
std::sort(members.begin(),members.end(),[&](MemberInfo& a,MemberInfo& b){return a.offset<b.offset;});
std::cout << "Offsets, Sizes and Padding:" << std::endl;
for (size_t i = 0; i < members.size(); ++i)
{
std::cout << "Offset of " << members[i].name << ": " << members[i].offset
<< ", Size: " << members[i].size
<< ", Padding: " << (i+1 < members.size() ? members[i+1].offset : sizeof(T)) - (members[i].offset + members[i].size) << std::endl;
}
}
int main() {
std::cout << "rec1" << std::endl;
printOffsetsAndSizes<rec1>();
std::cout << "=================================" << std::endl;
std::cout << "rec2" << std::endl;
printOffsetsAndSizes<rec2>();
std::cout << "=================================" << std::endl;
std::cout << "rec3" << std::endl;
printOffsetsAndSizes<rec3>();
std::cout << "=================================" << std::endl;
std::cout << "rec4" << std::endl;
printOffsetsAndSizes<rec4>();
std::cout << "=================================" << std::endl;
std::cout << "rec8" << std::endl;
printOffsetsAndSizes<rec8>();
std::cout << "=================================" << std::endl;
std::cout << "rec16" << std::endl;
printOffsetsAndSizes<rec16>();
std::cout << "=================================" << std::endl;
return 0;
}
malloc,free的实现
-
堆是进程的虚拟内存空间中用于动态分配内存的区域。每个进程在启动时,操作系统会为其分配一小段初始堆空间。
-
内存分配器管理堆上的内存块。(每个块有大小,有标记,以链表的方式链接)
可以首次适配(第一个满足)、最佳适配(遍历一遍满足且最小)、分离适配(不同大小的块放不同链表)。
大的块被分配后会分割多余的小的块,小的块释放后就合并成大的块。
-
如果内存池(堆)中的空闲块不足,
malloc
可能会调用sbrk()
或brk()
系统调用。void* sbrk(intptr_t increment); // 参数说明: // increment:要增加的堆空间大小(以字节为单位)。可以是负值表示减少堆空间。 // 返回值: // 成功时返回旧的程序断点地址(即原来堆的末尾位置)。 // 失败时返回 (void*)-1 并设置 errno。 // demo #include <unistd.h> #include <stdio.h> int main() { void* heap_end = sbrk(0); // 获取当前堆末尾地址 printf("Current heap end: %p\n", heap_end); // 扩展堆空间 if (sbrk(1024) == (void*)-1) { perror("sbrk failed"); return 1; } heap_end = sbrk(0); // 获取新的堆末尾地址 printf("New heap end: %p\n", heap_end); return 0; }
void* mmap(void* addr, size_t length, int prot, int flags, int fd, off_t offset); // 参数说明: - `addr`:指定映射的起始地址,通常设为 `NULL` 让内核选择地址。 - `length`:要映射的内存大小,以字节为单位。 - `prot`:内存保护标志,指定映射区域的访问权限。常用的值包括: - `PROT_READ`:可读 - `PROT_WRITE`:可写 - `PROT_EXEC`:可执行 - `PROT_NONE`:不可访问 - `flags`:指定映射类型。常用的值包括: - `MAP_SHARED`:共享映射,修改会影响底层文件和其他映射。 - `MAP_PRIVATE`:私有映射,修改不会影响其他映射和文件。 - `MAP_ANONYMOUS`:匿名映射,不与文件关联,通常用于内存分配。 - `fd`:文件描述符,如果使用匿名映射,设置为 `-1`。 - `offset`:文件的偏移量,通常设为 `0`。 // 返回值: - 成功时返回映射的虚拟内存地址。 - 失败时返回 `(void*)-1`,并设置 `errno`。 // demo #include <sys/mman.h> #include <fcntl.h> #include <unistd.h> #include <stdio.h> int main() { // 分配一块匿名内存 void* addr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (addr == MAP_FAILED) { perror("mmap failed"); return 1; } printf("Allocated memory at: %p\n", addr); // 写入数据 sprintf((char*)addr, "Hello, mmap!"); // 读取数据 printf("Data: %s\n", (char*)addr); // 释放内存 if (munmap(addr, 4096) == -1) { perror("munmap failed"); return 1; } return 0; }
-
sbrk
用于动态调整进程堆的大小,适合小块内存的逐步扩展。 -
mmap
用于映射文件或直接从内核获取一块新的虚拟内存,适合大内存块的分配。
-
而free更简单:
释放内存块:
-
free(ptr)
会将通过malloc
或相关函数分配的内存块标记为可用。它不会将内存直接归还给操作系统,而是将其加入到内存分配器的空闲链表(free list)中,供后续的malloc
调用重新利用。 -
如果
ptr
为NULL
,free
不会执行任何操作。
合并空闲块:
-
当一个内存块被释放后,内存分配器会检查相邻的内存块是否也是空闲的。如果是,分配器可能会将这些内存块合并成一个更大的块,减少碎片化,提升内存分配效率。
堆顶内存回收:
-
如果被释放的内存块位于堆的末尾(堆顶),内存分配器有时会尝试通过
sbrk
减少堆的大小,将空闲内存归还给操作系统。 -
这种情况比较少见,只有当大量堆顶内存被释放时才会发生,而且不是所有内存分配器都会实现这一功能。
复制内存和处理重叠
就是手写一个memcpy,注意内存重叠。
void* memcpy(void* dest, const void* src, size_t n) {
unsigned char* d = (unsigned char*)dest;
const unsigned char* s = (const unsigned char*)src;
// 如果内存重叠,处理内存重叠问题
if (d > s && d < s + n) {
// 从后向前复制,避免重叠区域覆盖
d += n;
s += n;
while (n--) {
*(--d) = *(--s);
}
} else {
// 如果不重叠,从前向后复制
while (n--) {
*d++ = *s++;
}
}
return dest;
}