内存分配和对齐

故事

面试官:一面就问点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的实现

  1. 堆是进程的虚拟内存空间中用于动态分配内存的区域。每个进程在启动时,操作系统会为其分配一小段初始堆空间。

  2. 内存分配器管理堆上的内存块。(每个块有大小,有标记,以链表的方式链接)

    可以首次适配(第一个满足)、最佳适配(遍历一遍满足且最小)、分离适配(不同大小的块放不同链表)。

    大的块被分配后会分割多余的小的块,小的块释放后就合并成大的块。

  3. 如果内存池(堆)中的空闲块不足,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 调用重新利用。

  • 如果 ptrNULLfree 不会执行任何操作。

合并空闲块

  • 当一个内存块被释放后,内存分配器会检查相邻的内存块是否也是空闲的。如果是,分配器可能会将这些内存块合并成一个更大的块,减少碎片化,提升内存分配效率。

堆顶内存回收

  • 如果被释放的内存块位于堆的末尾(堆顶),内存分配器有时会尝试通过 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;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值