有没有特定的原因垃圾收集不是为C设计的?

我听说C自动收集垃圾是不是最理想的 – 这有什么道理吗?

是否有特定原因没有为C实施垃圾收集?

不要听“C老了,这就是为什么它没有GC”的人。 GC存在基本问题无法克服,这使得它与C不兼容。

最大的问题是准确的垃圾收集需要能够扫描内存并识别遇到的任何指针。 某些更高级别的语言限制整数不使用所有可用位,因此可以使用高位来区分对象引用和整数。 这样的语言然后可以在特殊的字符串区域中存储字符串(可以包含任意八位字节序列),在这些区域中它们不能与指针混淆,并且一切都很好。 但是,AC实现不能这样做,因为字节,更大的整数,指针和其他所有内容可以一起存储在结构,联合或malloc返回的块的一部分中。

如果你抛弃精度要求并决定你可以使用一些永远不会被释放的对象,因为程序中的某些非指针数据与这些对象的地址具有相同的位模式,该怎么办? 现在假设您的程序从外部世界(网络/文件/等)接收数据。 我声称我可以让你的程序泄漏任意数量的内存,并最终耗尽内存,只要我能够猜出足够的指针并在我为程序提供的字符串中模拟它们。 如果你应用De Bruijn序列,这会变得容易多了。

除此之外,垃圾收集速度很慢。 你可以找到数百名喜欢以其他方式声称的学者,但这不会改变现实。 GC的性能问题可分为3大类:

  • 不可预测性
  • 缓存污染
  • 花在所有记忆上的时间

现在声称GC很快的人只是简单地将它与错误的东西进行比较:写得不好的C和C ++程序每秒分配和释放数千或数百万个对象。 是的,这些也会很慢,但至少可以预测,如果有必要,你可以测量和修复。 一个编写良好的C程序将花费很少的时间在malloc / free中,开销甚至无法衡量。

已经为C实现了垃圾收集(例如, Boehm-Demers-Weiser收集器 )。 由于出于多种原因,C未被指定为包含GC – 主要是因为他们所针对的硬件和他们正在构建的系统,它只是没有多大意义。

编辑(回答一些其他提出的指控):

  1. 为了使保守的GC定义良好,你基本上只需要对语言进行一次更改:说任何使指针暂时“不可见”的行为都会导致未定义的行为。 例如,在当前的C中,您可以将指针写入文件,覆盖内存中的指针,稍后将其读回,并且(假设它之前有效)仍然可以访问它指向的数据。 GC不一定“意识到”指针存在,因此它可以看到内存不再可访问,因此可以打开收集,因此后来的解引用不会“起作用”。

  2. 至于垃圾收集是不确定的:有实时收集器是绝对确定的,可用于硬实时系统。 还有用于手动管理的确定性堆管理器,但大多数手动管理器不具有确定性。

  3. 至于垃圾收集缓慢和/或破坏缓存:从技术上讲,这是真的,但它纯粹是一种技术性。 虽然(至少大多数情况下)避免这些问题的设计(例如,代际清除)是众所周知的,但可以认为它们并不完全是垃圾收集(尽管它们对程序员来说几乎完全相同)。

  4. 对于在未知或意外时间运行的GC:与手动管理的内存相比,这不一定或多或少。 您可以在一个单独的线程中运行GC,该线程运行(至少在某种程度上)是不可预测的。 通过手动内存管理来合并空闲块也是如此。 分配内存的特定尝试可能会触发收集周期,导致某些分配比其他分配慢得多; 使用惰性块的惰性合并的手动管理器也是如此。

  5. 奇怪的是,GC与C ++的兼容性远远低于 C ++。大多数C ++依赖于确定性地调用析构函数,但垃圾收集不再是这种情况。 这打破了许多代码 – 编写代码越好,它通常会导致更大的问题。

  6. 同样,C ++要求std::less为指针提供有意义的(更重要的是,一致的)结果,即使它们指向完全独立的对象。 使用复制收集器/清道夫需要一些额外的工作来满足这个要求(但我很确定它是可能的)。 处理(例如)某人散列地址并期望获得一致结果更加困难。 这通常是一个糟糕的想法,但它仍然可能,并应产生一致的结果。

C是在20世纪70年代早期发明的,用于编写操作系统和其他低级东西。 垃圾收集器在附近(例如,Smalltalk的早期版本),但我怀疑他们是否能够在如此轻量级的环境中运行,并且使用非常低级别的缓冲区和指针会有所有复杂性。

对不起,但是说C没有GC,因为它已经老了,或者因为它是针对不同的硬件而设计的,这有点荒谬。

问为什么C没有GC,但是其他语言有点像问“为什么锤子有一个钝端而斧头有一个尖锐的结束”? 嗯,语言是工具,不同的工具意味着构建具有不同要求的不同程序。 某些程序(如设备驱动程序或操作系统内核)的要求使得几乎不可能在满足它们的同时实现垃圾收集,因此需要像C这样的语言,因此创建了C语言。

