找回密码
 立即注册→加入我们

QQ登录

只需一步,快速开始

搜索
热搜: 下载 VB C 实现 编写
查看: 695|回复: 12

【C】给结构体快速分配内存:bunchalloc() 代码实现

[复制链接]
发表于 2023-3-16 17:02:34 | 显示全部楼层 |阅读模式

欢迎访问技术宅的结界,请注册或者登录吧。

您需要 登录 才可以下载或查看,没有账号?立即注册→加入我们

×

给结构体快速分配内存:bunchalloc() 代码实现

众所周知:malloc() 。尤其是在需要多线程运行的代码开发环境下更是性能瓶颈。不过通常情况下,C 语言这种 “拉栓一枪一个洞” 的编程语言在正经的开发过程中是不玩花活的,该怎么管理内存就怎么管理内存,除非 malloc() 被调用得过于频繁以至于造成了人可以感受到的延迟卡顿,一般还是按照常规的开发习惯来——需要内存的时候,直接调用 malloc();然后在对应的释放内存的地方写好你的 free(),省得维护代码的时候发现自己已经看不懂当初是怎么玩花活了。但是因为不玩花活,malloc() 的性能问题就会变得十分明显,使其成为一个既不想让人去碰触、又实实在在地给你制造麻烦的东西。

别的编程语言比如带 GC 的编程语言比如 C# 在这块就根本不需要程序员操心——内存分配本来就快(语言特性本身已经帮你玩了内存管理的花活);而不带 GC 的编程语言比如 C++ 则在使用智能指针 std::shared_ptr 的时候,通过使用 std::make_shared<类型名>(构造参数列表) 来给对象分配内存,可以在单次的内存分配操作中同时给智能指针的控制块和对象本身一起分配内存。

不过对于 C 语言而言针对内存管理的优化方案还是可以有的,这就需要你去观察、找出任何合适的优化的点。一个典型的例子:一个结构体,它里面需要有很多的成员 buffer(比如字符串),这些成员 buffer 的长度是明确且不会变化的,但并不能在编译期间就能推算出长度,因此不能用定长数组,只能在运行时推算出需要的长度后,给每个 buffer 调用 malloc() 分配内存。 这样的情况就很适合去进行优化 :想办法使用单次的 malloc() 调用,一次性给结构体和所有的成员 buffer 分配内存。这样就不用多次调用 malloc() 了。

既想代码写得舒服优雅、不像是在整花活,又要尽可能减少程序运行时内存分配的时间成本,那就需要设计一个针对结构体 + 成员 buffer 的内存分配函数, 要能允许任意类型的结构体都可以 用它来分配内存和 buffer。这个函数应该需要你 提供每个成员 buffer 的指针在结构体里的位置,然后在调用的时候,这个函数能帮你计算出每个 buffer 的偏移量,并 自动填写到结构体的成员变量里

针对这样的情况,我实现了一个 bunchalloc() 函数,用于针对这样的场景进行优化。而为了代码的规范性,我也给它弄了个配套的 bunchfree()(其实它里面就是一个 free())。

代码实现的思路是:让调用者使用参数列表传递结构体成员指针变量的偏移量和 buffer 的大小,然后被调用的 bunchalloc() 循环遍历参数列表,统计结构体的大小和所有 buffer 的大小,进行一次 malloc() 分配内存(并使用 memset() 清零内存),在获得了分配好的内存地址后,重新遍历参数列表,填写结构体的成员指针 使其指向各自的被分配的内存地址。在这个过程中默认 malloc() 分配到的内存地址是按运行环境需求对齐的,然后在计算每个 buffer 的地址的时候,同样进行内存对齐的处理,确保结构体的每个 buffer 指针都指向一个对齐了的内存地址。这样的话,我只需要一句 bunchalloc() 的调用,然后使用 stddef.h 提供的 offsetof() 宏来获取结构体成员的偏移量,按需要的个数填写参数即可。创建好的结构体内存的所有 buffer 字段都是填写好了地址的,而其它的字段或者内存则都是清零的,使用起来非常方便。

代码实现:bunchalloc.c

#include"bunchalloc.h"
#include <string.h>
#include <stdarg.h>
#include <stddef.h>

static size_t make_padded(size_t unpadded, size_t alignment)
{
    return ((unpadded - 1) / alignment + 1) * alignment;
}

