C 我应该采取什么预防措施来创建一个不调用未定义行为的内存池?

C 我应该采取什么预防措施来创建一个不调用未定义行为的内存池?,c,language-lawyer,undefined-behavior,c11,strict-aliasing,C,Language Lawyer,Undefined Behavior,C11,Strict Aliasing,我最初的问题是,在一个项目中,我有几个对象共享一个生命周期(即,一旦我释放其中一个,我就会释放所有对象),然后我想分配一个内存块。我有三种不同对象类型的数组,struct foo、void*和char。起初,我想要malloc()这样的块: // +---------------+---------+-----------+---------+---------+ // | struct foo[n] | padding | void *[m] | padding | char[o] | //

我最初的问题是,在一个项目中,我有几个对象共享一个生命周期(即,一旦我释放其中一个,我就会释放所有对象),然后我想分配一个内存块。我有三种不同对象类型的数组,
struct foo
void*
char
。起初,我想要
malloc()
这样的块:

// +---------------+---------+-----------+---------+---------+
// | struct foo[n] | padding | void *[m] | padding | char[o] |
// +---------------+---------+-----------+---------+---------+
但是。。。我如何在不调用未定义行为的情况下实现这一点?即,遵守类型别名规则、对齐。。。如何正确地计算内存块大小,声明内存块(及其有效类型),以及如何正确地以可移植的方式获取指向其中所有三个部分的指针

(我知道我可以
malloc()
3个块,这将导致三个
free()
,但我想知道如何在保持良好性能的情况下使用单个块。)


我想把我的问题扩展到一个更一般的问题:在保持程序良好运行的同时,应该采取什么预防措施来实现具有任意大小和对齐方式的对象?(假设可以在不调用未定义行为的情况下实现它。)

无论您如何努力,都不可能在纯C中实现
malloc

你总是在某个时候违反严格的别名。为避免疑问,使用没有动态存储持续时间的
char
缓冲区也会违反严格的别名规则。您还必须确保返回的任何指针都具有适当的对齐方式

如果您乐于将自己绑定到某个特定的平台上,那么您也可以使用
malloc
的特定实现来获得灵感


<>但是为什么不考虑编写一个名为MalOC/的存根函数,还建立一个其他分配对象的表?您甚至可以实现某种类型的observer/notify框架。另一个起点可能是用C编写的著名垃圾收集器。

来回答OP的一个问题

我如何在不调用未定义的行为的情况下实现这一点(希望malloc()像这样的块)

空间效率低下的方法。分配类型的
联合
。如果较小类型所需的尺寸不太大,则合理

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);
}

首先,确保使用
-fno严格别名
或编译器上的任何等效项。否则,即使满足所有对齐条件,编译器也可能使用别名规则来重叠同一内存块的不同使用

我怀疑这与标准作者的意图是否一致,但给定的优化器可能非常激进,安全实现类型无关内存池的唯一方法是禁用基于类型的别名分析。该标准的作者希望避免将某些使用基于类型的别名的编译器标记为不兼容。此外,他们认为他们可以遵从编译器编写者关于如何识别和处理可能出现别名的情况的判断。他们确定了编译器编写者可能认为没有必要识别别名(例如,在有符号和无符号类型之间)的情况,但期望编译器编写者做出合理的判断。我看不到任何证据表明,即使在其他形式的别名可能有用的平台上,他们也不打算将其允许的案例列表视为详尽无遗

此外,无论人们多么仔细地遵守标准,也不能保证编译器会应用破坏性的“优化”。至少在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];

不幸的是,即使解决了所有与平台相关的对齐问题,该标准也无法避免基于类型的别名问题。

如另一个答案所述,您无法在C本身中重新实现
malloc
。原因是如果没有
malloc
,就无法生成没有有效类型的对象

但是对于您的应用程序,您不需要这个,您可以使用
malloc
或类似的方法,如下所示,来拥有一大块内存而不会出现问题

如果有这么大的块,您必须知道如何将对象放置在此块中。这里的主要问题是对齐,您必须将所有对象放置在符合其最小对齐要求的边界上

从C11开始,可以使用
\u Alignof
操作符获得类型对齐,并且可以使用
aligned\u alloc
请求过度对齐的内存

将所有这些放在一起,如下所示:

  • 计算类型的所有路线的lcm
  • 使用
    aligned\u alloc
    请求足够的内存以该值对齐
  • 将所有对象放置在该路线的倍数上

如果您从通过
void*
指针接收的无类型对象开始,那么别名就不是问题。这个大对象的每个部分都有一个有效的类型,你可以用它来写,见我最近的文章

C标准的相关部分为6.5 p6:

访问其存储值的对象的有效类型为 对象的声明类型(如果有)。87)如果值存储在 通过具有以下类型的左值而没有声明类型的对象 如果不是字符类型,则左值的类型将成为 该访问和后续访问的有效对象类型 不修改存储值的访问。如果复制了一个值 变成
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;
}
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;