【问题标题】:Allocate memory for different types in one block在一个块中为不同类型分配内存
【发布时间】:2018-05-24 16:40:34
【问题描述】:

我对 C 中的动态内存分配有疑问。 我目前正在研究 C 中的人工神经网络实现。 我发现了一个名为 genann 的现有项目,在那里我注意到了一种我不熟悉的内存分配方法。

考虑一个结构:

typedef struct
{
    int an, bn;
    double *a, *b
} foo;

还有一个初始化函数:

foo *foo_init(int an, int bn)
{
    foo *f;

    f = malloc(sizeof(*f));
    f->an = an;
    f->bn = bn;

    f->a = malloc(an*sizeof(*f->a));
    f->b = malloc(bn*sizeof(*f->b));

    return f;
}

这里有一个不同的 init 函数,就像在提到的项目中一样:

foo *foo_init(int an, int bn)
{
    foo *f;

    f = malloc(sizeof(*f) + an*sizeof(*f->a) + bn*sizeof(*f->b));
    f->an = an;
    f->bn = bn;

    f->a = (double*)((char*)f + sizeof(*f)); // Could be f->a = (double*)(f + 1); ?
    f->b = f->a + an;

    return f;
}

所以在想这两种方法有什么区别。我能想到的第二种方法的唯一优点是我只需要分配和释放一个内存块,但由于它只是一个可能只调用一次的 init 函数,因此性能差异应该是微不足道的。另一方面,不同类型的指针指向同一个内存块,但我认为这并不违反严格的别名规则,因为它们指向块内的不同内存(?)。调整大小可能很困难,因为它不能像第一个 init 函数那样使用简单的 realloc 来完成。例如,如果我想将 a 和 b 缩小 1 并重新分配整个内存块,则最后两个 b 值会丢失(而不是一个 a,一个 b)。

我的结论是,以第一种方式分配内存更好,因为第二种方式几乎只有缺点。 init 函数之一是不好的做法吗?我错过了什么,这会使第二个功能更好吗?也许他们在项目中使用第二个是有特殊原因的?

提前致谢。

【问题讨论】:

  • @achal 我不这么认为。
  • @achal: 不应该是*(f->a),但由于-> 绑定更强,我不需要括号。

标签: c dynamic-memory-allocation


【解决方案1】:

如果您分配和释放大量这些结构,所节省的资金可能会产生重大影响。

它使用更少的空间,因为每个分配的块都有一些记录其大小的簿记。它还可以减少内存碎片。

此外,这样做可以确保ab 数组在内存中靠得很近。如果它们经常一起使用,这可以提高缓存命中率。

库实施者经常进行这些微优化,因为他们无法预测库的使用方式,并且他们希望在所有情况下都尽可能好地工作。当您编写自己的应用程序时,您可以更好地了解哪些代码将位于对性能调整非常重要的内部循环中。

【讨论】:

  • 是的。它们在一个内存分配块(而不是三个单独分配的块)中分配单个foo 结构所需的所有存储空间,然后将指针成员设置为指向该块的一部分。这节省了空间和分配时间,也使解除分配结构更容易和更快。
  • @DavidRTribble:但是它是如何节省空间的呢?
  • @Osiris 它节省了malloc()用来记录每个分配块大小的簿记空间。
  • @Barmar:好的,我明白我会保存分配和释放命令。您的意思是,如果我将所有内容分配在一个块中并经常访问 a 和 b 的元素组合,则该值已经在缓存中的可能性很高,因为 CPU 通常会在其中加载整个或部分块,这不会如果 a 和 b 位于完全不同的内存位置,会发生什么情况?
  • 是的,我就是这个意思。这称为“局部性”。
【解决方案2】:

第二种方法问题。由于对齐,以下可能会失败。

f->a = (double*)((char*)f + sizeof(*f));

malloc() 返回的指针对所有对象指针都有效。计算出的((char*)f + sizeof(*f)) 可能不满足对齐要求。在这种的情况下,很可能会起作用,但对于其他typedefs来说却不是这样。

自 C99 起,代码可以使用灵活的数组成员来获得单个分配的advantages 而不会冒 UB 的风险。语法也更简单。

typedef struct {
  int an, bn;
  double *a, *b;
  // Padding will be added here as needed to meet `data[]` alignment needs
  double data[];
} foo;

foo *foo_init(int an, int bn) {
  // `sizeof *f` includes space for `an,bn,a,b` 
  //  and optional alignment padding up to `data`, but not `data`.
  foo *f = malloc(sizeof *f + (an + bn) * sizeof *(f->data));
  if (f) {
    f->an = an;
    f->bn = bn;
    f->a = (double*) (f + 1);  // Guaranteed to align to f->data
    f->b = f->a + an;
  }
  return f;
}

考虑size_t 而不是int an, bn


在 C99 之前有一些方法可以使用 union 来执行此操作。类似的东西

typedef union {
  foo f;
  double data;
} both;

foo *foo_init(int an, int bn) {
  both *p = malloc(sizeof(both) + (an + bn) * sizeof *(f->data));
  foo *f = (foo *) p;
  if (f) {
    f->an = an;
    f->bn = bn;
    //                v--- Note p
    f->a = (double*) (p + 1);  // Guaranteed to align to p->data type
    f->b = f->a + an;
  }
  return f;
}

【讨论】:

  • 但是(double*)(p+1)(double*)((char*)f + sizeof(*f)) 之间实际上有区别吗,因为结构的sizeof 包含填充空间?根据this post,它应该具有相同的结果。它对我来说看起来更干净,这就是为什么我在我的问题中提到它,但我不知道它会有所作为。编辑:哦,你的意思是灵活的数组成员添加了额外的填充,这是必需的?
  • @Osiris 是的 - 我想你有它。 FMA 根据需要添加填充。在第二种情况下,如果需要,也会添加填充,以确保对齐代码应该有一个 both 数组。
猜你喜欢
  • 2011-05-17
  • 2023-04-02
  • 2016-02-13
  • 2015-12-24
  • 2015-05-16
  • 2011-06-02
  • 1970-01-01
  • 1970-01-01
  • 2020-05-23
相关资源
最近更新 更多