void* bunchalloc(size_t alignment, size_t headersize, ...)
{
    size_t i, count = 0;
    size_t member_offset, desired_size;;
    size_t padded_size = 0;
    size_t cur_offset = 0, cur_size;
    void* ret = NULL;
    va_list ap;
    if (!alignment) alignment = sizeof(size_t) * 2;
    cur_size = make_padded(headersize, alignment);

    va_start(ap, headersize);

    for(;;)
    {
        member_offset = va_arg(ap, size_t);
        desired_size = va_arg(ap, size_t);
        if (!desired_size) break;
        count += 1;

        padded_size = make_padded(desired_size, alignment);
        cur_offset = cur_size;
        cur_size += padded_size;
    };

    va_end(ap);

    ret = malloc(cur_size);
    if (!ret) return NULL;
    memset(ret, 0, cur_size);

    va_start(ap, headersize);

    // Align body data to an aligned address
    cur_size = make_padded(headersize, alignment);
    for (i = 0; i < count; i++)
    {
        member_offset = va_arg(ap, size_t);
        desired_size = va_arg(ap, size_t);
        padded_size = make_padded(desired_size, alignment);
        cur_offset = cur_size;
        cur_size += padded_size;

        *(void**)((size_t)ret + member_offset) = (void*)((size_t)ret + cur_offset);
    }

    va_end(ap);

    return ret;
}

void bunchfree(void* ptr)
{
    free(ptr);
}

头文件 bunchalloc.h

#ifndef _SATISALLOC_H_
#define _SATISALLOC_H_ 1

#include <stdlib.h>

void* bunchalloc(size_t alignment, size_t headersize, ...);
void bunchfree(void* ptr);

#endif

优雅用例

typedef struct video_frame_struct video_frame_t, *video_frame_p;
struct video_frame_struct
{
    double timestamp; // 帧时间戳
    uint32_t index;   // 帧索引

    uint32_t w, h;  // 宽度,高度
    uint32_t *data; // 图像数据
    uint32_t **row; // 图像的行指针

    uint32_t raw_w, raw_h;   // 原始宽度,原始高度
    uint32_t *raw_data;      // 原始图像数据
    uint32_t **raw_data_row; // 原始图像的行指针

    float *mono_data;      // 黑白化处理的图像数据
    float **mono_data_row; // 黑白图像的行指针

    video_frame_p next; // 链表的下一个视频帧
};

上述代码描述了一个视频播放器的视频帧数据的结构体。可以看到这个结构体有很多的 buffer 被用于图像处理。而 buffer 的长度基本都是确定不变的——因为是视频帧数据,所以 buffer 的大小在确定了视频的尺寸后就已经能明确了,但因为播放器需要兼容不同分辨率的视频,所以 buffer 的大小不能在编译期间定死。并且,因为是视频播放器的视频帧数据链表,它需要随着视频的播放而不断地动态分配、缓存、释放,所以这块的性能比较敏感。一旦性能跟不上,视频播放器或许就会掉帧。

我们来看看它原先的内存分配的代码是怎么写的:

// 释放函数
void video_frame_free(video_frame_p* pv)
{
    video_frame_p v;
    if (!pv) return;
    v = *pv;
    if (!v) return;
    *pv = NULL;

    // 释放成员 buffer
    free(v->data);
    free(v->row);
    free(v->raw_data);
    free(v->raw_data_row);
    free(v->mono_data);
    free(v->mono_data_row);

    // 释放结构体
    free(v);
}

