我遇到了这样一种情况,对realloc
进行不必要的调用会得到优化。但是,似乎clang和gcc都没有这样做(godbolt)。 -尽管我看到通过多次调用malloc
进行了优化。
示例:
void *myfunc() {
void *data;
data = malloc(100);
data = realloc(data, 200);
return data;
}
我希望将其优化为以下内容:
void *myfunc() {
return malloc(200);
}
为什么clang和gcc都没有对其进行优化? -不允许这样做吗?
答案 0 :(得分:25)
他们不允许这样做吗?
也许,但是在这种情况下没有进行优化可能是由于角落功能的差异。
如果剩余150字节可分配内存,
data = malloc(100); data = realloc(data, 200);
返回NULL
,其中100个字节已消耗(并泄漏),剩余50个字节。
data = malloc(200);
返回NULL
,其中消耗了0个字节(没有泄漏)并保留了150个字节。
功能不同在这种狭窄的情况下可能会阻止优化。
是否允许编译器优化重新分配?
也许-我希望它被允许。然而,增强编译器以确定何时可以这样做可能并不值得。
成功的malloc(n); ... realloc(p, 2*n)
与malloc(2*n);
不同之处在于...
可能已经设置了一些内存。
即使...
即使空代码也没有设置任何内存,可能超出了编译器的设计。
答案 1 :(得分:10)
如果作者认为这样做值得付出努力,那么将自己独立的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; }
可以说这样的实现比大多数其他实现更“高效”,但是必须使它变得非常钝,以使其非常有用,除非在少数情况下,在代码中调用上述函数永远不会执行的路径。
我认为,至少在与原始问题相同的情况下,标准显然会允许优化。即使在可能导致操作失败的情况下,如果不成功,该标准仍会允许它。很可能是,许多编译器未执行优化的原因是,作者认为这些好处不足以证明确定安全和有用的情况所需的努力。
答案 2 :(得分:4)
允许编译器优化对被视为 pure functions 的函数的多次调用,即没有任何副作用的函数。
所以问题是realloc()
是否是纯函数。
C11标准委员会N1570草案声明了有关realloc
功能的信息:
7.22.3.5重新分配功能
...
2.realloc
函数释放由ptr指向的旧对象,并返回指向大小由size指定的新对象的指针。在重新分配之前,新对象的内容应与旧对象的内容相同,直到新旧大小中的较小者为止。新对象中超出旧对象大小的任何字节都具有不确定的值。返回
4.realloc
函数返回一个指向新对象的指针(可能的值与指向旧对象的指针相同),如果新对象不能为空,则返回空指针已分配。
请注意,编译器无法在编译时预测每次调用将返回的指针的值。
这意味着realloc()
不能被视为纯函数,并且编译器也无法优化对其的多次调用。
答案 3 :(得分:1)
但是您没有检查在第二个realloc()中使用的第一个malloc()的返回值。也可能是NULL。
编译器如何在不对第一个调用的返回值进行不必要的假设的情况下将两个调用优化为一个调用?
然后还有另一种可能的情况。 FreeBSD used to have和realloc()
,基本上是malloc + memcpy +释放了旧指针。
假定可用内存仅剩230个字节。在该实现中,ptr = malloc(100)
后跟realloc(ptr, 200)
将失败,但是单个malloc(200)
将成功。
答案 4 :(得分:0)
我的理解是,这样的优化可能是被禁止的(特别是在malloc
成功但随后的realloc
失败的情况下,这确实是不可能的)。
您可以假设malloc
和realloc
总是成功的(这违反了C11标准n1570;也请研究我的joke-implementation of malloc
)。在这种假设下(严格意义上的错误,但是某些Linux系统使用memory overcommitment来给出这种错觉),如果您使用GCC,则可以编写自己的GCC plugin进行优化。< / p>
我不确定花几周或几个月的时间来编写这样的GCC插件是值得的(实际上,您可能希望它有时处理malloc
和realloc
之间的一些代码,然后并不是那么简单,因为您必须表征并检测可以接受的中间代码是什么),但这是您的选择。