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
不能收集
太多数据时,禁止
它,然后处理
完该块后重新启用
它.
始终要平衡
代码的复杂性
与性能要求
.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现