// 分配函数
video_frame_p video_frame_create(uint32_t width, uint32_t height, uint32_t raw_width, uint32_t raw_height)
{
    uint32_t i;

    // 分配结构体
    video_frame_p v = malloc(sizeof v[114514]);
    if (!v) return v;
    memset(v, 0, sizeof v[1919810]);

    // 填写字段
    v->w = width;
    v->h = height;
    v->raw_w = raw_width;
    v->raw_h = raw_height;

    // 分配帧缓冲
    v->data = malloc(width * height * sizeof v->data[0]);
    v->mono_data = malloc(width * height * sizeof v->mono_data[0]);
    v->raw_data = malloc(raw_width * raw_height * sizeof v->raw_data[0]);

    // 分配行指针
    v->row = malloc(height * sizeof v->row[0]);
    v->mono_data_row = malloc(height * sizeof v->mono_data_row[0]);
    v->raw_data_row = malloc(raw_height * sizeof v->raw_data_row[0]);

    // 一旦有任何一个 buffer 分配失败了,则跳转去销毁
    if (!v->data || !v->mono_data || !v->raw_data || !v->row || !v->mono_data_row || !v->raw_data_row) goto FailExit;

    // 配置行指针
    for (i = 0; i < height; i++)
    {
        v->row[i] = &v->data[i * width];
        v->mono_data_row[i] = &v->mono_data[i * width];
    }
    for (i = 0; i < raw_height; i++)
    {
        v->raw_data_row[i] = &v->raw_data[i * raw_width];
    }

    return v;
FailExit:
    video_free(&v);
    return NULL;
}

可以看到,每次调用 video_frame_create() 的时候,它要进行总共 7 次的 malloc() 调用。这样的场景适合使用 bunchalloc() 进行针对性的优化:

// 释放函数
void video_frame_free(video_frame_p* pv)
{
    video_frame_p v;
    if (!pv) return;
    v = *pv;
    if (!v) return;
    *pv = NULL;

    // 释放结构体
    bunchfree(v);
}

// 分配函数
video_frame_p video_frame_create(uint32_t width, uint32_t height, uint32_t raw_width, uint32_t raw_height)
{
    uint32_t i;

    // 分配结构体
    video_frame_p v = bunchalloc(0, sizeof v[114514],
        // 帧缓冲
        offsetof(video_frame_t, data), width * height * sizeof v->data[0],
        offsetof(video_frame_t, mono_data), width * height * sizeof v->mono_data[0],
        offsetof(video_frame_t, raw_data), raw_width * raw_height * sizeof v->raw_data[0],
        // 行指针
        offsetof(video_frame_t, row), height * sizeof v->row[0],
        offsetof(video_frame_t, mono_data_row), height * sizeof v->mono_data_row[0],
        offsetof(video_frame_t, raw_data_row), raw_height * sizeof v->raw_data_row[0],
        // 结束
        0, 0
    );
    if (!v) return v;

    // 填写字段
    v->w = width;
    v->h = height;
    v->raw_w = raw_width;
    v->raw_h = raw_height;

    // 配置行指针
    for (i = 0; i < height; i++)
    {
        v->row[i] = &v->data[i * width];
        v->mono_data_row[i] = &v->mono_data[i * width];
    }
    for (i = 0; i < raw_height; i++)
    {
        v->raw_data_row[i] = &v->raw_data[i * raw_width];
    }

    return v;
FailExit:
    video_free(&v);
    return NULL;
}

经过优化后,内存分配的过程变为在 bunchalloc() 内部推算好每个 buffer 的地址和所有需要的内存大小后,经过单次的 malloc() 调用即可分配全部所需的内存,包括结构体本身在内。bunchalloc() 函数返回后,你的结构体的所有 buffer 指针都被填写了正确的值,即推算出来的地址。

但是需要注意:如果你的结构体并不是所有的成员 buffer 都适合这样的情况(比如其中有一个 buffer 需要重新分配或者改变大小的时候),那么最好不要玩 bunchalloc() 的花活以免因为代码维护难度大而造成内存泄漏。写好注释、标注好这个 buffer 的特殊情况,针对它进行单独的处理。

本帖被以下淘专辑推荐:

回复

使用道具 举报

发表于 2023-3-16 17:32:38 | 显示全部楼层
慢:多次分配N个小内存。
快:一次分配一个大内存,再自行划分为N个小内存。

是这个意思吗?
回复 赞! 靠!

使用道具 举报

 楼主| 发表于 2023-3-16 19:04:28 | 显示全部楼层
美俪女神 发表于 2023-3-16 17:32
慢:多次分配N个小内存。
快:一次分配一个大内存,再自行划分为N个小内存。
对。尤其是多线程环境下,MSVC 的 `malloc()` 会上锁,然后就非常低效了。
回复 赞! 靠!

使用道具 举报

发表于 2023-3-24 22:01:25 | 显示全部楼层
0xAA55 发表于 2023-3-16 19:04
对。尤其是多线程环境下,MSVC 的 `malloc()` 会上锁,然后就非常低效了。


