【问题标题】:Can compiler combine multiple malloc calls into one?编译器可以将多个 malloc 调用合并为一个吗?
【发布时间】:2021-05-12 15:19:17
【问题描述】:

假设我们有以下两段代码:

    int *a = (int *)malloc(sizeof(*a));
    int *b = (int *)malloc(sizeof(*b));

    int *a = (int *)malloc(2 * sizeof(*a));
    int *b = a + 1;

它们都在堆上分配了两个整数,并且(假设正常使用)它们应该是等价的。第一个似乎更慢,因为它调用 malloc 两次,并且可以产生对缓存更友好的代码。然而,第二个可能是不安全的,因为我们可以通过增加 a 并写入结果指针意外地覆盖 b 指向的值(或者恶意的人可以通过知道 a 的位置立即更改 b 的值)。

上述说法可能不正确(例如,此处质疑速度:Minimizing the amount of malloc() calls improves performance?)但我的问题是:编译器能否进行这种类型的转换,或者两者之间是否存在根本不同?标准?如果可能的话,哪些编译器标志(比如说 gcc)可以允许它?

【问题讨论】:

  • 您应该指定哪种语言;我相信这两种情况的答案是不同的。
  • 很有可能第二个 (double malloc()) 片段使用的空间比第一个片段少。第一个需要两个大量的会计开销,所以在这种情况下ab 中的值相隔超过 4 个字节(在 32 位系统上可能至少相隔 8 个字节,并且在在 64 位系统上至少有 16 个字节,但这些数字可以通过不同的实现无休止地替代)。
  • 您迫切需要养成使用sizeof(int) 的习惯,而不是假设 4。像这样的代码使得从 32 位移植到 64 位远比应有的困难。 (当然,int 的大小相同,但其他类型的大小差别很大。)
  • 试图欺骗优化器,后果自负。
  • 这两个例子都可以写越界。两者都不安全。

标签: c++ c malloc


【解决方案1】:

实际上,不,编译器永远不会自动将 2 个 malloc() 调用组合成一个 malloc() 调用。每次调用malloc() 都会返回一个新内存块的地址,不能保证分配的块将位于彼此靠近的任何位置,并且每个分配的块必须单独为free()'d。因此,没有编译器会假设多个分配块之间的关系并尝试为您优化它们的分配。

现在,可能在一个非常简化的用例中,分配和释放在同一范围内,如果可以证明这样做是安全的,那么编译器供应商可能决定尝试优化,即:

void doIt()
{
    int *a = (int *)malloc(sizeof(*a));
    int *b = (int *)malloc(sizeof(*b));
    ...
    free(a);
    free(b);
}

可以变成:

void doIt()
{
    void *ptr = malloc(sizeof(int) * 2);
    int *a = (int *)ptr;
    int *b = a + 1;
    ...
    free(ptr);
}

但实际上,没有编译器供应商会真正尝试这样做。为了这样一点点的收获,付出努力或冒险是不值得的。而且它无论如何也不会在更复杂的场景中工作,例如:

void doIt()
{
    int *a = (int *)malloc(sizeof(*a));
    int *b = (int *)malloc(sizeof(*b));
    ...
    UseAndFree(a, b);
}

void UseAndFree(int *a, int *b)
{
    ...
    free(a);
    free(b);
}

【讨论】:

  • 我看到编译器完全优化了mallocfree。所以它可能不会将二减为一,但会将二减为零。
  • @EricPostpischil 只有当free()malloc() 的范围内,并且编译器知道该指针没有被用于free() 以外的任何东西时才有可能。
  • 是的,我已经看到了。
  • 您不需要在范围内有free。只需放下指针,malloc 就会被优化掉。
【解决方案2】:

不,它不能,因为编译器(通常)不知道ab 何时可能会得到free()'d,并且如果它将它们都作为单个分配的一部分进行分配,那么它也需要同时free()他们。

【讨论】:

  • 我不确定我是否遵循逻辑。编译器可以在任一指针的最后一个free 处选择free,对吗?在那种情况下会违反 as-if 规则吗?编译器不太可能这样做,但我不明白为什么他们不能
  • @cigien 可以,但为什么呢?分配器已经够复杂了。这使其复杂性达到了全新的水平。
  • 它不会是as-if,如果代码一遍又一遍地这样做的话。 Alloc A+B, free A, alloc C+D, free C, etc...内存耗尽。并且确定这种情况是否发生可能会归结为停机问题,
  • "它不能" 这句话有点太强了。例如,兼容的编译器可以假设为int 大小的块使用专用的固定大小分配器,并独立跟踪这些块。不太可能?是的当然。不可能的?没有。
  • 澄清一下:我并不是说编译器会这样做。但问题是问“可以”,答案是“不”,这似乎是错误的。编译器可以选择在程序结束时释放所有内存(当它绝对安全时)。这将是低效的,但却是一个 QOI 问题;我认为编译器会合规地这样做。
