为了测量这些Refs的性能,我将GHC生成的程序集转储到以下代码中:
import Data.IORef
main = do
r <- newIORef 18
v <- readIORef r
print v
我期望IORef被完全优化,只留下一个系统调用来写字符串"18"的标准输出。取而代之的是250条装配线。你知道有多少人会被处决吗?以下是我认为的程序的核心:
.globl Main.main1_info
Main.main1_info:
_c1Zi:
leaq -8(%rbp),%rax
cmpq %r15,%rax
jb _c1Zj
_c1Zk:
movq $block_c1Z9_info,-8(%rbp)
movl $Main.main2_closure+1,%ebx
addq $-8,%rbp
jmp stg_newMutVar#
_c1Zn:
movq $24,904(%r13)
jmp stg_gc_unpt_r1
.align 8
.long S1Zo_srt-(block_c1Z9_info)+0
.long 0
.quad 0
.quad 30064771104
block_c1Z9_info:
_c1Z9:
addq $24,%r12
cmpq 856(%r13),%r12
ja _c1Zn
_c1Zm:
movq 8(%rbx),%rax
movq $sat_s1Z2_info,-16(%r12)
movq %rax,(%r12)
movl $GHC.Types.True_closure+2,%edi
leaq -16(%r12),%rsi
movl $GHC.IO.Handle.FD.stdout_closure,%r14d
addq $8,%rbp
jmp GHC.IO.Handle.Text.hPutStr2_info
_c1Zj:
movl $Main.main1_closure,%ebx
jmp *-8(%r13)
我很关心这个jmp stg_newMutVar#
。它在组装中没有其他地方,所以也许GHC在稍后的链接阶段解决它。但它为什么会在这里,它有什么作用?
从几个链接开始:
-
MutVar
对象定义 -
newMutVar
的cmm
代码。 - 一个不全面但有用的GHC对象布局摘要。
cmm
和C
源代码不是特别可读,如果你还不熟悉宏和primops。不幸的是,我不知道有什么好方法来查看为cmm
primops生成的程序集,除非使用objdump或其他反汇编器查看可执行文件。
我仍然可以总结IORef
的运行时语义。
IORef
是GHC.Prim
对MutVar#
的包装。正如文档所说,MutVar#
就像一个单元素可变数组。它占用两个机器字,第一个是头,第二个是存储值(它是一个指向GHC对象的指针)。值MutVar#
本身就是指向这个双字对象的指针。
MutVar
-s不同于普通的不可变对象,最明显的是它参与了写屏障机制。GHC具有分代垃圾收集,因此在收集年轻代时,任何位于老一代中的MutVar
必须也是GC根,因为改变MutVar
可能会导致年轻对象变得可访问。因此,每当MutVar
从第0代(最年轻的)提升时,它就被添加到一个所谓的"可变列表"中,该列表包含对所有此类可变对象的引用。在旧代的GC期间重新构建可变列表。简而言之,老一代的MutVar
-s总是存在于可变列表中。
这是一种相当简单的处理可变变量的方法,如果我们在老一代中有大量的可变变量,小的垃圾收集会因为膨胀的可变列表而变慢,从而导致整个程序变慢。
由于可变变量在产品代码中并不重要,因此没有太多的需求或压力来优化RTS,以适应它们的大量使用。
如果你需要大量的可变变量,你应该使用单个可变的盒装数组,因为这只是对可变列表的单个引用,并且对于可能已经发生变化的元素的GC遍历具有基于位图的优化。
此外,正如您所看到的,newMutVar#
只是静态链接而不是内联的,尽管它是相当小的代码块。因此,它也没有被优化掉。这也是由于缺乏优化变异代码的努力和关注。相比之下,分配和复制已知大小的小原语数组目前是内联的,并且得到了极大的优化,因为Johan Tibell做了大量的工作来实现unordered-containers
库(为了使unordered-containers
更快)。