编译器为何不自动插入解除分配?


63

在像C这样的语言中,程序员应该插入免费的调用。为什么编译器不自动执行此操作?人类在合理的时间内(忽略错误)完成此操作,因此这并非没有可能。

编辑:为了将来参考,是另一个有趣的例子。


125
孩子们,这就是为什么我们教您可计算性理论。;)
拉斐尔

7
这不是可计算性问题,因为人类也无法在所有情况下都做出决定。这是一个完整性问题;释放分配语句包含的信息如果删除则无法通过分析完全恢复,除非该分析包括有关部署环境和预期操作的信息,而C源代码不包含这些信息。
纳特

41
不,这是一个可计算性问题。这是不可判定的内存给定的片段是否应该被释放。对于固定程序,无用户输入或其他外部干扰。
安德烈·鲍尔

1
评论不作进一步讨论;此对话已转移至聊天。所有未明确解决该问题以及如何改进该问题的评论将被删除。
拉斐尔

2
@BorisTreukhov,请带到聊天室。不,我不认为安德烈(Andrej)在说逃避分析是“不可能的”(尽管确切地确定在这种情况下意味着什么对我而言尚不清楚)。完全精确的逃逸分析无法确定的。所有人:请带到聊天室。请仅在此处发表旨在改善问题的评论-其他讨论和评论应在聊天室中发布。
DW

Answers:


80

因为无法确定程序是否会再次使用内存。这意味着free()在所有情况下,没有算法可以正确确定何时调用,这意味着任何尝试执行此操作的编译器都必然会产生一些内存泄漏程序和/或某些继续使用已释放内存的程序。即使您确保编译器从不执行第二个编译器,并且允许程序员插入调用以free()修复这些错误,知道何时调用free()该编译器比free()使用不尝试的编译器知道何时调用更加困难。帮助。


12
我们有覆盖问题,人类的解决不可判定问题的能力。我不能给您一个程序示例,该程序将被错误编译,因为这取决于编译器使用哪种算法。但是任何算法都会为无数个不同的程序产生错误的输出。
David Richerby

1
评论不作进一步讨论;此对话已转移至聊天
吉尔斯

2
伙计们,带它聊天。不一切都直接关系到答案本身,以及它如何可以改进将被删除。
拉斐尔

2
一般来说,编译器所做的很多事情是无法决定的。如果我们总是屈服于莱斯的定理,那么我们在编译器世界将无处可寻。
迪洪

3
这无关紧要。如果对于所有编译器来说都是不确定的,那么对于所有人类来说,也是不确定的。但是我们希望人类能够free()正确插入。
Paul Draper

53

正如大卫·里希比(David Richerby)正确指出的那样,这个问题通常无法确定。对象活动性是程序的全局属性,通常可能取决于程序的输入。

甚至精确的动态 垃圾回收也是一个无法确定的问题!所有现实生活中的垃圾收集器都将可达性用作未来是否需要分配的对象的保守估计。这是一个很好的近似值,但是仍然是一个近似值。

但这通常是正确的。在计算机科学领域,最臭名昭著的解决方案之一是“总的来说这是不可能的,因此我们无能为力”。相反,在许多情况下可以取得一些进展。

基于引用计数的实现非常接近“编译器插入释放”,因此很难分辨出区别。LLVM自动引用计数(在Objective-CSwift中使用)是一个著名的例子。

区域推断编译时垃圾收集是当前活跃的研究领域。事实证明,使用声明性语言(如MLMercury)要容易得多,在这些语言中,创建对象后便无法对其进行修改。

现在,在人员问题上,人员可以通过三种主要方式手动管理分配生存期:

  1. 通过了解程序和问题。例如,人类可以将具有相似寿命的对象放入同一分配对象中。编译器和垃圾收集器必须推断出这一点,但是人类拥有更精确的信息。
  2. 仅在需要时通过有选择地使用非本地簿记(例如参考计数)或其他特殊分配技术(例如区域)。同样,人类可以知道编译器必须在何处进行推断。
  3. 不好 毕竟,每个人都知道在现实世界中部署的程序泄漏缓慢。否则,有时需要围绕内存生存期对程序和内部API进行重组,从而降低可重用性和模块化。

评论不用于扩展讨论。如果您想讨论声明式和功能性,请在chat中进行
吉尔斯

2
到目前为止,这是对该问题的最佳答案(太多的答案甚至都没有解决)。您可能已经添加了有关Hans Boehm在协商GC方面的开拓性工作的参考:en.wikipedia.org/wiki/Boehm_garbage_collector。另一个有趣的观点是,可以相对于抽象语义或执行模型来定义数据活动性(或扩展意义上的有用性)。但是话题确实很广泛。
babou

