【问题标题】:A 90/10 Rule for Memory Management? [closed]内存管理的 90/10 规则? [关闭]
【发布时间】:2009-02-12 15:10:46
【问题描述】:

大多数程序员都同意垃圾收集是一件很棒的事情,并且在大多数应用程序中是非常值得的开销。然而,我个人的观察是,大多数对象的内存管理都是微不足道的,其中可能有 10%-20% 需要考虑诸如引用计数和一般非常复杂的内存管理方案之类的杂项。在我看来,通过保守地手动删除对象生命周期明显的大对象并让 GC 收集其余的对象,假设 GC 实现支持这样的事情,只需一小部分开销即可获得垃圾收集的所有好处.这将使 GC 运行的频率大大降低,消耗的多余内存也更少,同时仍然避免了实际上难以手动管理的情况。更有趣的是,如果编译器在生命周期很明显的地方自动插入确定性删除语句:

int myFunc() {
    Foo[] foo = new Foo[arbitraryNumber];  // May be too big to stack allocate.
    // do stuff such that the compiler can prove foo doesn't escape.
    // foo is obviously no longer needed, can be automatically deleted here.
    return someInteger;
}

当然,这可能不适用于复制 GC,但为了这篇文章,我们假设我们的 GC 没有复制。为什么这种混合内存管理方案在主流编程语言中显然如此罕见?

【问题讨论】:

  • 关于“个人观察”的模糊断言对其他程序员没有帮助。你测量过什么

标签: performance memory-management programming-languages garbage-collection


【解决方案1】:

因为这种情况太少见了。几乎没有方法是孤立的。它们都接受来自外部的对象或创建对象并将它们传递出去。

不访问任何字段、没有参数且不返回任何内容的方法不能做任何事情。

相反,GC 专注于最常见的情况(90%)并尝试控制这 90%(短暂的临时对象)。这意味着在通常情况下,您要检查的对象较少,其余的则无关紧要。接下来,您使用增量扫描(这样您就可以在短时间中断的小冲刺中运行)。

我曾经试图想出一个更好的 GC 算法,但惨遭失败。他们使用一种接近于奥术的方法。关于Java 5 GC Performance Tuning 的文档应该会给你一些想法。当然还有GC article in Wikipedia

归结为:使用 GC 甚至可以比使用传统的内存分配和释放模式更快。想想经典算法,它只是定位任何可到达的对象并将其复制到一个新的地方。如果您刚刚忘记了一大堆对象(例如,所有已分配对象的 90%),则该算法只需要检查其余对象(10%)。任何它无法达到的东西,无论有多少,都无关紧要。现在您可能会说复制很昂贵,但是 a) 这不是真的;今天,一个普通的桌面 CPU 可以在不到 100 毫秒的时间内复制 40MB,并且 b) 复制将保护您免受碎片的影响,因此这实际上是一件好事

【讨论】:

  • 好吧,愚蠢的错误。我基本上是在暗示一个纯函数,但纯函数应该返回一些东西。固定。
  • 检查你一生中写过的所有源代码。我怀疑您会发现许多方法可以做一些有用的事情并且 不适用于外部范围的对象。此外,正如我所解释的,活对象很昂贵,死对象是免费的。
  • 根据我的经验, 应用程序开发人员甚至不知道他们进行的一半分配。也许您只是没有意识到这些琐碎的垃圾箱,因为您没有意识到自己制造了多少垃圾。
  • 他们不需要这样。 GC 在黑暗时代一直是 CPU 的消耗者,但情况已经有了很大改善。随着时间的流逝,我们可以越来越忘记这一点,专注于我们的工作,而不是操作系统的工作原理。
【解决方案2】:

“大多数程序员都同意垃圾收集是一件很棒的事情,并且在大多数应用程序中是非常值得的。”

全面概括...

【讨论】:

  • 我支持这个声明。
  • 我同意垃圾收集是一件很棒的事情,并且在大多数应用程序中是非常值得的开销。 ;-)
  • 别误会,GC 和非 GC 在 IMO 中都有自己的位置。
【解决方案3】:

关于“显然不再需要”的简短说明:这并不容易;)

[...]
Foo() {
    someGlobalList.add(this);
}
[...]

除此之外,您能够手动删除大件物品的想法在我看来是个好主意。据我了解,它至少部分是用现代语言实现的(例如 C# 中的using,遗憾的是它实际上并不是免费的)。但是,没有达到您想要的程度。

【讨论】:

  • 是的,但请考虑“字符串 s1,s2,s3; ... s1 + s2 + s3”的情况。在 s1 和 s2 连接期间分配的内存在此表达式结束之前将是垃圾,保证。 -- 我的理解是各种 Lisp 都有这样的优化。
【解决方案4】:

您所描述的非转义对象的确定性删除语句现代 GC 实现相当有效。大多数分配的对象都是从池中分配的,并在方法退出时非常有效地删除 - 很少有最终在较慢的 GC 堆上。

这实际上产生了与您描述的相同的效果,但程序员干预较少。而且由于垃圾收集器是自动的,因此可以减少人为错误的范围(如果您删除了仍然保留引用的内容怎么办)。

【讨论】:

    【解决方案5】:

    如果您正在创建一个您希望一次仅在一个方法调用的上下文中使用并且没有最终确定的对象,那么我建议在使这种区别。例如,在 C# 中,您可以将实体声明为 struct 而不是 class。然后你的短生命周期、方法本地实例将被分配到堆栈而不是堆上,并且它们将在方法返回的那一刻被隐式释放。

    而且这种技术可以比你原来的想法更进一步,因为结构可以传递给其他方法,而不用担心破坏生命周期分析。

    对于数组,如题中,可以使用stackalloc命令来达到这个效果。

    【讨论】:

    • 我在想这闻起来很像堆栈与堆分配的对象:)
    【解决方案6】:

    这似乎正是 D 管理内存的方式。它收集垃圾,同时允许您在需要时专门删除对象,甚至完全避开 GC 以支持 malloc / free。 scoped 关键字似乎在堆栈上做你想做的事,虽然我不确定它是否真的在堆栈或堆上分配对象。

    【讨论】:

      【解决方案7】:

      虽然将手动清理与垃圾收集相结合可能有一些好处,但让垃圾收集器在不再需要对象和显示它们无法存活的时间之间继续管理对象有很大的好处根引用。除此之外,在非 GC 系统中,在删除对象时通常很难证明任何引用都不可能仍然存在。如果在引用仍然存在时删除对象,则尝试使用该引用可能会导致任意未定义的行为;在没有 GC 的情况下防范这种危险通常是很困难的。相比之下,如果在处理完对象后使对象失效,但将以前占用的内存的重用留给 GC,则可以确保使用失效对象的尝试将以可预测的方式失败。

      顺便说一句,如果我正在设计一个“微框架”,我将为可变和不可变对象设置单独的堆区域。如果代码可以判断自 gen0 或 gen1 被收集后是否已写入对象,则分代垃圾收集效果最佳。用可变对象做出这样的决定比用不可变对象要困难得多。另一方面,手动管理可变对象的有用生命周期通常比管理不可变对象的有用生命周期更容易,因为前者通常应该有一个明确的“所有者”,而后者通常没有。

      【讨论】:

        猜你喜欢
        • 2011-03-12
        • 1970-01-01
        • 2011-05-03
        • 1970-01-01
        • 1970-01-01
        • 2021-09-23
        • 1970-01-01
        • 2018-08-17
        • 1970-01-01
        相关资源
        最近更新 更多