【问题标题】:Is it safe to allocate too little space (if you know you won't need it)?分配太少的空间是否安全(如果您知道不需要它)?
【发布时间】:2011-09-09 02:02:03
【问题描述】:

因此,C99 祝福了常用的“灵活数组成员”hack,使我们能够创建 structs,它可以被过度分配以满足我们的大小要求。我怀疑在大多数理智的实现中这样做是完全安全的,但是如果我们知道在某些情况下我们不需要 struct 的某些成员,那么在 C 中“分配不足”是否合法?

抽象例子

假设我有一个类型:

struct a {
  bool   data_is_x;
  void * data;
  size_t pos;
};

如果data_is_x,那么data的类型就是需要使用pos成员的类型。否则,使用此struct 的函数将不需要pos 成员来获取struct 的此特定副本。本质上,struct 携带有关它是否具有pos 成员的信息,并且此信息在struct 的生命周期内不会更改(在邪恶的恶作剧之外,无论如何都会破坏任何东西) .可以这么说吗:

struct a *a = malloc(data_is_x ? sizeof(struct a) : offsetof(struct a, pos));

只有在需要时才会为pos 成员分配空间?或者它是否违反了使用对于 struct 指针来说太小的强制转换空间的约束,即使您从未使用过有问题的成员?

具体例子

我的实际用例有点复杂;它在这里主要是为了让您了解为什么我要这样做:

typedef struct {
  size_t size;
  void * data;
  size_t pos;
} mylist;

mylist_create 的代码指定,对于 size > 0data 是一个连续数据数组,长度为 size 个项目(无论项目是什么),但对于 size == 0,它是包含项目的双向链表的当前节点。所有与mylists 配合使用的函数都会检查size == 0 是否存在。如果是这样,他们会将数据作为链表处理,“当前”索引是data 指向的任何节点。如果没有,他们会将数据作为数组处理,“当前”索引存储在pos 中。

现在,如果size == 0 我们真的不需要pos 成员,但如果size > 0 我们需要。所以我的问题是,这样做是否合法:

mylist *list = malloc(size ? sizeof(mylist) : offsetof(mylist, pos));

如果我们保证(对未定义行为的惩罚),虽然size == 0,我们永远不会尝试(或需要)访问pos 成员?或者它是否在标准中的某个地方说它是 UB 甚至考虑这样做?

【问题讨论】:

  • 这是一个非常好的问题,我很遗憾地说我不知道​​答案。我希望你给出一个更简单的例子;我有一种感觉,人们很难追随...无论如何+1。
  • @R.. - 这基本上是我的用例;我使用它是因为我认为一个更抽象的例子可能没有意义,并提出了“你为什么要这样做?”的问题。 (当我看到人们试图做一些不明智的事情时,我自己经常会问这个问题)。如果您认为它会使问题更清楚,我可以将其更改为更简单,或者我可以保留它并添加一个更简单的版本。
  • 我会留下它,但更简单的版本可能也不错。

标签: c malloc flexible-array-member


【解决方案1】:

malloc 本身根本不关心你为一个结构分配了多少内存,它是取消引用块外未定义的内存。来自C996.5.3.2 Address and indirection operators

如果给指针分配了无效值,则一元 * 运算符的行为未定义。

而且,从7.20.3 Memory management functions,我们发现(我的斜体):

如果分配成功,则返回的指针经过适当对齐,以便可以将其分配给指向任何类型对象的指针,然后用于访问此类对象或此类对象数组分配的空间中 em>(直到空间被显式释放)。

因此,您可以执行以下操作:

typedef struct { char ch[100]; } ch100;
ch100 *c = malloc (1);

而且,如果您只尝试使用 c->ch[0] 做任何事情,这是完全可以接受的。


对于您的具体示例,假设您关心的是存储空间,我不太确定我会那么担心。如果您出于其他原因担心,请随意忽略这一点,尤其是因为其中包含的假设不是标准强制要求的。

据我了解,您有一个结构:

typedef struct {
  size_t size;
  void * data;
  size_t pos;
} mylist;

您只想使用data,其中size 为0,并且datapos,其中size 大于0。这排除了使用datapos在一个工会中。

大量的malloc 实现会将您请求的空间四舍五入为 16 字节的倍数(或 2 的更大幂),以缓解内存碎片问题。这当然不是标准要求的,但很常见。

假设(例如)32 位指针和size_t,您的 12 字节结构很可能会占用 16 字节的 arena 标头和 16 字节的数据块。即使您只要求 8 个字节(即没有 pos),这个块仍然是 16 个字节。

如果您有 64 位指针和 size_t 类型,它可能会有所不同 - 使用 pos 时为 24 字节,而没有时为 16。

但即便如此,除非您分配 很多 个这些结构,否则这可能不是问题。

【讨论】:

  • 你确定这是定义的吗?我希望它是,但我找不到理由。
  • 这是我的怀疑,但我认为它足够薄,我不妨问问这里的许多优秀的 C 大师是否会挖掘出对禁止这样做的标准的解释。 (尽管在您的示例中,您不需要使用(*c)[0] 吗?)
  • 另外,我认为 paxdiablo 的示例(带有数组)和您的示例(带有结构)的答案可能会有所不同。当然,从“道德”的角度来看,使用 struct 进行这种 hack 似乎更令人反感。
  • 我认为您的标准引用很成功。如果没有人提出反对意见,我明天会接受。
  • 我认为最大的问题是您正在“访问”哪个对象。如果标准中的任何内容暗示您正在“访问”结构/数组,则行为未定义。另一方面,如果您只“访问”元素/成员在分配的空间中,那么它似乎没问题。可能值得阅读标准中关于灵活数组成员的相当迂回的文本,以更好地理解这一点......
【解决方案2】:

这是完全合法的,但你可能应该通过两个结构来使它不那么晦涩,当你阅读它时:

struct leaf_node {
    size_t size;
    void *data;
    size_t pos;
};
struct linked_node {
    size_t size;
    void *next;
};

void *in = ...;

if (*(size_t*)(in) == 0) {
    struct leaf_node *node = in;
    ...
} else {
    struct linked_node *node = in;
    ....
}

这更符合 paxdiablo 引用的标准,您可以将指针强制转换为任何数据指针。如果您这样做,您还将始终确保将其转换为适合分配的缓冲区的结构(一个不必要但方便的壮举)。

paxdiablo 所说的 32 位系统上最小大小为 16 字节的说法通常是正确的,但您仍然可以分配大块来解决这个问题。

在 32 位系统上,linked_node 将使用 8 个字节。您必须使用池才能从您尝试做的事情中受益。

struct leaf_node *leaf_pool = malloc(N*sizeof(struct leaf_node));
struct linked_node *linked_pool = malloc(N*sizeof(struct linked_node));

当然,您永远不应该重新分配池,而是根据需要分配新池并重用节点。在这种情况下,单个 leaf_node 将使用 12 个字节。

同样适用于linked_node,如果您在池中分配它们,它将使用 8 个字节而不是 16 个字节。

只要你的结构没有在 GCC 中使用__attribute__ ((packed)),就不会有性能瓶颈,在这种情况下,你的结构可能会非常糟糕地对齐。尤其是如果你的结构中有一个额外的字符,它的大小为 13 字节。

现在,如果我们回到您最初的问题,您用来指向已分配数据的指针并不重要,只要您确保不访问缓冲区外的数据即可。您的结构本质上就像一个 char 字符串,并且您检查第一个 size_t 是否是“空字节”,如果是,则假设缓冲区较小。如果它不为空,则假定“字符串”更长并且您读取更多数据。涉及完全相同的风险,编译后的唯一区别是每个元素读取的大小。与转换为结构指针和读取元素相比,将[el] 用于字符串并没有什么神奇之处,因为您可以通过使用[el] 轻松导致段错误来验证。

【讨论】:

    【解决方案3】:

    据我所知,任何成员访问也是对聚合的访问,因此声明了一个有效类型,即我们得到一个分配的对象,它太小而无法实际包含其类型的值。

    这闻起来像是未定义的行为,但我实际上无法从标准中支持这一点,而且也有合理的论据支持另一种解释。

    【讨论】:

      【解决方案4】:

      您可能认为您节省了 4 或 8 个字节,但您的内存分配可以对齐。如果你使用 gcc 并且它的 16 字节对齐,你可以得到这样的东西。

      for (int i = 0; i <= 64; i++) {
          char *p = (char *) malloc(i);
          char *q = (char *) malloc(i);
          long long t = q - p;
          cout << "malloc(" << i << ") used " << t << " bytes " << endl;
      }
      

      打印

      malloc(0) used 32 bytes 
      malloc(1) used 32 bytes 
      malloc(2) used 32 bytes 
      malloc(3) used 32 bytes 
      malloc(4) used 32 bytes 
      malloc(5) used 32 bytes 
      malloc(6) used 32 bytes 
      malloc(7) used 32 bytes 
      malloc(8) used 32 bytes 
      malloc(9) used 32 bytes 
      malloc(10) used 32 bytes 
      malloc(11) used 32 bytes 
      malloc(12) used 32 bytes 
      malloc(13) used 32 bytes 
      malloc(14) used 32 bytes 
      malloc(15) used 32 bytes 
      malloc(16) used 32 bytes 
      malloc(17) used 32 bytes 
      malloc(18) used 32 bytes 
      malloc(19) used 32 bytes 
      malloc(20) used 32 bytes 
      malloc(21) used 32 bytes 
      malloc(22) used 32 bytes 
      malloc(23) used 32 bytes 
      malloc(24) used 32 bytes 
      malloc(25) used 48 bytes 
      malloc(26) used 48 bytes 
      malloc(27) used 48 bytes 
      malloc(28) used 48 bytes 
      malloc(29) used 48 bytes 
      malloc(30) used 48 bytes 
      malloc(31) used 48 bytes 
      malloc(32) used 48 bytes 
      malloc(33) used 48 bytes 
      malloc(34) used 48 bytes 
      malloc(35) used 48 bytes 
      malloc(36) used 48 bytes 
      malloc(37) used 48 bytes 
      malloc(38) used 48 bytes 
      malloc(39) used 48 bytes 
      malloc(40) used 48 bytes 
      malloc(41) used 64 bytes 
      malloc(42) used 64 bytes 
      malloc(43) used 64 bytes 
      malloc(44) used 64 bytes 
      malloc(45) used 64 bytes 
      malloc(46) used 64 bytes 
      malloc(47) used 64 bytes 
      malloc(48) used 64 bytes 
      malloc(49) used 64 bytes 
      malloc(50) used 64 bytes 
      malloc(51) used 64 bytes 
      malloc(52) used 64 bytes 
      malloc(53) used 64 bytes 
      malloc(54) used 64 bytes 
      malloc(55) used 64 bytes 
      malloc(56) used 64 bytes 
      malloc(57) used 80 bytes 
      malloc(58) used 80 bytes 
      malloc(59) used 80 bytes 
      malloc(60) used 80 bytes 
      malloc(61) used 80 bytes 
      malloc(62) used 80 bytes 
      malloc(63) used 80 bytes 
      malloc(64) used 80 bytes 
      

      根据您的系统,无论您使用 malloc(0) 还是 malloc(24),都可能使用相同数量的内存。

      【讨论】:

        【解决方案5】:

        在分配中节省 4 个字节几乎毫无意义,除非您谈论的是成千上万个字节,在这种情况下,您可能希望暂时使用具有“释放”结构的池分配方案,但在“可用”列表(“池”),而不是不断释放和重新分配它们。我保证会更快。但要干净利落地使用这样的方案,所有可重复使用的部分都需要能够轻松互换——也就是说,要有“size_t pos”成员。

        所以,是的,你想做的事是完全合法的;我只是不确定这是否值得复杂化和它强加的缺乏灵活性。

        【讨论】:

        • 不过,它并不缺乏灵活性,因为实际上在每种情况下,无论如何我都必须根据条件以不同的方式处理类型数据。无论如何,我必须进行检查并且有两个不同的代码块,所以没有真正增加的复杂性。
        猜你喜欢
        • 1970-01-01
        • 2010-11-26
        • 2014-12-13
        • 2018-06-22
        • 1970-01-01
        • 2014-12-12
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多