29

这是一个不完整的问题,而不是一个不确定性的问题

诚然,释放分配语句的最佳位置是不确定的,但这根本不是问题。由于对于人类和编译器来说都是无法确定的,因此,无论是手动还是自动处理,始终无法有意识地选择最佳的解除分配位置是不可能的。而且由于没有人能做到完美,因此在猜测近似最佳位置时,足够先进的编译器应能胜过人类。因此,不确定性不是我们需要显式释放语句的原因

在某些情况下,外部知识会影响释放语句的放置。删除这些语句等效于删除部分操作逻辑,而要求编译器自动生成该逻辑等效于要求其猜测您的想法。

例如,假设您正在编写一个Read-Evaluate-Print-Loop(REPL):用户键入命令,然后程序将执行该命令。用户可以通过在REPL中键入命令来分配/解除分配内存。您的源代码将指定REPL对每个可能的用户命令应执行的操作,包括在用户键入命令时进行的重新分配。

但是,如果C源代码未提供用于释放的显式命令,则编译器将需要推断,当用户将适当的命令输入到REPL中时,编译器应执行释放。该命令是“解除分配”,“免费”还是其他?编译器无法知道您想要的命令是什么。即使您进行逻辑编程以查找该命令字并且REPL找到了该命令字,除非您在源代码中明确告知它,否则编译器也无法知道它应该通过释放来响应它。

tl; dr 问题是C源代码不向编译器提供外部知识。不确定性不是问题,因为无论是手动还是自动化过程都存在不确定性。


3
评论不作进一步讨论;此对话已转移至聊天。所有其他未明确解决此答案的缺陷以及如何解决这些缺陷的注释将被删除。
拉斐尔

23

目前,没有任何答案是完全正确的。

编译器为何不自动插入解除分配?

  1. 有的。(我稍后再解释。)

  2. 通常,您可以free()在程序退出之前调用。但是您的问题中隐含需要free()尽快致电。

  3. free()一旦存储器不可达,何时调用任何C程序的问题是无法确定的,即,对于在有限时间内提供答案的任何算法,都存在无法解决的情况。这-以及任意程序的许多其他不确定性-可以从暂停问题中得到证明。

  4. 不确定的问题无法始终通过任何算法(无论是编译器还是人工算法)在有限时间内解决。

  5. 人类(尝试)写在一个子集 C程序是可以通过他们的算法(自己)来验证内存正确性。

  6. 某些语言通过将#5构建到编译器中来实现#1。它们不允许程序任意使用内存分配,而是允许确定的子集。FothRust是语言的两个示例,它们比C语言具有更多的限制内存分配malloc(),可以(1)检测程序是否在其可决定的集合之外编写(2)自动插入释放。


1
我了解Rust是如何做到的。但是我从未听说过Forth做到了这一点。你能详细说明吗?
米尔顿·席尔瓦

2
@ MiltonSilva,Forth –至少是其最基本的原始实现–仅具有堆栈,没有堆。它使分配/取消分配移动了调用堆栈指针,这是编译器可以轻松完成的任务。Forth的目标是非常简单的硬件,有时非动态内存是可行的。对于非平凡的程序,这显然不是可行的解决方案。
Paul Draper

10

“人类做到这一点,所以这并非不可能”是众所周知的谬论。我们不一定了解(更不用说控制)我们创造的东西了,金钱就是一个常见的例子。我们倾向于高估(有时是大幅度地)在技术问题上取得成功的机会,尤其是在似乎缺少人为因素的情况下。

人类在计算机编程中的表现非常差,计算机科学的研究(许多专业教育计划都缺乏)有助于理解为什么这个问题没有简单的解决方法。我们也许有一天,也许不是很遥远,被工作上的人工智能所取代。即使那样,也不会有一直自动地正确进行重新分配的通用算法。


1
接受人类易失性前提并假设人类创造的思维机器可能仍然是可靠的(即比人类更好)的谬论鲜为人知,但更令人着迷。可以采取行动的唯一假设是人的大脑具有完美计算的潜力
通配符

我从来没有说过思维机器可能是绝对可靠的。在许多情况下,它们已经比人类更好。2.对完美(甚至潜力)的期望是采取行动的前提,这是荒谬的。
安德烈·索萨莱莫斯

“也许有一天,也许不是很遥远,被工作中的人工智能所取代。” 这尤其是胡说八道。人是系统意图的源泉。没有人,系统就没有目的。“人工智能”可以被定义为apparency通过机器智能的当前时刻的决定,实际上带来过去一个程序员或系统设计者的聪明的决定。如果没有维护(必须由人来完成),则AI(或任何未经检查且完全自动化的系统)将失败。
通配符

