【问题标题】:D programming without the garbage collector没有垃圾收集器的 D 编程
【发布时间】:2012-11-14 12:03:43
【问题描述】:

我今天一直在看 D,从表面上看,它看起来很神奇。我喜欢它直接在语言中包含许多更高级别的构造,因此不必使用愚蠢的技巧或简洁的方法。如果 GC,我真正担心的一件事。我知道这是一个大问题,并且已经阅读了很多关于它的讨论。

我自己从这里的一个问题中得出的简单测试表明 GC 非常慢。比做同样事情的直接 C++ 慢 10 倍以上。 (显然测试并没有直接转换成现实世界,但性能损失是极端的,并且会减慢行为相似的现实世界发生(快速分配许多小对象)

我正在考虑编写一个实时低延迟音频应用程序,GC 可能会破坏应用程序的性能,使其几乎无用。从某种意义上说,如果它有任何问题,它将破坏实时音频方面,这更为重要,因为与图形不同,音频以更高的帧速率运行(44000+ vs 30-60)。 (由于它的低延迟,它比可以缓冲大量数据的标准音频播放器更为重要)

禁用 GC 将结果改进到 C++ 代码的 20% 以内。这很重要。最后给出代码供大家分析。

我的问题是:

  1. 用标准的智能指针实现替换 D 的 GC 以使依赖 GC 的库仍然可以使用是多么困难。如果我完全删除 GC,我将失去很多繁重的工作,因为与 C++ 相比,D 已经有限制库。
  2. GC.Disable 是否只是暂时停止垃圾收集(阻止 GC 线程运行),而 GC.Enable 是否会从中断处重新开始。因此,我可能会禁止 GC 在 CPU 使用率较高的情况下运行,以防止出现延迟问题。
  3. 有什么方法可以强制不使用 GC 的模式。 (这是因为我没有用 D 编程,当我开始编写不使用 GC 的眼镜时,我想确保我不会忘记实现自己的清理。
  4. 是否可以轻松替换 D 中的 GC? (不是我想要,但有一天玩不同的 GC 方法可能会很有趣......这类似于我想的 1)

我想做的是用记忆换取速度。我不需要每隔几秒钟运行一次 GC。事实上,如果我可以为我的数据结构正确实现自己的内存管理,那么它可能根本不需要经常运行。只有当内存变得稀缺时,我才可能需要运行它。不过,从我读过的内容来看,您等待调用它的时间越长,它就会越慢。由于通常在我的应用程序中有时我可以毫无问题地调用它,这将有助于减轻一些压力(但话又说回来,可能有几个小时我将无法调用它)。

我不太担心内存限制。我宁愿“浪费”内存而不是速度(当然,在一定程度上)。首先是延迟问题。

根据我的阅读,我至少可以走 C/C++ 的路线,只要我不使用任何依赖 GC 的库或语言结构。问题是,我不知道那些做的。我已经看到提到的字符串、新字符串等,但这是否意味着如果我不启用 GC,我就不能使用内置字符串?

我在一些错误报告中读到 GC 可能确实存在错误,这可以解释其性能问题吗?

另外,D 使用了更多的内存,事实上,D 在 C++ 程序之前就耗尽了内存。我想在这种情况下大约多出 15% 左右。我想那是给 GC 的。

我意识到以下代码并不代表您的普通程序,但它说的是当程序实例化大量对象时(例如,在启动时)它们会慢得多(10 倍是一个很大的因素)。 GC 可能在启动时“暂停”,那么这不一定是问题。

如果我不专门解除分配本地对象,如果我能以某种方式让编译器自动 GC 本地对象,那将是非常好的。这几乎是两全其美。

例如,

{
    Foo f = new Foo();
    ....
    dispose f; // Causes f to be disposed of immediately and treats f outside the GC
               // If left out then f is passed to the GC.
               // I suppose this might actually end up creating two kinds of Foo 
               // behind the scenes. 

    Foo g = new manualGC!Foo();   // Maybe something like this will keep GC's hands off 
                                  // g and allow it to be manually disposed of.
}

事实上,如果能够将不同类型的 GC 与不同类型的数据相关联,并且每个 GC 都是完全自包含的,这可能会很好。这样我就可以根据我的类型调整 GC 的性能。

代码:

module main;
import std.stdio, std.conv, core.memory;
import core.stdc.time;

class Foo{
    int x;
    this(int _x){x=_x;}
}

void main(string args[]) 
{

    clock_t start, end;
    double cpu_time_used;


    //GC.disable();
    start = clock();

    //int n = to!int(args[1]);
    int n = 10000000;
    Foo[] m = new Foo[n];

    foreach(i; 0..n)
    //for(int i = 0; i<n; i++)
    {
        m[i] = new Foo(i);
    }

    end = clock();
    cpu_time_used = (end - start);
    cpu_time_used = cpu_time_used / 1000.0;
    writeln(cpu_time_used);
    getchar();
}

C++ 代码

#include <cstdlib>
#include <iostream>
#include <time.h>
#include <math.h>
#include <stdio.h>

using namespace std;
class Foo{
public:
    int x;
    Foo(int _x);

};

Foo::Foo(int _x){
    x = _x;
}

int main(int argc, char** argv) {

    int n = 120000000;
    clock_t start, end;
    double cpu_time_used;




    start = clock();

    Foo** gx = new Foo*[n];
    for(int i=0;i<n;i++){
        gx[i] = new Foo(i);
    }


    end = clock();
    cpu_time_used = (end - start);
    cpu_time_used = cpu_time_used / 1000.0;
    cout << cpu_time_used;

    std::cin.get();
    return 0;
}

【问题讨论】:

  • @AbstractDissonance 在此期间肯定有些事情发生了变化(例如 GC 的性能)。然而,你测量的是不同的东西。在 C++ 基准示例中,您不会释放已创建对象的分配内存。因此,在 C++ 中根本没有测量释放内存所需的时间。但是,D 示例中的垃圾收集器应该至少运行一次,如果不是更频繁的话。在您停止时间之前,它可能不会释放所有内存。在我看来,如果您在 C++ 中释放内存并在 D 中的循环之后显式调用 GC,您会获得或多或少的可比时间。

标签: garbage-collection d


【解决方案1】:
  1. D 几乎可以使用任何 C 库,只需定义所需的函数即可。 D 也可以使用 C++ 库,但 D 不理解某些 C++ 结构。所以... D 可以使用几乎与 C++ 一样多的库。它们只是不是原生 D 库。

  2. 来自 D 的图书馆参考。
    核心内存:

    static nothrow void disable();
    

    禁用执行的自动垃圾收集以最小化进程占用空间。在实现认为正确的程序行为所必需的情况下,例如在内存不足的情况下,收集可能会继续发生。此函数是可重入的,但每次调用禁用时必须调用一次启用。

    static pure nothrow void free(void* p);
    

    释放 p 引用的内存。如果 p 为空,则不执行任何操作。如果 p 引用了最初不是由这个垃圾收集器分配的内存,或者它指向内存块的内部,则不会采取任何措施。无论是否设置了 FINALIZE 属性,该块都不会最终确定。如果需要最终确定,请改用 delete。

    static pure nothrow void* malloc(size_t sz, uint ba = 0);
    

    从垃圾收集器请求一个对齐的托管内存块。此内存可以通过调用 free 随意删除,也可以在收集运行期间自动丢弃和清理。如果分配失败,此函数将调用 onOutOfMemory,预计会抛出 OutOfMemoryError。

    所以是的。在这里阅读更多:http://dlang.org/garbage.html

    在这里:http://dlang.org/memory.html

    如果你真的需要上课,看看这个:http://dlang.org/memory.html#newdelete delete 已被弃用,但我相信你仍然可以 free() 它。

  3. 不要使用类,使用结构。结构是堆栈分配的,类是堆的。除非您需要多态性或其他类支持的东西,否则它们对于您正在做的事情来说是开销。如果你愿意,你可以使用 malloc 和 free。

  4. 或多或少...在此处填写函数定义:https://github.com/D-Programming-Language/druntime/blob/master/src/gcstub/gc.d。设置了一个 GC 代理系统,允许您自定义 GC。所以这并不是设计师不希望你做的事情。

这里有一点 GC 知识: 垃圾收集器不能保证为所有未引用的对象运行析构函数。此外,没有指定垃圾收集器为未引用对象调用析构函数的顺序。这意味着,当垃圾收集器为具有引用垃圾收集对象的成员的类的对象调用析构函数时,这些引用可能不再有效。这意味着析构函数不能引用子对象。此规则不适用于自动对象或使用 DeleteExpression 删除的对象,因为垃圾收集器未运行析构函数,这意味着所有引用都是有效的。

导入std.c.stdlib;那应该有malloc和free。

导入核心.内存; this 有 GC.malloc, GC.free, GC.addroots, //添加外部内存到 GC...

字符串需要 GC,因为它们是不可变字符的动态数组。 ( immutable(char)[] ) 动态数组需要 GC,静态不需要。

如果您想要手动管理,请继续。

import std.c.stdlib;
import core.memory;

char* one = cast(char*) GC.malloc(char.sizeof * 8);.
GC.free(one);//pardon me, I'm not used to manual memory management. 
//I am *asking* you to edit this to fix it, if it needs it.

为什么要为 int 创建一个包装类?你只是在放慢速度和浪费内存。

class Foo { int n; this(int _n){ n = _n; } }
writeln(Foo.sizeof);  //it's 8 bytes, btw
writeln(int.sizeof);  //Its *half* the size of Foo; 4 bytes.


Foo[] m;// = new Foo[n]; //8 sec
m.length=n; //7 sec minor optimization. at least on my machine.
foreach(i; 0..n)
    m[i] = new Foo(i);


int[] m;
m.length=n; //nice formatting. and default initialized to 0
//Ooops! forgot this...
foreach(i; 0..n)
    m[i] = i;//.145 sec

如果你真的需要,那么在 C 中编写 Time-sensitive 函数,并从 D 中调用它。 哎呀,如果时间真的很重要,请使用 D 的内联汇编来优化一切。

【讨论】:

  • 为了所有神圣的爱,如果我错了,有人纠正我。记忆不是我的强项。
  • 从个人经验来看 - 如果人们只想编写对 GC 友好的代码,90% 的性能抱怨都会消失......
  • 我看不到 int 周围的包装器是如何浪费内存的。你能解释一下吗?
  • 我的印象是一个对象包含的不仅仅是它所包装的内容的总和。 dlang.org/abi.html
  • 但是我不想从头开始,我不想扔掉字符串、数组切片等,因为它们很有用,是什么让 D 比 C 更有用。我只是想避免“哦该死,D 的 GC 完全阻止了我的应用程序的使用。(在我的应用程序中,如果由于音频丢失而出现“pops”和“clicks”,那么应用程序就没有用了)。现在,1.34 或 2.1 的系数可能没问题,但是 10 的因数太可怕了。你可能会说在现实世界中它不会是 10,这可能是真的。但这证明了 D 的 GC 存在性能问题。
【解决方案2】:

由于尚未关闭,最近版本的 D 具有 std.container 库,其中包含一个 Array 数据结构,该结构在内存方面比内置数组更有效。我无法确认库中的其他数据结构也很有效,但如果您需要更多的内存意识而不必求助于手动创建不需要垃圾收集的数据结构,则可能值得研究。

【讨论】:

    【解决方案3】:

    您也可以只分配所需的所有内存块,然后使用内存池在没有 GC 的情况下获取块。

    顺便说一句,它没有你提到的那么慢。而且 GC.disable() 并没有真正禁用它。

    【讨论】:

    • 对象池是视频游戏中常见的一种模式,它也可以极大地帮助音频应用程序。我会创建一系列池,每种类型的信息都需要一个(样本块、压缩数据部分等),并且总是从这些池中请求并在不再需要时将控制权返回给它们。因此,您可以限制内存碎片(与您无关),但还要确保您的小实例都不需要收集,因为它们始终包含在结构中。
    • 另一个好点是reuse() 模式,这意味着通过在已创建的对象上调用 ctor 来重用对象,防止任何 GC free 或 alloc(它实际上不是 ctor 调用,但它是类似)。
    【解决方案4】:

    我们可能会从不同的角度看待这个问题。您作为问题的基本原理提到的分配许多小对象的次优性能与 GC 无关。相反,这是通用(但次优)和高性能(但任务专用)内存管理工具之间的平衡问题。想法是:GC 的存在并不妨碍您编写实时应用程序,您只需要针对特殊情况使用更具体的工具(例如,object pools)。

    【讨论】:

      【解决方案5】:

      D 的 GC 根本不像 Java 那样复杂。它是开源的,因此任何人都可以尝试改进它。

      有一个名为 CDGC 的实验性并发 GC,并且有一个当前的 GSoC 项目来移除全局锁定:http://www.google-melange.com/gsoc/project/google/gsoc2012/avtuunainen/17001

      确保使用 LDC 或 GDC 进行编译以获得更好的优化代码。

      XomB 项目也使用自定义运行时,但我认为它是 D 版本 1。 http://wiki.xomb.org/index.php?title=Main_Page

      【讨论】:

        【解决方案6】:

        我建议你阅读这篇文章:http://3d.benjamin-thaut.de/?p=20 在那里你会发现一个标准库版本,它自己管理内存并完全避免垃圾收集。

        【讨论】:

        • 谢谢,我实际上遇到过一次,这是我对 D 可以做什么的第一个想法。很高兴知道有人在这方面做了一些工作以及会得到什么样的结果(基本上是他的“现实世界”应用程序中的一个因素 3,尽管由于延迟问题我的时间更紧迫)。根据我的阅读,他的图书馆并不完整。我会看看它,看看它会把我带到哪里。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-12-30
        • 1970-01-01
        相关资源
        最近更新 更多