我最初的问题是,在一个项目中,我有几个共享一生的对象(即,一旦我释放其中一个,我将全部释放它们),然后我想分配一块内存。我有三种不同对象类型的数组,struct foo
,void *
和char
。起初我想malloc()
这样一个块:
// +---------------+---------+-----------+---------+---------+
// | struct foo[n] | padding | void *[m] | padding | char[o] |
// +---------------+---------+-----------+---------+---------+
但是......如果不调用未定义的行为,我怎么能做到这一点?即,尊重类型别名规则,aligment ...如何正确计算内存块大小,声明内存块(具有有效类型),以及如何正确地获取指向其中所有三个部分的指针?
(我知道我可以malloc()
3个块,这将导致三个free()
,但我想知道如何使用单个块来执行它,同时仍然表现良好。)
我想将我的问题扩展到一个更普遍的问题:应该采取什么预防措施来为任意大小和对齐的对象实现memory pool,同时保持程序的良好运行? (假设可以在不调用未定义行为的情况下实现它。)
答案 0 :(得分:9)
无论你怎么努力,都无法在纯C中实现malloc
。
你总是在某些时候违反严格的别名。为避免疑问,使用不具有动态存储持续时间的char
缓冲区也会违反严格的别名规则。您还必须确保返回的任何指针都具有适当的对齐方式。
如果您乐意将自己绑定到特定平台,那么您也可以转向malloc
的特定实现以获取灵感。
但为什么不考虑编写一个调用malloc
的存根函数,并建立一个其他已分配对象的表?你甚至可以实现某种观察者/通知框架。另一个出发点可能是众所周知的垃圾收集器,它们都是用C语言编写的。
答案 1 :(得分:6)
正如另一个答案所说,你不能在C本身内重新实现malloc
。原因是您无法生成没有malloc
的有效类型的对象。
但是对于你的应用程序,你不需要这个,你可以使用malloc
或类似的,见下文,有一个大的内存块没有问题。
如果你有这么大的块,你必须知道如何将对象放在这个块中。这里的主要问题是对齐,您必须将所有对象放在与其最小对齐要求相对应的边界上。
从C11开始,可以使用_Alignof
运算符获取类型的对齐,并且可以使用aligned_alloc
请求总体内存。
把所有这些放在一起,这就是:
aligned_alloc
请求足够的内存与该值对齐如果您从通过void*
指针收到的无类型对象开始,那么别名就不是问题。该大对象的每个部分都具有您所写入的有效类型,请参阅我最近的blog entry。
C标准的相关部分是6.5 p6:
访问其存储值的对象的有效类型是 声明的对象类型,如果有的话.87)如果存储了一个值 通过具有类型的左值的没有声明类型的对象 不是一个字符类型,那么左值的类型就变成了 该访问和后续对象的有效类型 不修改存储值的访问。如果复制了值 使用memcpy或memmove进入没有声明类型的对象,或者是 复制为字符类型的数组,然后是有效类型 用于该访问的修改对象以及用于后续访问的对象 不修改值是从中有效的对象类型 如果有值,则复制该值。对于所有其他访问 没有声明类型的对象,对象的有效类型是 只是用于访问的左值的类型。
这里"对象没有声明类型"是由malloc
或类似物分配的对象(或子对象)。它清楚地表明,这些对象可以随时以任何类型写入,而不是将有效类型更改为所需的类型。
答案 2 :(得分:5)
通过了解3种类型union
的大小,可以实现更有效的分配。
union common {
struct foo f;
void * ptr;
char ch;
};
void *allocate3(struct foo **f, size_t m, void **ptr, size_t n, char **ch,
size_t o) {
size_t u_sz = sizeof (union common);
size_t f_sz = sizeof *f * m;
size_t f_cnt = (f_sz + u_sz - 1)/u_sz;
size_t p_sz = sizeof *ptr * n;
size_t p_cnt = (p_sz + u_sz - 1)/u_sz;
size_t c_sz = sizeof *ch * o;
size_t c_cnt = (c_sz + u_sz - 1)/u_sz;
size_t sum = f_cnt + p_cnt + c_cnt;
union common *u = malloc(sum * u_sz);
if (u) {
*f = &u[0].f;
*ptr = &u[f_cnt].ptr;
*ch = &u[f_cnt + c_cnt].ch;
}
return u;
}
这样,3个数组中的每一个都在union
边界上开始,因此可以满足对齐问题。通过将每个数组的空间调整为union
大小的倍数,浪费的空间少于first answer,但符合OP发布的目标。
有点浪费struct foo
很大,但o
很小。可以使用以下作为进一步改进。在最后一次之后不需要填充
阵列。
malloc((f_cnt + p_cnt) * u_sz + c_cz);
进一步考虑压缩分配。每个后续的“联合计数元素”可以使用省略早期类型的不同联合,依此类推。到达最后 - 这就是上面的想法的要点,最后一个数组只需要依赖于最后一个类型。这会使代码更复杂(容易出错),但在没有异常问题的情况下确实可以提高空间效率等。一些编码思路如下:
union common_last2 {
// struct foo f;
void * ptr;
char ch;
};
size_t u2_sz = sizeof (union common_last2);
size_t p_cnt = (p_sz + u2_sz - 1)/u2_sz;
... malloc(f_cnt*usz + p_cnt*u2_sz + c_cz);
*ch = tbd;
答案 3 :(得分:4)
首先,请务必使用-fno-strict-aliasing
或编译器中的等效内容。否则,即使满足所有对齐,编译器也可以使用别名规则来重叠相同内存块的不同用途。
我怀疑这与标准作者的意图是一致的,但是鉴于优化器可能非常激进,安全地实现类型无关内存池的唯一方法是禁用基于类型的别名分析。该标准的作者希望避免将某些使用基于类型的别名的编译器标记为不合规。此外,他们认为他们可以推迟编译器编写者关于如何识别和处理可能存在别名的情况的判断。他们确定了的情况,其中编译器编写者可能认为没有必要识别别名(例如,在有符号和无符号类型之间),但是否则期望编译器编写者能够做出合理的判断。我没有看到任何证据表明即使在其他形式的别名有用的平台上,他们也希望他们的允许案例清单被认为是详尽无遗的。
此外,无论人们如何谨慎遵守标准,无论如何都无法保证编译器应用“优化”。至少从gcc 6.2开始,存在别名错误,这些错误将破坏使用存储作为类型X的代码,将其写为Y,将其读取为Y,将相同的值写为X,并将存储读取为X - 行为,即100标准下定义的百分比。
如果处理了别名(例如使用指示的标志),并且您知道系统的最坏情况对齐要求,那么为池定义存储很容易:
union
{
char [POOL_BLOCK_SIZE] dat;
TYPE_WITH_WORST_ALIGNMENT align;
} memory_pool[POOL_BLOCK_COUNT];
不幸的是,即使所有与平台相关的对齐问题得到解决,标准也无法避免基于类型的别名问题。
答案 4 :(得分:2)
回答OP的一个问题
如何在不调用未定义行为的情况下完成此操作(想要malloc()这样的块)?
空间低效方法。分配union
种类型。如果较小类型所需的尺寸不是太大,则是合理的。
union common {
struct foo f;
void * ptr;
char ch;
};
void *allocate3(struct foo **f, size_t m, void **ptr, size_t n, char **ch,
size_t o) {
size_t sum = m + n + o;
union common *u = malloc(sizeof *u * sum);
if (u) {
*f = &u[0].f;
*ptr = &u[m].ptr;
*ch = &u[m + n].ch;
}
return u;
}
void sample() {
struct foo *f;
void *ptr;
char *ch;
size_t m, n, o;
void *base = allocate3(&f, m, &ptr, n, &ch, o);
if (base) {
// use data
}
free(base);
}