人与机器一样,意图总是来自外部
安德烈·索萨莱莫斯

完全不正确。(而且,“外部”也没有定义来源。)您是在声明这种意图实际上并不存在,还是在声明该意图存在但并不来自任何地方。也许您认为意图可以独立于目标而存在?在这种情况下,您会误解“意图”一词。无论哪种方式,面对面的演示都会很快改变您对此主题的看法。在此评论之后,我将继续发言,因为仅靠语言无法带来对“意图”的理解,因此在此进行进一步的讨论是没有意义的。
通配符

9

缺少自动内存管理是该语言的功能。

C不应被视为轻松编写软件的工具。它是使计算机执行您要执行的操作的工具。这包括在您选择时分配和取消分配内存。C是您想精确控制计算机或以与语言/标准库设计人员所期望的不同的方式进行操作时使用的低级语言。


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

2
这是对(CS部分)问题的答案?
拉斐尔

6
@Raphael计算机科学并不意味着我们应该寻找晦涩的技术答案。编译器会执行许多通常情况下无法完成的事情。如果我们需要自动内存管理,则可以通过多种方式实现它。C不会这样做,因为它不应该这样做。
朱尼

9

问题主要是历史性的产物,而不是不可能的实现。

大多数C编译器生成代码的方式是使编译器一次只能看到每个源文件。它永远不会看到整个程序。当一个源文件从另一源文件或库中调用函数时,编译器看到的只是带有函数返回类型的头文件,而不是函数的实际代码。这意味着,当有一个返回指针的函数时,编译器无法告诉指针所指向的内存是否需要释放。决定当时未显示给编译器的信息。另一方面,人类程序员可以自由地查找函数或文档的源代码,以找出需要使用指针执行的操作。

如果您研究更现代的低级语言(例如C ++ 11或Rust),您会发现它们主要通过在指针类型中明确显示内存所有权来解决此问题。在C ++中,您将使用a unique_ptr<T>而不是Plain T*来保存内存,并unique_ptr<T>确保当对象到达作用域的末尾时释放内存,这与Plain不同T*。程序员可以将内存从一个unique_ptr<T>转移到另一个,但是永远只能有一个unique_ptr<T>指向该内存。因此,始终很清楚谁拥有内存以及何时需要释放内存。

由于向后兼容的原因,C ++仍然允许使用旧样式的手动内存管理,因此可以创建错误或方法来绕过对a的保护unique_ptr<T>。Rust更加严格,因为它通过编译器错误来强制执行内存所有权规则。

至于不确定性,暂停问题等等,是的,如果您坚持使用C语义,则不可能为所有程序决定何时释放内存。但是,对于大多数实际程序,而不是学术练习或越野车软件,绝对可以决定何时免费和何时不收费。毕竟,这是人类首先可以弄清楚何时应该自由的唯一原因。


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

6

其他答案集中在是否可以进行垃圾回收,如何完成垃圾回收的一些细节以及一些问题上。

一个尚未解决的问题是垃圾收集不可避免的延迟。在C语言中,当程序员调用free()时,该内存可立即用于重用。(至少在理论上是这样!)因此,程序员可以释放其100MB结构,在一毫秒后分配另一个100MB结构,并期望总体内存使用保持不变。

对于垃圾回收,情况并非如此。垃圾收集系统在将未使用的内存返回到堆时会有一些延迟,这可能很重要。如果您的100MB结构超出范围,并且在一毫秒后您的程序设置了另一个100MB结构,则可以合理地期望您的系统在短时间内使用200MB。取决于系统,该“短时间段”可能是毫秒或秒,但仍然存在延迟。

如果您在具有大量RAM和虚拟内存的PC上运行,那么您可能永远不会注意到这一点。但是,如果您在资源更有限的系统(例如嵌入式系统或电话)上运行,则需要认真对待。这不仅是理论上的-我个人已经看到在使用.NET Compact Framework的WinCE系统上工作并使用C#开发时,这会造成问题(例如使设备崩溃)。


理论上,您可以在每次分配之前运行GC。
adrianN

4
@adrianN但是实际上并没有这样做,因为那会很麻烦。Graham的观点仍然成立:无论是在运行时还是在所需的多余内存方面,GC总是会产生大量开销。您可以将这种平衡调整到任一极端,但从根本上讲,您无法消除开销。
康拉德·鲁道夫

