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不能收集太多数据时,禁止它,然后处理完该块后重新启用它.
始终要平衡代码的复杂性与性能要求.
浙公网安备 33010602011771号