给结构体快速分配内存: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 的特殊情况,针对它进行单独的处理。