编译器内存优化 - 重用现有块

时间:2017-01-27 15:18:03

标签: c++ c memory-management compiler-optimization

说我要分配2个内存块。 我使用第一个内存块来存储内容并使用这些存储的数据。 然后我使用第二个内存块做类似的事情。

{
int a[10];
int b[10];

setup_0(a);
use_0(a);

setup_1(b);
use_1(b);    
}

 || compiler optimizes this to this?
 \/

{
int a[10];

setup_0(a);
use_0(a);

setup_1(a);
use_1(a);  
}

// the setup functions overwrites all 10 words

现在的问题是:编译器是否对此进行了优化,以便它们重用现有的内存块,而不是分配第二个内存块,如果编译器知道第一个块不会再被引用?

如果是这样的话: 这是否也适用于动态内存分配? 如果内存在范围之外仍然存在,但是以与示例中给出的方式相同的方式使用,这也是可能的吗? 我假设这只有在设置和foo在同一个c文件中实现(与调用代码存在于同一个对象中)时才有效吗?

4 个答案:

答案 0 :(得分:5)

  

编译器是否优化了这个

只有在询问特定编译器时才能回答这个问题。通过检查生成的代码可以找到答案。

  

如果编译器知道第一个块不会被再次引用,那么它们会重用现有的内存块而不是分配第二个内存块?

这样的优化不会改变程序的行为,所以它是允许的。另一个问题是:可能是否可以证明内存不会被引用?如果有可能,那么在合理的时间内证明是否容易?我觉得非常安全,说一般不可能证明,但在某些情况下证明是可以证明的。

  

我认为只有在相同的c文件中实现setup和foo(与调用代码存在于同一个对象中)时,这才有效吗?

通常需要证明记忆的不可接触性。理论上,链接时间优化可能会提升这一要求。

  

这是否也适用于动态内存分配?

理论上,因为它不会改变程序的行为。但是,动态内存分配通常由库执行,因此编译器可能无法证明缺乏副作用,因此无法证明删除分配不会改变行为。

  

如果内存在范围之外,但是以与示例中给出的方式相同的方式使用,是否也可以这样做?

如果编译器能够证明内存泄漏,那么也许。

即使可能进行优化,也不是很重要。节省一点堆栈空间可能对运行时间影响很小。如果数组很大,防止堆栈溢出可能很有用。

答案 1 :(得分:3)

https://godbolt.org/g/5nDqoC

#include <cstdlib>

extern int a;
extern int b;

int main()
{
  {
    int tab[1];
    tab[0] = 42;
    a = tab[0];
  }

  {
    int tab[1];
    tab[0] = 42;
    b = tab[0];
  }

  return 0;
}

使用带有-O3编译标志的gcc 7编译:

main:
        mov     DWORD PTR a[rip], 42
        mov     DWORD PTR b[rip], 42
        xor     eax, eax
        ret

如果您按照链接进行操作,您应该会看到在gcc上编译的代码和使用-O3优化级别的clang。由此产生的asm代码非常简单。由于在编译时知道存储在数组中的值,编译器可以轻松跳过所有内容并直接设置变量a和b。不需要你的缓冲区 遵循类似于示例中提供的代码:

https://godbolt.org/g/bZHSE4

#include <cstdlib>

int func1(const int (&tab)[10]);
int func2(const int (&tab)[10]);

int main()
{
  int a[10];
  int b[10];

  func1(a);
  func2(b);

  return 0;
}

使用带有-O3编译标志的gcc 7编译:

main:
        sub     rsp, 104
        mov     rdi, rsp ; first address is rsp
        call    func1(int const (&) [10])
        lea     rdi, [rsp+48] ; second address is [rsp+48]
        call    func2(int const (&) [10])
        xor     eax, eax
        add     rsp, 104
        ret

您可以看到发送到函数func1和func2的指针是不同的,因为在调用func1时使用的第一个指针是 rsp ,而 [rsp + 48] 在调用func2。

您可以看到编译器在可预测的情况下完全忽略您的代码。在另一种情况下,至少对于gcc 7和clang 3.9.1,它没有进行优化。

https://godbolt.org/g/TnV62V

#include <cstdlib>

extern int * a;
extern int * b;

inline int do_stuff(int ** to)
{
  *to = (int *) malloc(sizeof(int));
  (**to) = 42;
  return **to;
}

int main()
{
  do_stuff(&a);
  free(a);

  do_stuff(&b);
  free(b);

  return 0;
}

使用带有-O3编译标志的gcc 7编译:

main:
        sub     rsp, 8
        mov     edi, 4
        call    malloc
        mov     rdi, rax
        mov     QWORD PTR a[rip], rax
        call    free
        mov     edi, 4
        call    malloc
        mov     rdi, rax
        mov     QWORD PTR b[rip], rax
        call    free
        xor     eax, eax
        add     rsp, 8
        ret

