是否可以静态地预测何时仅从源代码中释放内存?


27

在程序执行期间,确定性的时间将内存(和资源锁)返回给OS。程序本身的控制流足以知道可以在哪里释放给定资源。就像人类程序员fclose(file)在程序完成后如何知道在哪里写一样。

GC通过在执行控制流时在运行时直接解决问题来解决此问题。但是,有关控制流的真实性的真正来源是来源。因此,从理论上讲,应该可以free()通过分析源(或AST)来确定在编译之前将调用插入到哪里。

引用计数是实现此目的的一种明显方法,但是很容易遇到仍然引用指针(仍在范围内)但不再需要指针的情况。这只是将手动分配指针的职责转换为手动管理这些指针的作用域/引用的职责。

似乎可以编写一个可以读取程序源代码的程序,并且:

  1. 预测程序控制流的所有排列-达到与观看程序实时执行类似的准确性
  2. 跟踪所有对分配资源的引用
  3. 对于每个引用,遍历整个后续控制流,以找到保证绝对不会取消引用的最早点
  4. 在这一点上,在源代码的那一行插入一个delocation语句

那里有什么已经做到了吗?我不认为Rust或C ++智能指针/ RAII是同一回事。


57
查找停止问题。这就是为什么“编译器无法确定程序是否执行X?”问题的祖父。总是用“不在一般情况下”回答。
棘手怪胎

18
在程序执行期间,确定性的时间将内存(和资源锁)返回给OS。
欣快感,2016年

9
@ratchetfreak谢谢,从来没有人知道过这样的停顿问题,这让我希望我获得计算机科学而不是化学学位。
zelcon

15
@ zelcon5,您现在知道化学反应停止问题了……:)
David Arno

7
@Euphoric,除非您构建程序,否则使用RAII或try-with-resources时,使用资源的界限非常清晰
棘手的怪胎

Answers:


23

举这个(人为的)例子:

void* resource1;
void* resource2;

while(true){

    int input = getInputFromUser();

    switch(input){
        case 1: resource1 = malloc(500); break;
        case 2: resource2 = resource1; break;
        case 3: useResource(resource1); useResource(resource2); break;
    }
}

什么时候应该叫免费?在malloc和Assign to之前resource1我们不能,因为它可能被复制到resource2,在分配给resource2我们之前,我们不能因为我们可能两次从用户那里得到2而没有中间1。

唯一可以确定的方法是测试resource1和resource2在情况1和2中是否不相等,如果不相等则释放旧值。这实际上是引用计数,您知道只有2种可能的引用。


实际上,这不是唯一的方法。另一种方法是只允许一个副本存在。当然,这有其自身的问题。
杰克·艾德利

27

RAII并非自然是同一回事,但具有相同的作用。它为以下问题提供了简单的答案:“您如何知道何时无法再访问它?” 通过使用范围来覆盖使用特定资源时的区域。

您可能要考虑类似的问题“我怎么知道我的程序在运行时不会遭受类型错误?”。解决方案不是通过程序预测所有执行路径,而是通过使用类型注释和推断系统来证明不会出现此类错误。Rust试图将该证明属性扩展到内存分配。

可以编写有关程序行为的证明,而不必解决暂停问题,但前提是您使用某种类型的注释来约束程序。另请参阅安全性证明(sel4等)


评论不作进一步讨论;此对话已转移至聊天
maple_shaft

13

是的,这存在于野外。ML Kit是一种生产质量的编译器,具有(或多或少)描述的策略作为其可用的内存管理选项之一。它还允许使用常规GC或与引用计数混合(您可以使用堆分析器查看哪种策略实际上将为您的程序产生最佳结果)。

ML Kit的原始作者回顾了基于区域的内存管理,回顾了它的成败。最终的结论是,在堆分析器的协助下,该策略是可行的。

(这很好地说明了为什么您通常不应该考虑暂停问题来回答实际的工程问题:我们不希望或不需要解决大多数现实程序的一般情况。)


5
我认为这是正确应用暂停问题的一个很好的例子。暂停问题告诉我们该问题在一般情况下是无法解决的,因此您需要在有限的情况下解决该问题。
Taemyr

请注意,这个问题变得远远可解,当我们谈论纯的或接近纯功能性的,无副作用的语言,如标准ML和Haskell

10

预测程序控制流的所有排列

