C++ 编译器内存优化-重用现有块
假设我要分配2个内存块。 我使用第一个内存块来存储一些东西,并使用这些存储的数据。 然后我使用第二个内存块做类似的事情C++ 编译器内存优化-重用现有块,c++,c,memory-management,compiler-optimization,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
{
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
现在的问题是:如果编译器知道第一个内存块不会被再次引用,编译器是否会对此进行优化,以便重用现有内存块,而不是分配第二个内存块
如果这是真的:
这也适用于动态内存分配吗?
如果内存持续存在于作用域之外,但使用方式与示例中给出的相同,是否也可以这样做?
我假设只有当setup和foo在同一个c文件中实现(与调用代码存在于同一个对象中)时,这才有效
编译器是否对此进行了优化
只有在询问特定编译器时,才能回答此问题。通过检查生成的代码可以找到答案
如果编译器知道第一个内存块不会被再次引用,那么它们会重用现有的内存块,而不是分配第二个内存块吗
这种优化不会改变程序的行为,因此是允许的。另一个问题是:是否有可能证明内存不会被引用?如果可能的话,那么在合理的时间内证明是否足够容易?我很有把握地说,一般来说是不可能证明的,但在某些情况下是可以证明的
我假设只有当setup和foo在同一个c文件中实现(与调用代码存在于同一个对象中)时,这才有效
这通常需要证明内存的不可接触性。理论上,链路时间优化可能会提高这一要求
这也适用于动态内存分配吗
理论上,因为它不会改变程序的行为。但是,动态内存分配通常由库执行,因此编译器可能无法证明没有副作用,因此无法证明删除分配不会改变行为
如果内存持续存在于作用域之外,但使用方式与示例中给出的相同,是否也可以这样做
如果编译器能够证明内存泄漏,那么可能
即使优化是可能的,也不是很重要。节省一点堆栈空间可能对运行时影响很小。如果数组很大,防止堆栈溢出可能会很有用。由于编译器看到
a
被用作函数的参数,它不会优化b
。它不能,因为它不知道在使用a
和b
的函数中发生了什么。对于a
:编译器不知道a
不再使用了
就编译器而言,a
的地址例如可以由setup0
存储在全局变量中,并在使用b
调用时由setup1
使用
如果您遵循该链接,您应该会看到在gcc上编译的代码,并使用-O3优化级别进行编译。生成的asm代码非常简单。由于数组中存储的值在编译时是已知的,编译器可以轻松跳过所有内容,直接设置变量a和b。不需要缓冲区。下面是与示例中提供的代码类似的代码: 您可以看到发送到函数func1和func2的指针不同,因为在调用func1时使用的第一个指针是rsp,在调用func2时使用的是[rsp+48] 您可以看到,在代码是可预测的情况下,要么编译器完全忽略代码。在另一种情况下,至少对于gcc 7和clang 3.9.1,它没有得到优化
虽然不能流利地阅读本文,但很容易看出,在下面的示例中,malloc和free没有通过gcc或clang进行优化(如果您想尝试使用更多编译器,请根据自己的情况,但不要忘记设置优化标志)。 您可以清楚地看到一个对“malloc”的调用,然后是对“free”的调用,两次
优化堆栈空间不太可能真正影响程序的速度,除非您处理大量数据。 优化动态分配的内存更为重要。如果你打算这样做的话,你将不得不使用第三方库或者运行你自己的系统,这不是一个简单的任务
编辑:忘了提及显而易见的一点,这是非常依赖于编译器的。是的,理论上,编译器可以按照您所描述的优化代码,假设它可以证明这些函数没有修改作为参数传入的数组 但实际上,不,这不会发生。您可以编写一个简单的测试用例来验证这一点。我避免了定义helper函数,因此编译器无法内联它们,而是通过const引用传递数组,以确保编译器知道函数不会修改它们:
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);
}
如您所见,没有编译器(GCC、Clang、ICC或MSVC)会将其优化为使用10个元素的单堆栈分配数组。当然,每个编译器在堆栈上分配的空间不同。其中一些原因是由于不同的呼叫约定,可能需要也可能不需要红色区域。否则,这是由于优化器的对齐首选项
以GCC的输出为例,可以立即看出它没有重用数组a
。以下是带注释的反汇编:
;在堆栈上分配104字节
; 通过从堆栈指针中减去RSP。
; (x86上的堆栈始终向下增长。)
副区长,104
; 将堆栈顶部的地址放在RDI中,
; 这就是将数组传递给setup_0()的方式。
移动rdi,rsp
调用设置_0(int const(&)[10])
; 由于安装程序_0()可能已使va崩溃
main:
mov DWORD PTR a[rip], 42
mov DWORD PTR b[rip], 42
xor eax, eax
ret
#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;
}
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
#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;
}
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
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);
}
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);
}