【问题标题】:Are compilers allowed to optimize out realloc?允许编译器优化 realloc 吗?
【发布时间】:2019-04-21 17:47:35
【问题描述】:

我遇到了这样一种情况:优化对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 都没有优化它? - 他们不允许这样做吗?

【问题讨论】:

  • 如果允许编译器删除对外部函数的调用,我会感到非常惊讶。如果您链接到自己的实现malloc 的库怎么办?
  • @Gerhardh malloc 不是外部函数,它是标准库的一部分。允许编译器内联它或以其他方式实现它。
  • @Lundin:如果函数包含任何副作用,则不允许编译器优化函数调用,这是不正确的。不允许编译器优化可观察到的行为。如果无法观察到副作用(及其后果),则可以将其移除。
  • @Lundin 有道理,但是,为什么连续两次调用 malloc/free 被优化了(godbolt.org/z/gBVXcp)?如果它有副作用,那是不允许的,不是吗?
  • @Lundin:不需要观察不到的副作用。

标签: c gcc clang language-lawyer compiler-optimization


【解决方案1】:

他们不允许这样做吗?

也许,但在这种情况下未进行优化可能是由于角功能差异。


如果剩余 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); 不同。

确保...(即使是空代码)没有设置任何内存可能超出了该编译器的设计。

【讨论】:

  • 我也是这么想的。但是,this example 表明 realloc 之间会阻止 malloc/free 被优化。如果删除它,编译器将优化掉 malloc 并释放。 - 据我所知,结果不会有任何区别?
  • @Julius Per this code ,我认为没有理由取消优化。但是考虑一下如果两个代码都以char *data = malloc(100); if (data == NULL) { return NULL; } *data = 1 开头,那么功能是不同的。在 realloc() 复制前 100 个字节时,编译器可能看不到复制未初始化的代码在您的代码中并不重要。顺便说一句:第二个编译器是 C++ 而不是 C。建议将 C 与 C 进行比较。
  • @Julius IOWs,成功的malloc(n); ... realloc(p, 2*n)malloc(2*n); 不同,... 可能已经设置了一些内存。确保... 代码没有设置任何内存可能超出了编译器的设计。
  • @chux 这是一个有趣的想法。我可以想象,在某些情况下要证明特定内存区域没有变化是相当困难的——尽管在其他情况下很可能很简单。
  • malloc(n); ... realloc(p, 2*n)malloc(2*n); ... 有何不同?
【解决方案2】:

如果作者认为这样做值得付出努力,那么捆绑了自己的自包含版本 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 明确声明旧对象被释放并返回一个新对象。即使使用私有分配器,编译器也无法在编译时预测指向新分配的指针。
  • @GregA.Woods:根据 as-if 规则,如果结果行为可能是由单独执行操作导致的,则允许编译器合并操作。程序可以通过什么标准定义的方式来观察realloc 是否真的做了任何事情,而不是产生一个指向可能已经像请求一样大的分配的指针?
  • 我想如果知道编译器自己的分配器总是最初分配更多的空间,比如十倍的空间,如果编译器可以在编译时预测分配对象的所需大小是X,并且还预测传递给realloc() 的新大小小于10X,那么它可以假设对象不会改变位置。但是我不知道当前标准是否仍然允许优化。也许吧。
【解决方案3】:

允许编译器优化对被视为 pure functions 的函数的多次调用,即没有任何副作用的函数。

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

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

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

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

请注意,编译器在编译时无法预测每次调用将返回的指针值。

这意味着realloc()不能被认为是一个纯函数,编译器不能优化对它的多次调用。

【讨论】:

【解决方案4】:

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

编译器如何在不对第一个调用的返回值做出无根据的假设的情况下将两个调用优化为一个调用?

然后还有另一种可能的情况。 FreeBSD used to haverealloc() 基本上是 malloc + memcpy + 释放旧指针。

假设只剩下 230 字节的可用内存。在该实现中,ptr = malloc(100) 后跟 realloc(ptr, 200) 将失败,但单个 malloc(200) 将成功。

【讨论】:

  • 您对检查是正确的,但我已在 cmets 中的 least one example 提交,其中包括检查返回值 - 它似乎没有任何区别。实际上,编译器有时会做出这样的假设,我可以demonstrate
【解决方案5】:

我的理解是这样的优化可能会被禁止(尤其是在 malloc 成功但以下 realloc 失败的情况下 - 确实不太可能)。

您可以假设mallocrealloc 总是成功(这违反了C11 标准,n1570;还可以查看我的joke-implementation of malloc)。在那个假设中(严格意义上是错误的,但是一些 Linux 系统有 memory overcommitment 给出这种错觉),如果你使用 GCC,你可能会编写自己的 GCC plugin 来进行这样的优化。

我不确定花几个星期或几个月来编写这样一个 GCC 插件是否值得(实际上,您可能希望它有时处理 mallocrealloc 之间的一些代码,但事实并非如此很简单,因为您必须表征和检测什么样的中间代码是可以接受的),但这个选择是您的。

【讨论】:

    猜你喜欢
    • 2013-10-23
    • 2018-12-30
    • 2021-03-19
    • 1970-01-01
    • 1970-01-01
    • 2019-02-26
    • 2011-10-30
    • 2019-11-22
    相关资源
    最近更新 更多