这也是驱动老哥的常用方法了。。。

比如要动态分配一个UNICODE_STRING,新手通常来说是先申请一个sizeof(UNICODE_STRING)字节的内存p1,再申请wcslen(string)*2字节的内存p2,然后设置p1->Buffer=p2。

驱动老哥则是直接申请sizeof(UNICODE_STRING)+wcslen(string)*2字节的内存p。然后设置p->Buffer=(PWCHAR)(p+sizeof(UNICODE_STRING))。
回复 赞! 靠!

使用道具 举报

 楼主| 发表于 2023-3-25 00:54:41 | 显示全部楼层
美俪女神 发表于 2023-3-24 22:01
这也是驱动老哥的常用方法了。。。

比如要动态分配一个UNICODE_STRING,新手通常来说是先申请一个sizeof ...


和驱动老哥可能不同的地方在于:我专门提供了配套的释放函数 bunchfree() 对应内存分配函数 bunchalloc(),在编程习惯上也许更合理(因为使用 bunchalloc() 分配内存后,你使用配套的 bunchfree() 释放内存)而驱动老哥的 UNICODE_STRING 貌似并不是一个“如何释放内存是一眼能看出来的”类型,也就是说,如果你的 UNICODE_STRING* 有的是一次性分配来的,有的则是结构体和字符串分别分配来的,那你如何区分哪个可以直接释放、哪个需要分别释放呢?
回复 赞! 靠!

使用道具 举报

发表于 2023-3-28 15:24:58 | 显示全部楼层
0xAA55 发表于 2023-3-25 00:54
和驱动老哥可能不同的地方在于:我专门提供了配套的释放函数 bunchfree() 对应内存分配函数 bunchalloc() ...


这就要看需求了。有些时候需要释放UNICODE_STRING而保留BUFFER就单独分配。。。

比如在PWCHAR里保存了配置文件路径,但是内核API不接收PWCHAR,只接收PUNICODE_STRING,这个时候就分配一个PUNICODE_STRING,等调用完了内核API,就只释放掉PUNICODE_STRING而不释放PWCHAR。
回复 赞! 靠!

使用道具 举报

发表于 2023-4-24 18:17:01 | 显示全部楼层
其实站长可以看看SV里的 ARRAY_Z 结构。它和站长写的bunchalloc有些相似,ARRAY_Z 应该使用起来更便利一些:
  1. /* Function name: strInitArrayZ
  2. * Description:   Allocate a sized array.
  3. * Parameters:
  4. *      parrz Pointer to the sized array you want to allocate.
  5. *        num Number of elements.
  6. *       size Size of each element.
  7. *            If size equaled to 0, function would return a NULL.
  8. * Return value:  Pointer to new allocated buffer.
  9. * Caution:       Address of parrz Must Be Allocated first.
  10. */
  11. void * strInitArrayZ(P_ARRAY_Z parrz, size_t num, size_t size)
  12. {
  13.         if (NULL == (parrz->pdata = (PUCHAR) malloc(num * size)))
  14.         {
  15.                 parrz->num = 0;
  16.                 parrz->pdata = NULL;
  17.         }
  18.         else
  19.                 parrz->num = num;
  20.         return parrz->pdata;
  21. }

  22. /* Function name: strCreateArrayZ
  23. * Description:   Create a sized array.
  24. * Parameters:
  25. *        num Number of elements.
  26. *       size Size of each element.
  27. * Return value:  Pointer to new allocated buffer.
  28. *                If function returned NULL, it would indicate an allocation failure.
  29. * Caution:       Address of parrz Must Be Allocated first.
  30. */
  31. P_ARRAY_Z strCreateArrayZ(size_t num, size_t size)
  32. {
  33.         P_ARRAY_Z parrz = (P_ARRAY_Z) malloc(sizeof(ARRAY_Z));
  34.         if (NULL == parrz || NULL == strInitArrayZ(parrz, num, size))
  35.         {
  36.                 free(parrz);
  37.                 return NULL;
  38.         }
  39.         return parrz;
  40. }
复制代码