就像所有其他工具一样,它就是它的方式,因为它要解决的问题迫使它成为那样。 说这是因为它是在70年代创建的,或者是因为旧的硬件使得它看起来像完全是间接的。 如果这是真的,C会在70年代消失,但它仍然是一种流行的语言。

此外,如果没有某种运行时系统,gc很难干净地实现,但C意味着可以在裸硬件上运行。

C是一种非常古老的语言,缺乏许多现代语言的钟声和哨声。 要添加垃圾收集,现在需要重新使用该语言。 一般来说,任何愿意对C进行多次更改的人都会创建自己的语言。

将自动垃圾收集添加到语言上通常会降低性能,或者会导致垃圾收集在非常时间发生。 将垃圾收集添加到C将导致它失去其中一个比较优势,因为它可以用于需要实时或接近实时响应时间的系统级编程。

作为一种语言,C被故意设计为非常小。 它具有很少的内在操作或function,其中大多数反映了CPU中的基本指令。 它通常被称为“便携式汇编语言”。

这使得编写可移植程序以切换电话呼叫变得非常好,这些程序在内存很少的小型计算机上运行,​​这是贝尔实验室在他们第一次使用C和Unix时在70年代早期所做的事情的一部分。

这也使C非常适合编写OS内核,设备驱动程序等。 这样的可执行文件不能指望拥有丰富,复杂的运行时环境,如垃圾收集 – 甚至只是动态内存分配,如malloc()。 那是因为它们实际上构成了这些环境的基础,所以你遇到了“通过你的引导拉动自己”的问题。

人们已经为C编写了垃圾收集的内存分配库。但是库并不是C语言本身的一部分,而且复杂性不可能作为标准C库的一部分被接受。 C程序员对标准的更改非常保守。

C是一种非常低级的语言。 这是你可能选择用垃圾收集等东西编写高级语言的那种语言。它既小又简单,完全符合你的要求。

它需要C ++在C上构建并添加更复杂/自动的内存管理(比如在对象超出范围时调用析构函数)。 然后你可能想知道为什么C ++没有垃圾收集,在这种情况下看看Stroustrup 有什么要说的 :简单来说,人们想要以更直接的方式做事,真正想要它的人可以使用库(也可以用于C)。

我喜欢JWZ关于C.的说法:-)

C是“认为它是语言的PDP-11汇编程序”。

因此,如果你看看如何创建一个可移植的汇编程序,那么C看起来完全没有垃圾收集,原因很简单,CPU没有“ 垃圾收集 ”指令。*

(虽然到目前为止大多数其他CPU指令都被C捕获得很好,但有些情况并非如此,例如Cs缺乏对SIMD操作的表现力等)

*是的,我知道有些人会找一个certificate我错的例子。 但在一般情况下……

Objective_C是ANSI C,添加了12个关键字和一个名为id的类型。 它有垃圾收集。 它使用引用计数。

可以为C实现垃圾收集,但是使用普通的CPU硬件会有点困难和低效。 为了真正使垃圾收集工作得好,人们需要一种使用基本+偏移量寻址的架构; 每个指针的基本部分都需要始终指向已分配区域的开头。 请注意,如果基数是“选择器”而不是线性地址,则32位选择器加上32位偏移量可以访问40亿个对象,每个对象最多可以达到4个演出。 我认为这样的设计对于许多应用来说可能比64位线性寻址更有效,因为对象引用和偏移将使用与64位指针一样多的缓存空间。 使用64位似乎是浪费。

BTW,一种使用基本+偏移地址的架构将比线性寻址系统具有另一个优势:垃圾收集可以与其他操作同时运行。 如果线程在重新定位该特定对象时尝试访问数据对象,那么垃圾收集必须打扰正在运行的线程的唯一时间。 在这种情况下,要么必须中止重定位尝试(假设它是一个非重叠的副本),要么线程必须等待复制该特定对象。 对于实时代码而言,比必须为GC暂停所有线程时存在的情况要好得多。

C的强度(和弱点)是它小巧,简单,重量轻,非常适合嵌入式和系统应用。 GC会增加开销,而且不再那么小而且简单。 GC for C根本不合适 。 它并不复杂。

1972年。

C的设计始于1972年。

更糟糕的是,c是在过时的硬件上设计的。 1972年。

别误会我的意思。 他们在1972年进行了垃圾收集,但到目前为止人们抱怨的所有问题都是当时真正的 ,非常有效的问题。

如果你想进行核心优化,垃圾收集是没有用的,因为如果你决定最好释放分配的空间,那么你可以在项目中比垃圾收集器做得更好。 在20世纪70年代,我不知道开发人员决定不向C包含垃圾收集器的确切原因,可能他们想要对释放进行绝对控制,但我确信后来他们甚至不考虑添加垃圾收集器,因为即使将垃圾收集器集成到非常好的语言中,也构建了许多C项目,并且该语言的新版本必须具有向后兼容性。

编辑:“还有编译器,库和操作系统级机制,用于执行数组边界检查,缓冲区溢出检测,序列化和自动垃圾收集,这些不是C的标准部分。” 这对您来说可能很有意思,您可以在这里阅读更多内容。