D如何支持写障碍
原文
我开始思考编译器中的指针障碍.不把它们看作新类型,而是当作类似切片中的增加了安全性和功能的检查区间,但在特例下,如果必要,可在本地和全局绕过它们.
你仍然拥有今天D所拥有的指针.唯一区别是添加了一种绕过GC写障碍的方法,可能类似:
core.memory.__raw(T)
//
struct __raw(T) {
T ptr;
alias ptr this;
auto opAssign(T rhs) { /*绕过边界检查*/ }
}
注意别名 本.如果真有这样隐式转换回普通指针的指针.
另一方面,构造时,总是显式绕过检查.唯一工作是表明你想绕过正常检查,甚至可类似
slice.ptr[0]
在本地绕过边界检查.你不必真正用它;严格说是一种可选的性能增强,用于就像可绕过边界检查一样的特例.
编译器中,同样类似边界检查,可添加:
-barrier=[none|writes]
命令行开关来全局控制它.如果没有编译屏障,不能用一些GC实现.
屏障自身被转发给druntime函数或llvm内部函数,或适合编译器和垃集的函数(这会使运行时交换出收集器成为额外性能损失,因为屏障也是函数指针,但这是合理决定).
函数像:
core.memory.__ptr_write(void** where, void* what)
因此,如果实现有最大灵活性.用内部函数和内联,当关闭屏障时,会化简为:
mov [where], what;
注意:
1,因为从未写入常指针,它不需要写屏障.
const T* a; a = something;
在前端不需要特别注意,因为编译不过.
2,自赋值指针,
int* a; a++;
int[] a;
a = a[1 .. $];
也不需要写屏障.因为目的是保护改变GC行为的竞争条件,而GC只关心它指向的块,这些操作本身禁止改变它指向的块.如果切片越界,则通不过检查,甚至根据C规则,原始指针超出原始赋值块也是未定义行为.
只需确保是原子的替换实际指针;确保实际生成inc ptr,而不是读-改-写.我肯定编译器已这样做了,但必须指定来确保.
编译器前端可闲着,因为后端可检测到这些自修改并消除检查.
我想,即即使启用它,很多D函数也根本不会生成障碍除非遗漏了些重点.你重新赋值指针到另一块时,就会生成障碍.
3,也可消除借用引用的障碍,因为你知道GC会看到它在其他地方的规范引用.关键是:写操作会影响GC在运行过程中的行为吗?如果是,则需要保护它.如果不是,可跳过它.安全最好;不需要的额外障碍对性能会造成很小影响,而缺少需要的障碍则是严重运行时错误.
4,省略了保守的栈扫描,和复制/移动的GC实现.但对世代GC和大多数增量和并发GC的实现足够了.当然,需要命令行开关来生成更多代码,来支持其他方案,但是我勾画出可能工作的最简单的.
5,上面都仅适用直接写至指针,而不是通过指针写.
char* a;
a = x; // 有写障
*a = x; // 没有.
指针值变化才是重要的,GC不关心随机字符值,只关心引用和未引用哪些内存块.
不同GC实现的好处:
当前,GC标记阶段停止所有线程,这样就不会竞争,当它标记时,如果GC在其他地方工作时,它会变化.有了写障,除非实际写指针,可避免停止线程.
假设音频线程正在消耗GC资源,但只写入到现有的ushort[]缓冲区,它的活动永远不会使GC工作进程失效,所以可继续运行音频线程.这不需要显式用户工作,也不需要注销它.
另一个工作线程也许正在处理一些数据,然后发送缓冲到一个函数.根据借用指针是否管用,它可能一直运行,但是更简单的,更少漏洞的实现会让它完成处理数据,然后只在复制缓冲指针到函数时才因为触发写屏障而真正停止.到那时,GC可能几乎完成了,即实际停止时间比当前实现要小得多.
写障碍是完全的净收入.
浙公网安备 33010602011771号