如果 Python 4.0 摆脱了 GIL…( 三 )


Biased Reference Counting
这其实是2018年ACM上的一篇论文 Biased Reference Counting 提出的一种全新 Reference Counting 理论:并发多个线程同时进行 Reference Counting 操作时 , 我们往往需要把每一次操作 Atomic 化 , 这样才能保证各个线程之间得到的 count 值保持一致;但我们忽略了一个因素 , 如果一个对象经常会被某一个线程操作 , 而被其他线程操作的频次很少 , 那我们是不是可以给这一个类似 "owner" 的线程一些特殊的优化 , 即便让其他的线程慢一点也影响不大?
而事实上 , 绝大多数对象都是面临这样一种情况 。所以 , 这里我们就 “Biased” 了 , 让 “owner” 线程的 Reference Counting 操作速度达到极致 , 而不用保证 Atomic  , 只需要让其他所有的线程 Atomic 即可(好吧 , 这里我也不是很懂 , 为什么 Non-Atomic 就一定比 Atomic 要快 , 但我知道为了做到 Atomic 显然要做某些牺牲 , 等有时间我再具体看看为啥 , 然后补充到这里) 。这一点非常关键 , 是整个 nogil 项目对性能贡献最大的一点 , 我画了个动画帮助理解:

如果 Python 4.0 摆脱了 GIL…

文章插图
 
Immortalization
上面的 Biased Reference Counting 好用的前提是“大多数变量只有一个线程会经常使用” , 但对于那些 0、1、True、False、 None之类的变量呢?这些变量可是几乎每一个线程都要频繁使用的 。为了提高这类变量的操作速度 , Sam 很巧妙地把这些变量 Immortalize(永久化)了 , 使得这类变量的引用不再需要做计数!我看到了这里 , 就有种强烈的“md我怎么没想到”的感觉 。
不过实现 Immortalization 也不是没有牺牲的:计数值的 LSB(最低有效位 , Least Significant Bit)不能再用了 , 因为 LSB 被用来代表这个变量是不是可以永久化掉了 。这里会结合下面的 Deferred Reference Counting 再多讨论一些 。
Deferred Reference Counting
继续揪着 Reference Counting 不放:那些既不能被永久化掉的同时又需要频繁使用的对象怎么办(怎么有点谐音梗...)?这个第二低有效位也被拿来征用了 , 被用来表示某个对象是否需要“Defer”它的引用计数 。这个“Defer”的意思我个人感觉有一点误导 , 因为它其实并非“延后” , 根本就是不再计数了 , 把所有释放相关的工作都交给 GC (Garbage Collector)了 , 毕竟很多引用的 top-level functions 或者 modules 本来就是只能被 GC 给释放掉 。
这里的具体实现我也不是很懂 , 但知道大概是因为局部变量一般是在内存的 Stack 上 , Deferred Reference Counting 是完全不用管 Stack 上的计数变化 , 但如果一个对象的引用是被放在 Heap 上的 , 这个时候计数其实是照常的 , 只不过不会因为 Heap 上的计数为 0 而直接释放掉它 , 毕竟这个时候有可能有 Stack 内存还在引用它 。
Immortalization 和 Deferred Reference Counting 加起来一下就用掉了两个最低位 , 也就是说以后每次调用 Py_INCREF 和 Py_DECREF , Reference Count每次变化就是 4 了 , 感觉怪怪的 。不过按 Sam 的原话 , 这里其实变化是 1 还是 4 并不重要 , 毕竟我们大部分情况下只关心这个计数是不是零就够了 。这么说 , 也确实有些道理 。
Mimalloc
Python 的内存分配器 PyMalloc 被换成 mimalloc 了 。看 mimalloc 文档看到第二段就感觉好厉害:
mimalloc is a drop-in replacement for malloc and can be used in other programs without code changes
这哪里是换掉 PyMalloc , 这原来是可以直接换掉 malloc 了 。。。
具体实现细节我就没有看了 , 因为我知道我肯定看不懂 。但是这里使用它的原因就很明显了:因为 PyMalloc 有 GIL 的保护 , 所以不需要也做不到 thread-safe , 而 mimalloc 可以让 Python 做到 thread-safe 同时性能大幅提升 。
Collection Read-only Access
写到这里 , 终于写到了码农们熟悉的 list 和 dict 对象了 。
当我们引用或一个 list 或 dict 对象 , 发生的过程大致可以简单地分成三个步骤:
 
  1. 加载这个对象的地址
  2. 修改对象的 Reference Count


    推荐阅读