垃圾收集从 LISP 的早期就已经存在,现在 - 几十年过去了 - 大多数现代编程语言都在使用它。
假设您正在使用其中一种语言,您有什么理由不使用垃圾收集,而是以某种方式手动管理内存分配?
你曾经不得不这样做吗?
如果可能,请给出可靠的例子。
垃圾收集从 LISP 的早期就已经存在,现在 - 几十年过去了 - 大多数现代编程语言都在使用它。
假设您正在使用其中一种语言,您有什么理由不使用垃圾收集,而是以某种方式手动管理内存分配?
你曾经不得不这样做吗?
如果可能,请给出可靠的例子。
我能想到几个:
确定性释放/清理
实时系统
不放弃一半的内存或处理器时间 - 取决于算法
更快的内存分配/释放和特定于应用程序的内存分配、释放和管理。基本上是写你自己的内存东西——通常用于性能敏感的应用程序。这可以在应用程序的行为得到很好理解的情况下完成。对于通用 GC(如 Java 和 C#),这是不可能的。
编辑
也就是说,GC 肯定对社区的大部分人都有好处。它使我们能够更多地关注问题领域,而不是漂亮的编程技巧或模式。不过,我仍然是一个“非托管”C++ 开发人员。在这种情况下,良好的做法和工具会有所帮助。
内存分配?不,我认为 GC 比我更擅长。
但是稀缺的资源分配,如文件句柄、数据库连接等?完成后,我编写代码来关闭它们。GC不会为您这样做。
我做了很多嵌入式开发,问题更可能是是使用 malloc 还是静态分配,而垃圾收集不是一种选择。
我还编写了很多基于 PC 的支持工具,并且很乐意在可用且足够快的地方使用 GC,这意味着我不必使用 pedant::std::string。
我写了很多压缩和加密代码,除非我真的弯曲实现,否则 GC 性能通常不够好。GC 还要求您对地址别名技巧非常小心。我通常用 C 编写性能敏感代码,并从 Python / C# 前端调用它。
所以我的回答是有理由避免 GC,但原因几乎总是性能,然后最好用另一种语言编写需要它的东西,而不是试图欺骗 GC。
如果我在 MSVC++ 中开发一些东西,我从不使用垃圾收集。部分是因为它是非标准的,但也因为我在 C++ 中没有 GC 并且在安全内存回收中自动设计。话虽如此,我认为 C++ 是一个可憎的东西,它无法提供 C 的翻译透明度和可预测性,或者后来的 OO 语言的范围内存安全(除其他外)。
使用垃圾收集器可能很难编写实时应用程序。也许使用在另一个线程中工作的增量 GC,但这是额外的开销。
两个字:空间硬化
我知道这是一个极端情况,但仍然适用。适用于火星探测器核心的编码标准之一实际上禁止动态内存分配。虽然这确实是极端的,但它说明了“部署并忘记它,无忧无虑”的理想。
简而言之,对您的代码实际上对某人的计算机所做的事情有所了解。如果你这样做了,而且你很保守……那就让记忆精灵来处理剩下的事情。当您在四核上开发时,您的用户可能会使用更老的东西,可用的内存要少得多。
使用垃圾收集作为安全网,注意分配的内容。
我能想到的一种情况是,当您处理达到数百兆字节或更多的大型数据集时。根据具体情况,您可能希望在完成后立即释放此内存,以便其他应用程序可以使用它。
此外,在处理一些非托管代码时,您可能希望阻止 GC 收集一些数据,因为非托管部分仍在使用这些数据。尽管我仍然必须想出一个很好的理由,为什么仅仅保留对它的引用可能还不够好。:P
我处理过的一种情况是图像处理。在研究裁剪图像的算法时,我发现托管库的速度不足以同时在大图像或多个图像上进行裁剪。
以合理的速度对图像进行处理的唯一方法是在我的情况下使用非托管代码。这是在 C# .NET 中从事一个小型个人副项目时,我不想学习第三方库,因为项目的规模很大,因为我想学习它以使自己变得更好。可能有一个现有的第三方库(可能是 Paint.NET)可以做到这一点,但它仍然需要非托管代码。
有两种主要类型的实时系统,硬的和软的。主要区别在于硬实时系统要求算法始终在特定的时间预算内完成,而软系统希望它正常发生。软系统可能会使用设计良好的垃圾收集器,尽管普通的垃圾收集器是不可接受的。但是,如果硬实时系统算法没有及时完成,那么生命可能处于危险之中。您会在核反应堆、飞机和航天飞机中找到此类系统,即便如此,也只能在构成操作系统和驱动程序的专业软件中找到。可以说这不是你常见的编程工作。
编写这些系统的人不倾向于使用通用编程语言。Ada 是为编写这类实时系统而设计的。尽管在某些系统中是此类系统的特殊语言,但该语言被进一步缩减为称为 Spark 的子集。Spark 是 Ada 语言的一个特殊的安全关键子集,它不允许的功能之一是创建新对象。对象的 new 关键字被完全禁止,因为它可能会耗尽内存和可变的执行时间。实际上,Spark 中的所有内存访问都是通过绝对内存位置或堆栈变量完成的,并且不会在堆上进行新的分配。垃圾收集器不仅完全无用,而且对保证的执行时间有害。
这些类型的系统并不常见,但在它们存在的地方需要一些非常特殊的编程技术,并且保证执行时间至关重要。
几乎所有这些答案都归结为性能和控制。我在之前的文章中没有看到的一个角度是,跳过 GC 可以通过两种方式为您的应用程序提供更可预测的缓存行为。
假设您正在使用其中一种语言,您有什么理由不使用垃圾收集,而是以某种方式手动管理内存分配?
可能有几个可能的原因:
由于垃圾收集器导致的程序延迟高得无法接受。
回收前的延迟时间长得令人无法接受,例如,在 .NET 上分配一个大数组会将其放入不经常收集的大对象堆 (LOH) 中,因此在变得无法访问后它会停留一段时间。
与垃圾收集相关的其他开销高得令人无法接受,例如写屏障。
垃圾收集器的特性是不可接受的,例如,当 32 位地址空间用尽时,即使理论上有足够的可用空间,.NET 上的加倍数组也会使大对象堆 (LOH) 碎片化,从而导致内存不足。在 OCaml(可能还有大多数 GC 语言)中,具有深线程堆栈的函数运行速度越来越慢。同样在 OCaml 中,线程被 GC 上的全局锁阻止并行运行,因此(理论上)可以通过下降到 C 并使用手动内存管理来实现并行性。
你曾经不得不这样做吗?
不,我从来没有这样做过。我这样做是为了好玩。例如,我用 F#(一种 .NET 语言)编写了一个垃圾收集器,为了让我的计时具有代表性,我采用了一种无分配风格以避免 GC 延迟。在生产代码中,我不得不使用垃圾收集器如何工作的知识来优化我的程序,但我什至不必从 .NET 中规避它,更不用说完全放弃 .NET,因为它强加了 GC。
我最接近放弃垃圾收集的是放弃 OCaml 语言本身,因为它的 GC 会阻碍并行性。但是,我最终迁移到了 F#,它是一种 .NET 语言,因此继承了 CLR 出色的多核 GC。
在视频游戏中,您不想在游戏帧之间运行垃圾收集器。
例如,大坏蛋就在你面前,你的生命值下降到 10。你决定跑向四倍伤害加电。一旦你拿起通电,你就准备好转向你的敌人,用你最强大的武器开火。
当道具消失时,仅仅因为游戏世界必须删除道具的数据而运行垃圾收集器将是一个坏主意。
电子游戏通常通过确定特定地图中需要什么来管理它们的对象(这就是为什么加载包含大量对象的地图需要一段时间的原因)。某些游戏引擎会在某些事件后调用垃圾收集器(保存后,当引擎检测到附近没有威胁时等)。
除了视频游戏,我没有找到任何关闭垃圾收集的充分理由。
编辑:在阅读了其他评论之后,我意识到嵌入式系统和空间强化(分别是 Bill 和 tinkertim 的评论)也是关闭垃圾收集器的好理由
我不太明白这个问题。既然你问的是一种使用 GC 的语言,我假设你是在问像这样的例子
我从来没有找到做#1的理由,但#2是偶尔出现的原因。许多垃圾收集器提供终结机制,这是您绑定到对象的操作,系统在回收对象之前运行该操作。但通常系统无法保证终结器是否实际运行,因此终结器的效用有限。
我在垃圾收集语言中所做的主要事情是密切关注我所做的其他工作的每个单位的分配数量。分配通常是性能瓶颈,尤其是在 Java 或 .NET 系统中。在 ML、Haskell 或 LISP 之类的语言中,这不是什么问题,它们通常设计为程序将疯狂分配的想法。
编辑:对评论的回复更长。
不是每个人都明白,在性能方面,分配器和 GC 必须被视为一个团队。在最先进的系统中,分配是从连续的可用空间(“托儿所”)完成的,并且与测试和增量一样快。但是,除非分配的对象非常短暂,否则该对象会产生债务:它必须从托儿所中复制出来,如果它存在一段时间,它可能会被复制几代。最好的系统使用连续的可用空间进行分配,并在某些时候从复制切换到标记/扫描或标记/扫描/压缩旧对象。所以如果你非常挑剔,你可以忽略分配,如果
否则,分配的对象最初可能很便宜,但它们代表必须稍后完成的工作。即使分配本身的成本是测试和增量,减少分配仍然是提高性能的最佳方法。我已经使用最先进的分配器和收集器调整了数十个 ML 程序,这仍然是正确的;即使使用最好的技术,内存管理也是一个常见的性能瓶颈。
你会惊讶于有多少分配器不能很好地处理非常短寿命的对象。我刚刚从 Lua 5.1.4(可能是最快的脚本语言,带有分代 GC)通过替换 30 个替换序列获得了很大的加速,每个替换都分配了一个大表达式的新副本,同时替换30 个名字,这分配了一份大表达式而不是 30 个。性能问题消失了。
执行越关键,你就越想推迟垃圾收集,但是你推迟垃圾收集的时间越长,它最终会成为一个更大的问题。
使用上下文来确定需要:
1.
2.
3.
4.
在使用垃圾回收的系统中,弱指针有时用于实现简单的缓存机制,因为没有强引用的对象只有在内存压力触发垃圾回收时才会被释放。然而,使用 ARC,值在它们最后一个强引用被删除后立即被释放,使得弱引用不适合这种目的。
参考