释放内存时的“延迟”在虚拟内存系统中比在资源有限的系统中更多。在前一种情况下,即使系统有200MB的可用空间,使用100MB的程序也可能比200MB的程序更好,但是在后一种情况下,除非在某些情况下延迟可以被接受,否则早于所需的运行GC将没有任何好处。部分代码比其他部分
超级猫

我看不到它是如何尝试回答(CS部分)问题的。
拉斐尔

1
@Raphael我已经用垃圾收集原理解释了一个公认的问题,垃圾收集的原理是(或应该说)CS的基本缺点之一。我什至已经给出了亲身实践的经验,以证明这不是纯粹的理论问题。如果您不了解此事,很高兴与您联系,以提高您对该主题的了解。
格雷厄姆

4

这个问题假定一个重新分配是程序员应该从源代码的其他部分推导出来的。不是。“在程序的这一点上,内存引用FOO不再有用”,是只有在程序员将其编码为(使用过程语言)释放语句后,才在程序员心中知道的信息

从理论上讲,它与其他任何代码行都没有什么不同。为什么编译器不自动插入“此时,在程序中,检查寄存器BAR以进行输入”“如果函数调用返回非零,则退出当前子例程”?从编译器的角度来看,原因是“不完整”,如此答案所示。但是,当程序员没有告诉他所知道的一切时,任何程序都会遭受不完整的困扰。

在现实生活中,解除分配是艰巨的工作或样板。我们的大脑会自动填充它们并对此抱怨,并且“编译器可以做得更好或更好”的想法是正确的。从理论上讲,事实并非如此,尽管幸运的是其他语言为我们提供了更多的理论选择。


4
“'在程序的这一点上,内存引用FOO不再有用'是仅在程序员心中已知的信息” –显然是错误的。a)对于许多FOO,弄清楚这一点很简单,例如具有值语义的局部变量。b)您建议程序员总是知道这一点,这显然是过于乐观的假设;如果属实,那么由于不良的内存处理是至关重要的安全性软件,因此我们不会有任何严重的错误。do,我们这样做。
拉斐尔

我只是建议该语言是为程序员确实知道FOO不再有用的情况而设计的。显然,我同意这通常不是真的,这就是为什么我们需要进行静态分析和/或垃圾收集。我们做到了,万岁。但是OP的问题是,这些东西什么时候不像手工编写的dealloc那样有价值?
特拉维斯·威尔逊

4

什么做:有垃圾收集,并有使用引用计数(Objective-C中,斯威夫特)编译器。那些进行引用计数的程序需要避免强大的引用周期,从而需要程序员的帮助。

对“为什么” 的真正答案是,编译器作者尚未想出一种足够好,足够快的方法来使其在编译器中可用。由于编译器编写者通常很聪明,因此您可以得出结论,很难找到一种足够好且足够快的方法。

它非常非常困难的原因之一当然是不确定。在计算机科学中,当我们谈论“可确定性”时,是指“做出正确的决定”。人类程序员当然可以轻松地决定将内存分配到哪里,因为他们不仅限于正确的决定。而且他们经常做出错误的决定。


我在这里看不到任何贡献。
babou

3

在像C这样的语言中,程序员应该插入免费的调用。为什么编译器不自动执行此操作?

因为内存块的生存期是程序员的决定,而不是编译器的决定。

而已。这是C语言的设计。编译器无法知道分配内存块的意图。人类之所以能够做到这一点,是因为他们知道每个内存块的目的,并且当实现此目的时就可以释放它。这是所编写程序设计的一部分。

C是低级语言,因此将您的内存块传递给另一个进程甚至另一个处理器的实例非常常见。在极端情况下,程序员可能会故意分配一块内存,而永远不会再使用它,只是给系统的其他部分施加了内存压力。编译器无法知道是否仍需要该块。


-1

在像C这样的语言中,程序员应该插入免费的调用。为什么编译器不自动执行此操作?

在C语言和许多其他语言中,确实存在一种工具,可以使编译器在编译时明确应完成的情况下做到与之等效:使用自动持续时间变量(即普通局部变量) 。编译器负责为此类变量安排足够的空间,并负责在其(明确定义的)生存期结束时释放该空间。

自C99以来,可变长度数组一直是C的功能,因此,原则上,自动持续时间对象实际上可用于C中所有可计算持续时间的动态分配对象所具有的功能。当然,在实践中,C 实现可能会对VLA的使用施加很大的实际限制-即,其大小可能由于在堆栈上分配而受到限制-但这是实现方面的考虑,而不是语言设计上的考虑。

那些预期用途无法为其提供自动持续时间的对象正是那些在编译时无法确定其寿命的对象。

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.