【解决方案3】:

这可能永远不会发生的原因有很多,但最重要的是生命周期,如果这些分配是独立进行的,则可以独立释放。如果一起制作,它们将锁定相同​​的生命周期。

这种细微差别最好由开发人员表达,而不是由编译器决定。

第二个“不安全”是否可以覆盖值?在 C 和扩展 C++ 中,该语言不能保护您免受不良编程的影响。您可以随时使用任何必要的手段向自己的脚开枪:

int a;
int b;

int* p = &a;
p[1] = 9; // Bullet, meet foot

(&b)[-1] = 9; // Why not?

如果你想分配 N 个东西,一定要使用calloc() 来表达它,或者使用适当大小的malloc()。除非有充分的理由,否则单独分配是没有意义的。

通常你不会分配一个int,这有点没用,但在某些情况下,这可能是唯一合理的选择。通常是更大的块,例如完整的struct 或字符缓冲区。

【讨论】:

  • @0___________ 我的打字错误。固定的。谢谢。
  • "如果它们一起被锁定到相同的生命周期" 最后,这是一个实施问题,对此没有任何保证。假设一个固定大小的分配器,其中使用的块用主“位图”中的位标记。那么完全有可能一个malloc 翻转两个 位,然后每个free 翻转一个位。
  • @dxiv 取决于分配器的内部结构以及它与主机操作系统的接口方式。简单的分配器可能会发现这很容易做到,但这并不意味着这是一个好主意。
  • @tadman 重复之前的评论,不太可能 - 当然是,但不可能 - 不。猜猜这取决于您如何阅读 OP 的问题:“可以 编译器做...?”。在“一个普通/理智的编译器会做......”的意义上,答案是否定的。但从“编译器在技术上是否允许梦想做......”的意义上,答案只是可能。
  • @dxiv理论上,编译器可以做无数的事情。我更愿意将范围限制在他们可能做的事情,或者做这样的事情有实际意义的地方。比如“编译器可以检测到代码中的所有错误吗?”或“编译器能否证明 P=NP?”从技术上讲,是的。实际上,没有。
【解决方案4】:

首先:

int *a = (int *)malloc(8);
int *b = a + 4;

不是你想的那样。你想要:

int *a = malloc(sizeof(*a) * 2);
int *b = a + 1;

说明指针算术是你需要学习的东西。

其次:编译器​​不会更改代码中的任何内容,也不会将任何函数调用合并为一个。您尝试实现的是微优化。如果您想使用更大的内存块,只需使用数组即可。

int *a = malloc(sizeof(*a) * 2);

a[0] = 5;
a[1] = 6;
/* some other code */

free(a);

不要使用“魔术”号是malloc 只有sizeof 的对象。不要强制转换 malloc 的结果

【讨论】:

  • 感谢 sizeof 的建议,实际上我不知道我可以在当前声明的变量上使用 sizeof。我已经编辑了我的问题。
【解决方案5】:

我已经用 bignum 库做到了这一点,但你只释放了一个指针。

//initialization every time program runs
extern bignum_t *scratch00;  //these are useful for taylor series, etc.
extern bignum_t *scratch01;
extern bignum_t *scratch02;

.
.
.

bignum_t *bn_malloc(int bignums)
{
    return(malloc(bignums * bn_numbytes));
}

.
.
.

//bignums specific to the program being written at the moment
bignum_t *numerator;
bignum_t *denom;
bignum_t *denom_add;
bignum_t *accum;
bignum_t *term;

.
.
.

numerator = bn_malloc(1);
denom = bn_malloc(1);
denom_add = bn_malloc(1);
accum = bn_malloc(1);
term = bn_malloc(1);

【讨论】:

  • 当然 programmer 可以实现阻塞内存...但问题是关于 compiler 是否可以做到这一点,即作为后台 -无需程序员进行任何特殊步骤即可优化场景。
猜你喜欢
  • 2014-05-30
  • 2016-12-03
  • 2023-03-05
  • 1970-01-01
  • 1970-01-01
  • 2011-02-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多