以上是申请内存的代,然后有逐个读取指针的代码:
  1. void * strLocateItemArrayZ_O(P_ARRAY_Z parrz, size_t size, size_t index)
  2. {
  3.         return (index < strLevelArrayZ(parrz) ? parrz->pdata + index * size : NULL);
  4. }
复制代码

还有各种配套操作,比如排序,扩容,回收等等。
回复 赞! 靠!

使用道具 举报

 楼主| 发表于 2023-4-24 18:31:25 | 显示全部楼层
usr 发表于 2023-4-24 18:17
其实站长可以看看SV里的 ARRAY_Z 结构。它和站长写的bunchalloc有些相似,ARRAY_Z 应该使用起来更便利一些 ...

没有我的便利。因为它用了结构体,你得先准备一个结构体,然后才能 malloc,关键是它那个结构体也是 malloc 出来的。

所以性能也不行。
回复 赞! 靠!

使用道具 举报

发表于 2023-4-24 21:33:15 | 显示全部楼层
本帖最后由 usr 于 2023-4-24 22:23 编辑
0xAA55 发表于 2023-4-24 18:31
没有我的便利。因为它用了结构体,你得先准备一个结构体,然后才能 malloc,关键是它那个结构体也是 mall ...


其实malloc影响蛮小的:
  1. struct st_test
  2. {
  3.     int a;
  4.     int b;
  5. };

  6. int main()
  7. {
  8.     size_t i;
  9.     LARGE_INTEGER la, lb, lc;

  10.     QueryPerformanceFrequency(&lc);
  11.     QueryPerformanceCounter(&la);
  12.     for (i = 0; i < 999999; ++i) bunchalloc(0, sizeof(struct st_test), offsetof(struct st_test, a), 0, offsetof(struct st_test, b), 0);
  13.     QueryPerformanceCounter(&lb);
  14.     printf("%lf\n", (double)(lb.QuadPart - la.QuadPart) / (double)lc.QuadPart);

  15.     QueryPerformanceFrequency(&lc);
  16.     QueryPerformanceCounter(&la);
  17.     for (i = 0; i < 999999; ++i) strCreateArrayZ(1, sizeof(struct st_test));
  18.     QueryPerformanceCounter(&lb);
  19.     printf("%lf\n", (double)(lb.QuadPart - la.QuadPart) / (double)lc.QuadPart);

  20.     return 0;
  21. }
复制代码


结果:
0.061134
0.090754

不过只用一次malloc的bunchalloc效率确实是高,尤其是回收的时候只用一次free
回复 赞! 靠!

使用道具 举报

 楼主| 发表于 2023-4-25 11:06:26 | 显示全部楼层
usr 发表于 2023-4-24 21:33
其实malloc影响蛮小的:

这都还算小?而且你这只是测的单线程环境,你不知道 malloc() 有个全局锁么?多线程环境下,malloc() 会锁堆。
回复 赞! 靠!

使用道具 举报

发表于 2023-4-25 12:52:00 | 显示全部楼层
0xAA55 发表于 2023-4-25 11:06
这都还算小?而且你这只是测的单线程环境,你不知道 malloc() 有个全局锁么?多线程环境下,malloc() 会 ...

malloc 有锁才是线程安全的呀,关于malloc是否会锁堆我不知道没看过malloc源码不知道封锁粒度。
回复 赞! 靠!

使用道具 举报

 楼主| 发表于 2023-4-28 15:16:04 | 显示全部楼层
usr 发表于 2023-4-25 12:52
malloc 有锁才是线程安全的呀,关于malloc是否会锁堆我不知道没看过malloc源码不知道封锁粒度。 ...

既然如此,你还敢说 malloc 的影响小?
回复 赞! 靠!

使用道具 举报

发表于 2023-4-28 20:22:50 | 显示全部楼层
0xAA55 发表于 2023-4-28 15:16
既然如此,你还敢说 malloc 的影响小?

得看malloc的封锁粒度,当前的运行环境,几个线程在请求资源,有无死锁。malloc的影响是动态变化的。如果排除这些复杂的影响,单线程调用malloc影响是很小的。
回复 赞! 靠!

使用道具 举报

本版积分规则

QQ|Archiver|小黑屋|技术宅的结界 ( 滇ICP备16008837号 )|网站地图

GMT+8, 2024-5-27 02:46 , Processed in 0.039451 second(s), 27 queries , Gzip On.

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表