我听说C自动收集垃圾不是最理想的 - 这有什么道理吗?
是否有特定原因没有为C实现垃圾收集?
答案 0 :(得分:55)
不要听“C老了,这就是为什么它没有GC”的人。 GC存在一些基本问题无法克服,这使得它与C不兼容。
最大的问题是准确的垃圾收集需要能够扫描内存并识别遇到的任何指针。某些更高级语言限制整数不使用所有可用位,因此可以使用高位来区分对象引用和整数。然后,这些语言可以在特殊的字符串区域中存储字符串(可以包含任意八位字节序列),在这些区域中它们不能与指针混淆,并且一切都很好。但是,C实现不能这样做,因为字节,更大的整数,指针和其他所有内容可以一起存储在结构,联合中,或者作为malloc
返回的块的一部分。
如果你抛弃精度要求并决定你可以使用一些永远不会释放的对象,因为程序中的某些非指针数据与这些对象的地址具有相同的位模式会怎样?现在假设您的程序从外部世界(网络/文件/等)接收数据。我声称我可以让你的程序泄漏任意数量的内存,并最终耗尽内存,只要我能够猜出足够的指针并在我为程序提供的字符串中模拟它们。如果您应用De Bruijn Sequences,这会变得更容易。
除此之外,垃圾收集速度很慢。你可以找到数百名喜欢以其他方式声称的学者,但这不会改变现实。 GC的性能问题可分为3大类:
现在声称GC很快的人只是将它与错误的东西进行比较:写得不好的C和C ++程序每秒分配和释放数千或数百万个对象。是的,这些也会很慢,但至少可以预测,如果有必要可以测量和修复。一个写得很好的C程序将在malloc
/ free
花费这么少的时间,以至于开销甚至无法衡量。
答案 1 :(得分:26)
已为C实现 垃圾收集(例如,Boehm-Demers-Weiser collector)。出于多种原因,C未被指定为包含GC,主要是因为他们所针对的硬件和他们正在构建的系统,它只是没有多大意义。
编辑(回答一些其他提出的指控):
为了使保守的GC定义良好,你基本上只需对语言进行一次更改:说任何使指针暂时“不可见”的行为都会导致未定义的行为。例如,在当前的C中,您可以将指针写入文件,覆盖内存中的指针,稍后将其读回,并且(假设之前有效)仍然可以访问它指向的数据。 GC不一定“意识到”指针存在,因此它可以看到内存不再可访问,因此对收集开放,因此后来的解引用不会“起作用”。
至于垃圾收集是非确定性的:有实时收集器是绝对确定的,可用于硬实时系统。还有用于手动管理的确定性堆管理器,但大多数手动管理器不确定性。
至于垃圾收集缓慢和/或颠倒缓存:从技术上讲,这是真的,但它纯粹是一种技术性。虽然(至少大多数情况下)避免这些问题的设计(例如,代际清除)是众所周知的,但是它们可以说它们并不完全是垃圾收集(尽管它们对程序员来说几乎完全相同)。
对于在未知或意外时间运行的GC:与手动管理的内存相比,这不一定或多或少。您可以在一个单独的线程中运行GC,该线程运行(至少在某种程度上)是不可预测的。通过手动内存管理来合并空闲块也是如此。分配内存的特定尝试可能会触发收集周期,导致某些分配比其他分配慢得多;使用惰性块的惰性合并的手动管理器也是如此。
奇怪的是,与C ++相比,GC与C ++相比更少兼容。大多数C ++依赖于确定性地调用析构函数,但垃圾收集不再是这种情况。这会破坏大量代码 - 编写代码越好,通常会导致问题越大。
同样,C ++要求std::less<T>
为指针提供有意义的(更重要的是,一致的)结果,即使它们指向完全独立的对象。使用复制收集器/清道夫需要一些额外的工作来满足这个要求(但我很确定它是可能的)。处理(例如)某人散列地址并期望获得一致结果更加困难。这通常是一个糟糕的想法,但它仍然可能,并且应该产生一致的结果。
答案 2 :(得分:18)
答案 3 :(得分:16)
我很抱歉,但是说C没有GC,因为它已经老了,或者因为它是针对不同的硬件而设计的,这有点荒谬。
问为什么C没有GC,但是其他语言有点像问“为什么锤子有一个钝端而斧头有一个尖锐的结束”?嗯,语言是工具,不同的工具意味着构建具有不同要求的不同程序。某些程序(如设备驱动程序或操作系统内核)的要求几乎不可能实现垃圾收集,同时仍然满足它们,因此需要使用C语言,因此创建了C语言。
就像其他工具一样,它就是这样,因为它要解决的问题迫使它成为那样。说这是因为它是在70年代创建的,或者是因为旧的硬件使得它看起来像完全是间接的。如果这是真的,C会在70年代消失,但它仍然是一种流行的语言。
此外,如果没有某种运行时系统,gc很难干净地实现,但C意味着可以在裸硬件上运行。
答案 4 :(得分:11)
将自动垃圾收集添加到语言上通常会降低性能,或者会导致垃圾收集在不常见的时间发生。将垃圾收集添加到C会导致它失去其中一个比较优势,因为它可以用于需要实时或接近实时响应时间的系统级编程。
答案 5 :(得分:7)
作为一种语言,C故意设计得非常小。它具有很少的内在操作或功能,其中大多数反映了CPU中的基本指令。它通常被称为“便携式汇编语言”。
这使得编写便携式程序以切换周围的电话是很好的,这些程序在内存非常少的小型计算机上运行,这是贝尔实验室在他们第一次使用C和Unix时70年代早期的一部分。
这也使C非常适合编写OS内核,设备驱动程序等。这样的可执行文件不能指望拥有丰富,复杂的运行时环境,如垃圾收集 - 甚至只是动态内存分配,如malloc()。那是因为它们实际上构成了这些环境的基础,所以你会遇到“通过你的引导来解决问题”的问题。
人们已经为C编写了垃圾收集的内存分配库。但是库不是C语言本身的一部分,而且复杂的东西也不太可能被接受为标准C库的一部分。 C程序员对标准的更改非常保守。
答案 6 :(得分:5)
C是一种非常低级的语言。这是你可以选择用垃圾收集等东西来编写高级语言的那种语言。它既小又简单,完全符合你的要求。
C ++需要在C上构建并添加更复杂/自动的内存管理(比如当对象超出范围时调用析构函数)。然后你可能想知道为什么C ++没有垃圾收集,在这种情况下看看Stroustrup has to say:简单地说,人们希望以更直接的方式做事,真正想要它的人可以使用库(也可用于C)。
答案 7 :(得分:5)
我喜欢JWZ关于C.的说法: - )
C是“认为它是一种语言的PDP-11汇编程序”。
因此,如果你看看如何创建一个可移植的汇编程序,那么C看起来完全没有垃圾收集,原因很简单,CPU没有“垃圾收集”指令。* < / p>
(虽然今天大多数其他CPU指令都被C捕获得很好,但有些情况并非如此,例如Cs缺乏对SIMD操作的表现力等。)
*是的,我知道有些人会找一个证明我错的例子。但在一般情况下......
答案 8 :(得分:4)
Objective_C是ANSI C,添加了12个关键字和一个名为id的类型。它有垃圾收集。它使用引用计数。
答案 9 :(得分:3)
可以为C实现垃圾收集,但是使用普通的CPU硬件会有点困难和低效。为了真正使垃圾收集工作得好,人们需要一种使用基本+偏移量寻址的架构;每个指针的基本部分都需要始终指向分配区域的开头。请注意,如果基数是“选择器”而不是线性地址,则32位选择器加上32位偏移量可以访问40亿个对象,每个对象最多可以达到4个演出。我认为这样的设计对于许多应用来说可能比64位线性寻址更有效,因为对象引用和偏移将使用与64位指针一样多的缓存空间。使用64位似乎是浪费。
BTW,一种使用基本+偏移地址的架构将比线性寻址系统具有另一个优势:垃圾收集可以与其他操作同时运行。如果线程在重新定位该特定对象时尝试访问数据对象,则垃圾收集必须打扰正在运行的线程的唯一时间。在这种情况下,要么必须中止重定位尝试(假设它是一个非重叠的副本),要么线程必须等待复制该特定对象。对于实时代码而言,比必须为GC暂停所有线程时存在的情况要好得多。答案 10 :(得分:3)
C的强度(和弱点)是它小巧,简单,重量轻,非常适合嵌入式和系统应用。 GC会增加开销,而且不再那么小而且简单。 GC for C根本不是合适的。它并不复杂。
答案 11 :(得分:2)
1972
C的设计始于1972年。
更糟糕的是,c是在过时的硬件上设计的。 1972年。
别误会我的意思。他们在1972年进行了垃圾收集,但人们今天抱怨的所有问题都是真正的,当时非常有效。
答案 12 :(得分:2)
如果你想进行核心优化,垃圾收集是没有用的,因为如果你决定最好释放分配的空间,那么你可以在项目中做得比垃圾收集器好得多。在20世纪70年代,我不知道开发人员决定不向C包含垃圾收集器的确切原因,可能他们想要对释放进行绝对控制,但是我确信后来他们甚至不考虑添加垃圾收集器,因为即使将垃圾收集器集成到非常好的语言中,也构建了许多C项目,并且该语言的新版本必须具有向后兼容性。
编辑: “还有编译器,库和操作系统级机制,用于执行数组边界检查,缓冲区溢出检测,序列化和自动垃圾收集,这些不是C的标准部分。” 这对您来说可能很有意思,您可以阅读更多here。