虽然不能流利地阅读本文,但很容易通过下面的例子说明,malloc和free没有被gcc或clang优化(如果你想尝试更多的编译器,适合自己,但不要'忘记设置优化标志。 您可以清楚地看到对“malloc”的调用,然后调用“free”,两次

优化堆栈空间不太可能真正影响程序的速度,除非您操纵大量数据。 优化动态分配的内存更具相关性。 AFAIK如果您计划这样做,您将不得不使用第三方库或运行您自己的系统,这不是一项微不足道的任务。

编辑:忘了提及显而易见的,这是非常依赖于编译器的。

答案 2 :(得分:0)

由于编译器发现a用作函数的参数,因此不会优化b。它不能,因为它不知道在使用ab的函数中发生了什么。 a也是如此:编译器不知道a已不再使用。

就编译器而言,a的地址可以是例如setup0。已由setup1存储在全局变量中,并在b调用best_in_place.jquery-ui时由<%= javascript_include_tag "best_in_place.jquery-ui" %>使用。

答案 3 :(得分:-2)

是的,从理论上讲,编译器可以在您描述时优化代码,假设它可以证明这些函数不会修改作为参数传入的数组。

但在实践中,不,那不会发生。您可以编写一个简单的测试用例来验证这一点。我已经避免定义辅助函数,因此编译器不能内联它们,但是通过const-reference传递数组以确保编译器知道函数不会修改它们:

void setup_0(const int (&p)[10]);
void use_0  (const int (&p)[10]);
void setup_1(const int (&p)[10]);
void use_1  (const int (&p)[10]);

void TestFxn()
{
   int a[10];
   int b[10];

   setup_0(a);
   use_0(a);

   setup_1(b);
   use_1(b);
}

正如您所见here on Godbolt's Compiler Explorer,没有编译器(GCC,Clang,ICC或MSVC)会对此进行优化,以使用单个堆栈分配的10个元素数组。当然,每个编译器在堆栈上分配的空间有所不同。其中一些是由于不同的调用约定,可能需要也可能不需要红色区域。否则,这是由于优化程序的对齐首选项。

以GCC的输出为例,您可以立即告诉它重用数组a。以下是反汇编,带有我的注释:

; Allocate 104 bytes on the stack
; by subtracting from the stack pointer, RSP.
; (The stack always grows downward on x86.)
sub     rsp, 104


; Place the address of the top of the stack in RDI,
; which is how the array is passed to setup_0().
mov     rdi, rsp
call    setup_0(int const (&) [10])

; Since setup_0() may have clobbered the value in RDI,
; "refresh" it with the address at the top of the stack,
; and call use_0().
mov     rdi, rsp
call    use_0(int const (&) [10])


; We are now finished with array 'a', so add 48 bytes
; to the top of the stack (RSP), and place the result
; in the RDI register.
lea     rdi, [rsp+48]

; Now, RDI contains what is effectively the address of
; array 'b', so call setup_1().
; The parameter is passed in RDI, just like before.
call    setup_1(int const (&) [10])

; Second verse, same as the first: "refresh" the address
; of array 'b' in RDI, since it might have been clobbered,
; and pass it to use_1().
lea     rdi, [rsp+48]
call    use_1(int const (&) [10])


; Clean up the stack by adding 104 bytes to compensate for the
; same 104 bytes that we subtracted at the top of the function.
add     rsp, 104
ret

那么,是什么给出的?在进行重要优化时,编译器是否只是在这里大量丢失了船?不可以。堆叠上的分配空间极其快速且便宜。分配~50个字节会有很少的好处,而不是~100个字节。也可以安全地玩它并分别为两个阵列分配足够的空间。

如果两个数组都非常大,那么可能在重用第二个数组的堆栈空间方面更有利,但根据经验,编译器也不会这样做。

这是否适用于动态内存分配?不,没有。我从来没有见过像这样优化动态内存分配的编译器,我也不希望看到它。它没有意义。如果你想重新使用内存块,你可以编写代码来重用它而不是分配一个单独的块。

我想你认为如果你有类似下面的C代码:

void TestFxn()
{
   int* a = malloc(sizeof(int) * 10);
   setup_0(a);
   use_0(a);
   free(a);

   int* b = malloc(sizeof(int) * 10);
   setup_1(b);
   use_1(b);
   free(b);
}

优化器可以看到您正在释放a,然后立即重新分配与b大小相同的块?好吧,优化器不会认识到这一点并且忽略了对freemalloc的背靠背调用,但运行时库(和/或操作系统)很可能会这样。 free是一个非常便宜的操作,并且由于刚刚发布了适当大小的块,因此分配也非常便宜。 (大多数运行时库为应用程序维护一个私有堆,甚至不会将内存返回给操作系统,因此根据内存分配策略,甚至可能会得到完全相同的内存分配策略阻止。)