这就是问题所在。对于任何不平凡的程序而言,排列的数量是如此之大(实际上是无限的),以至于所需的时间和内存将使其完全不切实际。


好点子。我想,如果有的话,量子处理器是唯一的希望
zelcon

4
@ zelcon5哈哈,不。量子计算使情况变得更糟,而不是更好。它向程序添加了其他(“隐藏”)变量,并增加了更多不确定性。我见过的大多数实用的QC代码都依赖于“量子用于快速计算,经典用于确认”。我自己几乎还没有涉足量子计算的表面,但是在我看来,如果没有经典计算机来备份和检查其结果,量子计算机可能不是很有用。
a安

8

暂停问题证明这并非在所有情况下都是可行的。但是,在很多情况下还是有可能的,实际上,几乎所有编译器都可能对大多数变量进行了处理。这样一来,编译器可以告诉您仅在堆栈甚至寄存器上分配一个变量,而不是长期堆存储是安全的。

如果您具有纯函数或确实具有良好的所有权语义,则可以进一步扩展该静态分析,尽管这样做的成本高得令人望而却步,因此您的代码需要花费更多的分支。


好吧,编译器认为它可以释放内存。但事实并非如此。考虑一下常见的初学者错误,即返回指针或对局部变量的引用。琐碎的情况被编译器捕获了,是真的。不那么琐碎的不是。
彼得-恢复莫妮卡

该错误是由程序员使用必须由Peter手动管理内存分配的语言编写的。当编译器管理内存分配时,不会发生此类错误。
Karl Bielefeldt

好吧,您做了一个非常笼统的声明,包括短语“几乎所有编译器”,其中必须包括C编译器。
彼得-恢复莫妮卡

2
C编译器使用它来确定可以将哪些临时变量分配给寄存器。
Karl Bielefeldt

4

如果单个程序员或团队编写整个程序,则可以确定应该释放内存(和其他资源)的设计点是合理的。因此,是的,在更有限的上下文中对设计进行静态分析可能就足够了。

但是,当您考虑第三方DLL,API,框架(以及抛出线程)时,使用程序员很难正确地推断出哪个实体拥有什么内存,并且在所有情况下都不可能。最后一次使用它的时间。我们通常对语言的怀疑没有充分记录浅对象和深对象和数组的内存所有权的转移。如果程序员不能(静态或动态!)对此进行推理,那么编译器很可能也不会这样做。同样,这是由于以下事实:在方法调用或接口等中未捕获内存所有权转移,因此,不可能静态地预测代码中何时或何地释放内存。

由于这是一个严重的问题,因此许多现代语言都选择了垃圾回收,垃圾回收会在上次实时引用后的某个时间自动回收内存。GC具有很高的性能成本(尤其是对于实时应用而言),因此并不是万能的解决方案。此外,使用GC(例如,只会增长的集合)仍然会发生内存泄漏。对于大多数编程练习来说,这仍然是一个很好的解决方案。

有一些替代方案(一些正在出现)。

Rust语言将RAII发挥到了极致。它提供了一种语言构造,用于更详细地定义类和接口方法中的所有权转移,例如,在调用者和被调用者之间转移对象或借用对象的对象,或寿命更长的对象。它为内存管理提供了高度的编译时安全性。但是,它不是一门简单的语言,也不是没有问题(例如,我认为设计不是完全稳定的,某些事情仍在试验中,因此正在改变)。

Swift和Objective-C走了另一条路,这主要是自动引用计数。引用计数会引起循环问题,例如,程序员面临很多挑战,尤其是对于闭包。


3
当然,GC有成本,但也有性能优势。例如,在.NET上,从堆中分配几乎是免费的,因为它使用“堆栈分配”模式-只需增加一个指针即可。我已经看到运行在.NET GC周围的应用程序比使用手动内存分配的应用程序运行速度更快,这确实不是很明确。同样,引用计数实际上是非常昂贵的(只是在GC的不同位置),如果可以避免的话,您就不需要付费。如果需要实时性能,静态分配通常仍然是唯一的方法。
a安

2

如果程序不依赖于任何未知输入,则可以,它应该是可能的(警告:这可能是一个复杂的任务,可能会花费很长时间;但是对于程序也是如此)。这样的程序在编译时是完全可以解决的。用C ++术语来说,它们可能(几乎)完全由constexprs 组成。简单的例子是计算pi的前100位或对已知字典进行排序。


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.