编译器是否允许优化掉realloc函数?

38

我遇到一个情况,如果能够优化掉不必要的 realloc 调用将非常有用。然而,似乎 Clang 和 GCC 都不会这样做 (Compiler Explorer (godbolt.org)),尽管我看到对多个 malloc 调用进行了优化。

示例:

void *myfunc() {
    void *data;
    data = malloc(100);
    data = realloc(data, 200);
    return data;
}

我期望它被优化为以下内容:

void *myfunc() {
    return malloc(200);
}
为什么Clang和GCC都没有对它进行优化处理?-他们不允许这样做吗?

为什么Clang和GCC都没有对其进行优化处理? - 他们不被允许这样做吗?


10
如果编译器被允许移除对外部函数的调用,我会感到非常惊讶。假如你链接了自己实现 malloc 函数的库,该怎么办? - Gerhardh
12
malloc不是外部函数,它是标准库的一部分。编译器可以自由地将其内联或以其他方式实现。 - n. m.
11
@Lundin:如果一个函数包含任何副作用,编译器就不允许优化掉函数调用这一过程的。编译器不允许优化掉可观察行为。如果一个副作用(及其影响)是不可观察的,那么它可以被删除。 - Eric Postpischil
3
这样做似乎很合理,但是为什么两个连续的malloc/free调用会被优化掉(https://godbolt.org/z/gBVXcp)? 如果它具有副作用,那么这是不允许的,对吗? - Julius
13
一个无法观察到的副作用是不必要的。 - Eric Postpischil
显示剩余8条评论
5个回答

25

他们不能这样做吗?

也许是因为角落里的功能差异,这种情况下没有进行优化。


如果剩余150字节可分配内存,
data = malloc(100); data = realloc(data, 200);将返回NULL,消耗了100字节(并泄漏了)并且剩余50字节。

data = malloc(200);将返回NULL,没有消耗任何字节(也没有泄漏),并且剩余150字节。

在这种特殊情况下具有不同的功能可能会阻止优化。


编译器可以优化掉realloc吗?

也许 - 我认为这是允许的。然而,可能不值得增强编译器以确定何时可以进行优化。

...可能设置了一些内存时,成功的malloc(n); ... realloc(p, 2*n)malloc(2*n);不同。

即使是空代码,那个编译器的设计也可能无法确保...没有设置任何内存。


3
我也在考虑这个。然而,这个例子展示了realloc会阻止编译器优化掉malloc和free。如果你将其移除,编译器就会优化掉malloc和free。据我所见,这不会对结果产生任何影响。 - Julius
2
根据@Julius Per的这段代码,我看不到禁止优化的理由。但是,考虑如果两个代码都以char *data = malloc(100); if (data == NULL) { return NULL; } *data = 1开始,那么这两个函数就不同了。使用realloc()复制前100个字节时,编译器可能没有看到复制未初始化的内容在你的代码中并不重要。顺便说一下:第二个编译器是C++而不是C。建议将C与C进行比较。 - chux - Reinstate Monica
1
@Julius IOWs,成功的malloc(n); ... realloc(p, 2*n)malloc(2*n);不同,当...可能设置了一些内存时。编译器的设计可能超出了...代码未设置任何内存的保证范围。 - chux - Reinstate Monica
2
@chux 这是一个有趣的想法。我可以想象在某些情况下证明特定内存区域没有更改可能会非常困难 - 尽管在其他情况下可能很简单。 - Julius
2
malloc(n); ... realloc(p, 2*n)malloc(2*n); ... 有什么不同? - Andrew Svietlichnyy
显示剩余10条评论

11

如果一个编译器打包了自己独立版本的malloc/calloc/free/realloc,且作者认为这样做值得付出的努力,那么它可以合理地执行所示的优化。即使是链接到外部提供的函数的编译器,如果记录了它不把这些函数的调用顺序视为可观察的副作用,仍然可以执行这样的优化,但这种处理可能会更脆弱一些。

如果在malloc()和realloc()之间没有分配或释放存储空间,当执行malloc()时就已经知道realloc()的大小,并且realloc()的大小大于malloc()的大小,则将malloc()和realloc()操作合并成单个更大的分配可能是有意义的。但是,如果内存状态在此期间发生改变,这种优化可能会导致本应成功的操作失败。例如,给定以下序列:

void *p1 = malloc(2000000000);
void *p2 = malloc(2);
free(p1);
p2 = realloc(p2, 2000000000);

在p1被释放之后,系统可能才有2000000000个字节可用于p2。如果更改代码如下:

void *p1 = malloc(2000000000);
void *p2 = malloc(2000000000);
free(p1);

这将导致分配p2的失败。因为标准从未保证分配请求会成功,所以这种行为不算不符合标准。另一方面,以下也是“符合标准”的实现:

void *malloc(size_t size) { return 0; }
void *calloc(size_t size, size_t count) { return 0; }
void free(void *p) {  }
void *realloc(void *p, size_t size) { return 0; }

这样的实现或许可以被认为比大多数其他实现更加“高效”,但除了在罕见情况下上述函数被调用但从未执行的代码路径中,很难将其视为非常有用。

我认为标准显然允许优化,至少在像原始问题中那样简单的情况下是可以的。即使在可能导致操作失败的情况下,标准仍然允许这种优化。很可能,许多编译器没有执行此优化的原因是作者认为收益不足以证明确定案例安全且有效所需的努力。


标准上来说,不行。C99和C11明确规定旧对象被释放并返回一个新对象。即使使用私有分配器,编译器也无法在编译时预测指向新分配的指针。 - Greg A. Woods
1
根据“好像规则”,如果编译器合并操作后的行为与分别执行这些操作的结果相同,那么编译器就可以进行合并。按照哪些标准定义的方式,程序才能观察到realloc是否真正执行了除返回指向已经达到请求大小的内存分配指针之外的其他操作? - supercat
1
我想,如果编译器自己的分配器总是知道要最初分配多一些空间,比如说十倍于所需空间,而且如果编译器能够在编译时预测到分配对象的期望大小为X,并且还能预测到传递给realloc()的新大小小于10X,那么它可以假设对象不会改变位置。然而,我不知道这种优化是否仍然被当前标准允许。也许可以。 - Greg A. Woods

4

编译器允许优化多次调用被认为是纯函数的函数,即没有任何副作用的函数。

所以问题是realloc()是否是一个纯函数。

C11标准委员会草案N1570对realloc函数进行了如下说明:

7.22.3.5 realloc函数 ... 2. realloc函数释放指针ptr指向的旧对象,并返回一个指针,该指针指向大小由size指定的新对象。新对象的内容应与释放前的旧对象相同,不超过新旧大小的较小者。新对象中超出旧对象大小的任何字节具有未确定值。

返回值 4. realloc函数返回一个指向新对象的指针(可能具有与旧对象指针相同的值),如果无法分配新对象,则返回空指针。

请注意编译器无法在编译时预测每次调用将返回的指针值。这意味着realloc()不能被视为纯函数,编译器无法优化多次调用它。

评论不适合进行长时间的讨论;此对话已被移至聊天室 - user3956566

1

但是你没有检查第一个malloc()的返回值,而你在第二个realloc()中使用了它。它也有可能是NULL。

如果编译器不想对第一个调用进行假设并将其优化为单个调用,那该怎么办?

接下来还有另一种可能的情况。FreeBSD曾经有一个realloc()函数,它基本上是使用malloc + memcpy + free旧指针的方式实现的。

假设只剩下230字节的可用内存。在这种实现中,ptr = malloc(100)后面跟着realloc(ptr, 200)会失败,但是单独执行malloc(200)却会成功。


你对检查的观点是正确的,但我已经在评论中提交了至少一个示例(https://godbolt.org/z/z4mGaT),其中包括检查返回值-似乎没有什么区别。实际上,编译器有时确实会做出这样的假设,我可以进行演示(https://godbolt.org/z/mFIrwX)。 - Julius

0

我的理解是这样的优化可能被禁止(特别是对于-确实不太可能的情况,即malloc成功但随后的realloc失败)。

你可以假设mallocrealloc总是成功的(这违反了C11标准,n1570;还可以看看我关于malloc玩笑实现)。在这个假设下(严格来说是错误的,但一些Linux系统有内存过度承诺来给出这种幻觉),如果你使用GCC,你可以编写自己的GCC插件来进行这样的优化。

我不确定是否值得花费几周甚至几个月的时间编写这样一个GCC插件(实际上,您可能希望它在处理mallocrealloc之间的某些代码时进行处理,那么情况就不是那么简单了,因为您必须表征和检测这种中间代码是否可接受),但这是您自己的选择。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接