• d不扫描来缩短垃集


    原文
    无指针类型GC内存块不会增加gc暂停时间.

    当前实现中的GC优化技巧

    对D的GC来说,最差之一是有个很大的活动堆.它接近内存极限,垃集,降低一点,赋值,又回到极限,你花费大量时间在GC上,甚至无法成功收集!最后,一遍又一遍地扫描所有内存,只为了释放一点点.
    写屏障支持的分代GC,给出一个方案,也许GC也可检测,一趟并未释放太多,并延迟下一趟.

    GC.disable策略

    也可在小规模中出现.考虑直接赋值循环,比如

    while(true) array ~= stuff;
    
    • 1

    GC时间增长,来使用越来越多运行时,但几乎不会释放(但一些数组片段,因为被复制到新的,更大的块,可能会).
    因此,此时停止垃集,可能会提高整体性能,程序一次提供所有内存,并不再收集,直到完成工作.编译器现在不能检测.但可以!

    程序员可知道这里,并设置GC.disable一段时间,来纯赋值循环,然后结束时用GC.enable.通过避免你知道不必要的扫描并延迟工作来显著改善时间.

    当然,在多线程的纯循环有点难.

    测试

    class A {
        Object[64] x;
    }
    
    void main(string[] args) {
        import core.memory;
        import core.time;
        import std.stdio;
        A[] array;
        import std.conv;
    
        // 这是纯赋值循环
        MonoTime allocStart = MonoTime.currTime;
        GC.disable();
        array.length = to!int(args[1]);
        foreach(ref a; array)
            a = new A;
        GC.enable();
        writeln("Alloc time: ", MonoTime.currTime - allocStart);
    
        // 纯垃集收集,不释放.本质是测量扫描时间
        MonoTime start = MonoTime.currTime;
        GC.collect();
        writeln("收集时间: ", MonoTime.currTime - start);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    对比下来,可减少一半的时间.为什么?因为停止了不必要的扫描.GC实现不知道它没工作要做,但我们知道了,所以可选择性禁止.

    扫描 对比 未扫描的堆赋值

    接着,只关注收集时间.运行不同大小程序,此处只显示收集时间输出:

    $./测试330000
    8毫秒和623微秒
    $./测试340000
    11毫秒,570微秒和2纳秒
    $./测试350000
    14毫秒,361微秒和3纳秒
    $./测试360000
    17毫秒,368微秒和2纳秒
    $./测试370000
    20毫秒,222微秒和9毫秒
    $./测试380000
    23毫秒,42微秒和2纳秒
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    注意,随着扫描堆大小线性增长.这是性能最差的原因:如果在增长,每次扫描时间更长,从而二次性能.
    现在更改A为:

    class A {
        long[64] x;
    }
    
    • 1
    • 2
    • 3

    再次测试:

    $./测试310000
    369微秒
    $./测试320000
    336微秒和4纳秒
    $./测试350000
    673微秒和7纳秒
    $./测试3100000
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    1毫秒,383微秒和4纳秒
    仍有增长,数组更长,但是明显不那么明显了,因为不再需要扫描类自身,只需要扫描类数组.大大减小;还不到原示例的5%!

    经验教训:如果要减少GC暂停,则需要特别减少扫描堆大小,(还有栈大小,尽管栈大小一般没有大).
    可如下测试栈大小:

    A[64000] sarr;
    A[64000] sarr2;
    import std.conv;
    array.length = to!int(args[1]);
    foreach(ref a; array)
        a = new A;
    foreach(ref a; sarr)
        a = new A;
    foreach(ref a; sarr2)
        a = new A;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    即使数组长度为9,也只增加了大约1ms,大约是它的两倍;是保守地扫描栈.

    活动单个对象数也应有影响,扫描内存的原始量要大得多.用long[640]替换Object[64]来测试.

    $./测试3 1000000
    16毫秒,545微秒和5毫秒
    
    • 1
    • 2

    要扫描数组中的100万个类对象,每个对象约有2KB,因此占用了超过1GB的内存.这16ms在游戏中可能是一个完整的帧.每个对象都占用了大量内存,但是如果把它改回Object[640],你会得到超过一秒的暂停!

    为什么?因为它现在也要扫描A里面的内容,所以更多的工作.

    因此图像和视频并不重要.当然,带一百万个引用的数组会增加时间,但是资源自身大小并不重要.它们不包含指针,所以不会给GC增加大量工作.它知道不必扫描它,所以不会查看它们.
    现在看看:

    class A {
        //Object y;
        long[640] x;
    }
    
    • 1
    • 2
    • 3
    • 4

    为:

    $./测试31000000
    14毫秒,992微秒和2纳秒
    
    • 1
    • 2

    取消注释y对象:

    $./测试31000000
    1,608毫秒,980微秒和6毫秒
    
    • 1
    • 2

    哇,爆炸了.为什么?在GC赋值内存时,实现会在上设置一些标志.其中一个是包含是否有指向GC内存的不扫描(NO_SCAN)标志.新字节[](n)/new ubyte[](n),因为是字节无指针.原A类只包含typeinfo指针,它不是GC,所以编译器传递NO_SCAN.但是一旦添加了对象 a;A类型,则其中存在潜在垃集指针,因此NO_SCAN无效.

    由于这些标志,对整个内存块都适用,因此任意指针就是要扫描的意思.
    注意,不必在代码中使用NO_SCAN(除非直接调用GC.malloc),它是从你用分配的类型中自动计算出来的.
    注意:精确垃圾选项,也差不多!可用--DRT-gcopt开关测试:

    $ ./test3 --DRT-gcopt=gc:precise 1000000
    1,516毫秒,797微秒和4毫秒
    $ ./test3 --DRT-gcopt=gc:conservative 1000000
    1,600毫秒,766微秒和9毫秒
    
    • 1
    • 2
    • 3
    • 4

    为什么差别不大呢?精确扫描仍然是一种扫描.它知道必须扫描一部分对象,所以不能标记整块NO_SCAN.精确扫描只是改变了如何扫描,如果已完成扫描,则不会改变.精确扫描会在扫描过程中跳过部分内存块.但由于该访问模式并不快,因此增益很小.精确扫描可能比保守扫描慢.
    精确扫描更多的是消除错误指针,像指针的整数放一块,不能使扫描更快.

    结语

    目前,在D的gc中最大好处是,使大块分配完全没有指针,则完全不必扫描,从而节省大量时间.有时候只需从剩余对象中分开资源内存.
    小的好处是,知道GC不能收集太多数据时,禁止它,然后处理完该块后重新启用它.

    始终要平衡代码的复杂性性能要求.

  • 相关阅读:
    .skip() 和 .only() 的使用
    cmake链接ffmpeg静态库的方法,及报错答解
    【JavaEE初阶】多线程 _ 基础篇 _ 定时器(案例三)
    力扣第376题 摆动序列 c++ 贪心
    蓝桥杯c++A组第二个填空题
    《俞军产品方法论》笔记心得 01
    机器学习之归一化
    火车头采集器文章组合聚合
    霸占热搜!官方下场发放免单攻略,饿了么营销如何抓住“薅羊毛”心理?
    理财基金行情数据查询
  • 原文地址:https://blog.csdn.net/fqbqrr/article/details/127746997