假设我分配了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
现在的问题是:如果编译器知道第一个内存块不会被再次引用,编译器是否对此进行优化,以便重用现有的内存块,而不是分配第二个内存块?
如果为真:这也适用于动态内存分配吗?如果内存在作用域之外持久存在,但使用方式与示例中相同,这也可能吗?我假设这只适用于setup和foo在同一个c文件中实现的情况(与调用代码存在于同一个对象中)?
5条答案
按热度按时间bq3bfh9z1#
编译器是否对此进行优化
这个问题只有在你询问一个特定的编译器时才能得到答案。而答案可以通过检查生成的代码来找到。
以便在编译器知道第一个内存块不会被再次引用的情况下,重用现有的内存块,而不是分配第二个内存块。
这样的优化不会改变程序的行为,所以是允许的。另一个问题是:是否 * 可能 * 证明内存不会被引用?如果可能,那么在合理的时间内证明是否足够容易?我觉得很有把握地说,一般来说不可能证明,但在某些情况下是可以证明的。
我假设这只在setup和foo在同一个c文件中实现时才起作用(与调用代码存在于同一个对象中)?
这通常需要证明内存的不可触及性,而链接时间优化理论上可能会提高这一要求。
这也适用于动态内存分配吗?
从理论上讲,因为它不会改变程序的行为。然而,动态内存分配通常是由库执行的,因此编译器可能无法证明没有副作用,因此无法证明删除分配不会改变行为。
如果内存在作用域之外持续存在,但使用的方式与示例中给出的相同,是否也可能出现这种情况?
如果编译器能够证明内存泄漏,那么也许。
即使优化是可能的,但它并不是很重要。节省一点堆栈空间可能对运行时间的影响很小。如果数组很大,它可能有助于防止堆栈溢出。
tp5buhyn2#
https://godbolt.org/g/5nDqoC
使用带有-O3编译标志的gcc 7编译:
如果你点击链接,你会看到代码在gcc和clang上编译,优化级别为-O3。生成的asm代码非常简单。因为数组中存储的值在编译时就知道了,所以编译器可以很容易地跳过一切,直接设置变量a和b。不需要缓冲区。
遵循与示例中提供的代码类似的代码:
https://godbolt.org/g/bZHSE4
使用带有-O3编译标志的gcc 7编译:
您可以看到发送到函数func 1和func 2的指针是不同的,因为在调用func 1时使用的第一个指针是 rsp,而在调用func 2时使用的第一个指针是 [rsp+48]。
你可以看到编译器要么完全忽略你的代码,如果它是可预测的;要么,至少对于gcc 7和clang 3.9.1,它没有优化。
https://godbolt.org/g/TnV62V
使用带有-O3编译标志的gcc 7编译:
虽然阅读这篇文章并不流利,但很容易看出,在下面的例子中,malloc和free既没有被gcc也没有被clang优化(如果你想尝试使用更多的编译器,请自便,但不要忘记设置优化标志)。
优化堆栈空间不太可能真正影响程序的运行速度,除非你要处理大量的数据。优化动态分配的内存更为重要。AFAIK如果你打算这样做,你将不得不使用第三方库或运行你自己的系统,这不是一个简单的任务。
编辑:忘了提一个显而易见的问题,这是非常依赖编译器的。
mutmk8jj3#
当编译器看到
a
被用作函数的参数时,它不会优化b
。它不能,因为它不知道在使用a
和b
的函数中发生了什么。对于a
也是如此:编译器并不知道a
不再使用。就编译器而言,
a
的地址可以例如由setup0
存储在全局变量中,并且当用b
调用setup1
时将由setup1
使用。6uxekuva4#
简短的回答是:否!编译器无法将此代码优化为您建议的代码,因为它在语义上不等效。详细说明:
a
和b
的生存期简化为完整的块。setup_0
或use_0
中的一个在某个全局变量中存储了指向a
的指针。现在,setup_1
和use_1
可以通过该全局变量与b
(例如,它可以将数组元素a
和b
相加。如果您对代码进行了建议的转换,这将导致未定义的行为。如果您真的想对生命周期进行声明,则必须按以下方式编写代码:还请注意gcc 12.x和clang 15都做了优化。如果你注解掉了花括号,优化就(正确地!)没有完成。
fruv7luv5#
是的,从理论上讲,编译器可以像您描述的那样优化代码,前提是它可以证明这些函数没有修改作为参数传入的数组。
但是实际上,不,这种情况不会发生。你可以写一个简单的测试用例来验证这一点。我避免了定义helper函数,这样编译器就不能内联它们,而是通过常量引用传递数组,以确保编译器知道函数不会修改它们:
正如你所看到的here on Godbolt's Compiler Explorer,没有编译器(愚者,Clang,ICC,或者MSVC)会优化它来使用一个由10个元素组成的堆栈分配数组。当然,每个编译器在堆栈上分配多少空间是不同的。其中一些是由于不同的调用约定,可能需要也可能不需要红色区域。另外,这是由于优化器的对齐偏好。
以愚者的输出为例,您可以立即看出它 * 没有 * 重用数组
a
。那么,为什么呢?当涉及到一个重要的优化时,编译器只是在这里错过了船吗?不是。在堆栈上分配空间是非常快和便宜的。分配~50个字节与~100个字节相比没有什么好处。最好还是安全起见,分别为两个数组分配足够的空间。
如果两个数组都非常大,那么为第二个数组重用堆栈空间 * 可能 * 会有更多的好处,但根据经验,编译器也不会这样做。
这是否适用于动态内存分配?不适用。我从来没有见过一个编译器可以像这样优化动态内存分配,我也不希望看到这样的编译器。这没有意义。如果你想重用内存块,你应该编写代码来重用它,而不是分配一个单独的内存块。
我想你会想,如果你有像下面这样的C代码:
优化器可以看到您正在释放
a
,然后立即重新分配一个与b
大小相同的块?那么,优化器将无法识别这种情况,并省略对free
和malloc
的连续调用,但运行时库free
是一个非常廉价的操作,并且由于刚刚释放了适当大小的块,分配也将非常便宜。(大多数运行时库为应用程序维护一个私有堆,甚至不会将内存返回给操作系统,因此根据内存分配策略,您甚至有可能得到完全相同的块。)