d不扫描来缩短垃集

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

当前实现中的GC优化技巧

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

GC.disable策略

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

while(true) array ~= stuff;

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);
}

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

扫描 对比 未扫描的堆赋值

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

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

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

class A {
    long[64] x;
}

再次测试:

$./测试310000
369微秒
$./测试320000
336微秒和4纳秒
$./测试350000
673微秒和7纳秒
$./测试3100000

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;

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

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

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

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

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

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

class A {
    //Object y;
    long[640] x;
}

为:

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

取消注释y对象:

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

哇,爆炸了.为什么?在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毫秒

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

结语

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

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

posted @   zjh6  阅读(83)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示