
几乎所有不平凡的程序都会分配和使用动态内存。 随着程序变得越来越复杂,错误变得更加昂贵,正确地执行此操作变得越来越重要。
典型的问题是:
- 内存泄漏(不释放已用完的内存)
- 双重释放(内存释放不止一次)
- 释放后使用(使用指向先前释放的内存的指针)
挑战在于跟踪负责释放内存的指针(即拥有内存的指针),并区分仅指向内存的指针,控制它们的位置以及其中哪些处于活动状态(范围内)。
典型的解决方案如下:
- 垃圾回收(GC)-GC拥有内存块,并定期扫描它们以寻找指向这些块的指针。 如果找不到指针,则释放内存。 该方案是可靠的,并用于Go和Java之类的语言中。 但是GC倾向于使用比必要更多的内存,由于重新打包(原始插入的写入门)而使代码暂停并减慢了代码的速度。
- 引用计数(RC)-RC对象拥有内存并存储指向其自身的指针的计数器。 当此计数器减小到零时,将释放内存。 这也是一种可靠的机制,并且已被C ++和ObjectiveC等语言接受。 RC的存储效率很高,另外仅需要计数器下方的空间。 RC的不利方面是维护计数器,嵌入异常处理程序以确保减少计数以及阻塞程序流之间共享的对象所必需的开销。 为了提高性能,程序员有时会通过绕过计数器临时引用RC对象而被欺骗,从而有可能错误地执行该操作。
- 手动控制-手动内存管理是Sysalny malloc和免费的。 就内存使用而言,它是快速而有效的,但是这种语言并不能完全正确地依靠程序员的经验和热情来正确地完成所有事情。 我使用malloc和free已有35年了,在无穷无尽的经验的帮助下,我很少犯错误。 但这不是编程技术可以依靠的方式,请注意,我说的是“很少”,而不是“从不”。
解决方案2和3在某种程度上取决于对程序员的信任,以正确地完成所有操作。 基于信念的系统无法很好地扩展,事实证明内存管理错误很难重新检查(非常糟糕,以至于某些编码标准禁止使用动态内存)。
但是,还有第四种方式-所有权和借款,OB。 它的存储效率很高,与手动操作一样快,并且需要自动重新检查。 最近,该方法已被Rust编程语言所普及。 它还有其缺点,特别是需要重新考虑算法和数据结构的规划。
您可以处理负面问题,本文的其余部分是OB系统如何工作以及我们如何建议将其编写为D语言的示意图,我最初认为这是不可能的,但是花了一些时间思考之后,我找到了解决方法。 它与我们使用函数式编程所做的相似-具有传递不变性和“纯”函数。
拥有
谁拥有内存中的对象的决定非常简单-只有一个指向该对象的指针,它就是所有者。 他还负责释放内存,之后内存变为无效。 由于指向存储器中对象的指针是所有者,因此在此数据结构内没有其他指针,因此该数据结构形成一棵树。
第二个结果是指针使用移动而不是复制的语义:
T* f(); void g(T*); T* p = f(); T* q = p;
禁止从数据结构内部删除指针:
struct S { T* p; } S* f(); S* s = f(); T* q = sp;
为什么不仅仅将sp标记为无效? 问题在于,这将需要在运行时设置标签,但应在编译阶段解决,因为它仅被视为编译错误。
自身指针超出范围的退出也是一个错误:
void h() { T* p = f(); }
您必须以不同的方式移动指针值:
void g(T*); void h() { T* p = f(); g(p);
这解决了释放后的内存泄漏问题和用法(提示:为清楚起见,将f()替换为malloc(),将g()替换为free()。)
所有这些都可以在编译阶段使用
数据流分析(DFA)技术进行验证,就像它用于
删除常见的子表达式一样 ,DFA可以
消除可能出现的程序转换中的任何棘手问题。
借用
上述任期制是可靠的,但过于严格。
考虑:
struct S { void car(); void bar(); } struct S* f(); S* s = f(); s.car();
为此,s.car()必须有一种方法可以使指针在退出时返回。
这就是借贷的工作方式。 s.car()在s.car()的持续时间内获取s的副本。 s在运行时无效,并且在s.car()退出时再次变为有效。
在D中,
结构成员函数通过引用获取
this指针,因此我们可以通过一个小的扩展来适应借用:通过引用获取参数即可。
D还支持指针作用域,因此借用是自然的:
void g(scope T*); T* f(); T* p = f(); g(p);
(当函数通过引用接收参数或使用具有范围的指针时,禁止将它们扩展到函数或范围的边界之外。这与借用的语义相对应。)
以这种方式借用保证了在任何给定时刻指向内存中对象的指针的唯一性。
即使所有权对象还由多个常量指针(但只有一个可变的指针)指示,也可以通过所有权系统也可靠的理解来进一步扩展借用。 常量指针无法更改或释放内存。 这意味着可以从可变所有者那里借用几个常量指针,但是在这些常量指针存在时,他无权使用。
例如:
T* f(); void g(T*); T* p = f();
原则
前面的内容可以简化为以下理解,即内存中的对象的行为就像是处于以下两种状态之一:
- 恰好有一个指向它的可变指针
- 一个或多个其他常量指针
细心的读者会发现我写的东西有些奇怪:“好像”。 我想暗示什么? 这是怎么回事? 是的,有一个。 计算机编程语言充满了“好像”在幕后的样子,就像实际上您的银行帐户中的钱不存在一样(我很抱歉,如果这对某人造成了严重震撼),这没什么不同。 继续阅读!
但首先,对该主题进行更深入的探讨。
在D中整合所有权/借用技术
这些技术是否与人们通常用D编写的方式不兼容,并且几乎所有现有的D程序都不会中断吗? 它不是那么容易修复,而是那么多,以至于您必须从头开始重新设计所有算法?
的确是。 除非D有(几乎)秘密武器:功能属性。 事实证明,可以在定期进行语义分析之后针对每个功能分别实现所有权/借用(OB)的语义。 细心的读者会注意到,没有添加任何新语法,仅对现有代码施加了限制。 D已经有使用功能属性更改其语义的历史,例如,使用
纯属性创建“纯”功能。 为了启用OB语义,添加了@
live属性。
这意味着可以根据需要将OB逐渐添加到D上的代码中,并释放资源。 这样就可以添加OB,这一点很关键,可以在完全正常运行,经过测试且可以立即发布的状态下不断支持该项目。 它还使您可以自动化监视已将多少百分比的项目转移到OB的过程。 这项技术已添加到其他D语言保证的列表中,这些保证涉及使用内存的可靠性(例如,控制对堆栈上临时变量的指针的非分配)。
好像
严格遵守OB不能实现某些必要的事情,例如引用计数对象。 毕竟,RC对象被设计为具有许多指向它们的指针。 由于RC对象在使用内存时是安全的(如果正确实现),因此可以将它们与OB一起使用,而不会对可靠性产生负面影响。 它们只是无法使用OB技术创建。 解决方案是D中还有其他函数属性,例如@
system 。 @
系统具有禁用许多可靠性检查的功能。 自然,OB也将在@system的代码中被禁用。 这就是RC技术的实现对OB控制隐藏的地方。
但是在带有OB,RC的代码中,对象看起来好像遵循所有规则,所以没有问题!
要成功使用OB,将需要许多类似的库类型。
结论
本文是OB技术的基本概述。 我正在研究更详细的规范。 我可能错过了一些东西,在水线以下的某个地方,但是到目前为止,一切看起来都很不错。 对于D来说,这是一个非常令人兴奋的发展,我期待实现它。
有关Walter的进一步讨论和评论,请参阅
/ r / programming subreddit和
Hacker News上的主题。