编译器内存优化 - 重用现有块
compiler memory optimization - reusing existing blocks
假设我要分配 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
现在的问题是:如果编译器知道第一个块不会再次被引用,编译器是否对此进行了优化,以便重用现有的内存块,而不是分配第二个内存块?
如果这是真的: 这也适用于动态内存分配吗? 如果内存保留在范围之外,但使用方式与示例中给出的方式相同,这是否也可能? 我认为这仅在同一个 c 文件中实现安装程序和 foo 时才有效(与调用代码存在于同一对象中)?
是否对此进行了优化
只有当您询问特定的编译器时,才能回答此问题。答案可以通过检查生成的代码找到。
以便他们重用现有的内存块,而不是分配第二个内存块,如果编译器知道第一个块不会再次被引用?
这种优化不会改变程序的行为,因此是允许的。另一个问题是:是否有可能证明内存不会被引用?如果有可能,那么在合理的时间内证明是否足够容易?我觉得很安全地说,一般来说不可能证明,但在某些情况下是可以证明的。
我认为这仅在同一个 c 文件中实现安装程序和 foo 时才有效(与调用代码存在于同一对象中)?
这通常需要证明记忆的不可触碰性。从理论上讲,链接时间优化可能会解除这一要求。
这也适用于动态内存分配吗?
从理论上讲,因为它不会改变程序的行为。但是,动态内存分配通常由库执行,因此编译器可能无法证明没有副作用,因此无法证明删除分配不会改变行为。
如果内存保留在范围之外,但使用方式与示例中给出的方式相同,这是否也可能?
如果编译器能够证明内存泄漏,那么也许。
尽管优化是可能的,但它并不是很重要。节省一点堆栈空间可能对运行时影响很小。如果数组很大,防止堆栈溢出可能很有用。
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;
}
使用 gcc 7 和 -O3 编译标志编译:
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;
}
使用 gcc 7 和 -O3 编译标志编译:
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,在调用 func2 时是[rsp+48]。
您可以看到,在可预测的情况下,编译器会完全忽略您的代码。在另一种情况下,至少对于 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;
}
使用 gcc 7 和 -O3 编译标志编译:
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 也没有被 gcc 或 clang 优化(如果你想尝试使用更多的编译器,适合你自己,但不要忘记设置优化标志)。 您可以清楚地看到对"malloc"的调用,然后是对"free"的调用,两次
优化堆栈空间不太可能真正影响程序的速度,除非您操作大量数据。 优化动态分配的内存更为相关。AFAIK 如果您打算这样做,您将不得不使用第三方库或运行自己的系统,这不是一项微不足道的任务。
编辑:忘了提到显而易见的,这是非常依赖于编译器的。
当编译器看到a
用作函数的参数时,它不会优化b
。它不能,因为它不知道使用a
和b
的函数中会发生什么。a
也一样:编译器不知道不再使用a
。
就编译器而言,例如,a
的地址可以由setup0
存储在全局变量中,并在使用b
调用时被setup1
使用。
简短的回答是:不!编译器无法将此代码优化为您建议的内容,因为它在语义上不等效。 长扩展:a
和b
的生命周期对整个块进行了一些简化。 所以现在让我们假设,setup_0
或use_0
中的一个在某个全局变量中存储了一个指向a
的指针。现在允许setup_1
和use_1
通过这个全局变量与b
结合使用a
(例如,它可以添加a
和b
的数组元素。如果您建议的代码转换已完成,这将导致未定义的行为。如果你真的想对生存期做出陈述,你必须按以下方式编写代码:
{
{ // Lifetime block for a
char a[100];
setup_0(a);
use_0(a);
} // Lifetime of a ends here, so no one of the following called
// function is allowed to access it. If it does access it by
// accident it is undefined behaviour
char b[100];
setup_1(b); // Not allowed to access a
use_1(b); // Not allowed to access a
}
另请注意,gcc 12.x 和 clang 15 都会进行优化。如果您注释掉大括号,则优化(正确!)未完成。
是的,理论上,编译器可以按照您的描述优化代码,假设它可以证明这些函数没有修改作为参数传入的数组。
但在实践中,不,这不会发生。您可以编写一个简单的测试用例来验证这一点。我避免定义帮助程序函数,因此编译器无法内联它们,而是通过 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);
}
正如你在Godbolt的编译器资源管理器上看到的,没有编译器(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
那么,什么给了呢?当涉及到重要的优化时,编译器是否只是在这里大量错过了这条船?不。在堆栈上分配空间非常快速且便宜。与 ~100 字节相比,分配 ~50 个字节几乎没有什么好处。不妨安全起见,分别为两个阵列分配足够的空间。
如果两个数组都非常大,则重用第二个数组的堆栈空间可能会有更多的好处,但根据经验,编译器也不会这样做。
这是否适用于动态内存分配?不。强调没有。我从未见过像这样围绕动态内存分配进行优化的编译器,我不希望看到一个。这根本说不通。如果要重用内存块,则需要编写代码来重用它,而不是分配单独的块。
我想你在想,如果你有类似以下 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
大小相同的块?好吧,优化器不会识别这一点并避免对free
和malloc
的背靠背调用,但运行时库(和/或操作系统)很可能会识别。free
是一个非常便宜的操作,而且由于刚刚发布了适当大小的块,因此分配也将非常便宜。(大多数运行时库为应用程序维护一个私有堆,甚至不会将内存返回到操作系统,因此根据内存分配策略,您甚至有可能返回完全相同的块。
- 对于堆上的页面对齐内存分配是否有任何优化或不同的 API?
- C++二和.优化内存使用
- 如何控制或优化或删除或释放 UNION 中未使用的内存
- 如果 RMW 操作没有任何变化,是否可以针对所有内存顺序对其进行优化
- std::stable_sort: 如何选择内存优化算法而不是时间优化算法?
- 字符串编码用于内存优化
- 编译器内存优化 - 重用现有块
- 编译器是否优化析构函数中的内存集
- 矢量函数的C 内存优化
- 如何在CPU和内存中优化C 中的重型地图插入
- C++对间接运算符的标准描述是否保证内存写入不会被优化掉
- 内存分配,用于在C 11中循环中函数的返回值:如何优化
- 编译器优化了内存分配
- C++字符串内存重用优化
- 优化地形渲染的内存
- 内存对齐优化不仅性能,而且内存大小
- 优化数据结构,使其充分利用虚拟内存
- 海量数据集中的内存优化
- 针对大型数组的 C# 内存优化
- 内存优化结构cpp