perfbook notes: Hardware

Perfbook 全称是 Is Parallel Programming Hard, And, If So, What Can You Do About It? 作者是 Paul E. McKenney,也是 RCU 的发明者。这本书相对来说语法有点点难,我个人看的有那么点小吃力,但是作者能够非常形象的描述并发数据结构是怎么设计的,并且从 scalable-counter、partition、lock、data-ownership 介绍到 RCU。书具体内容很好,也值得我做一些笔记以便未来回顾。

书的编写会包括一些 Quiz,有的还是比较有难度的,帮助自己理解问题。我的笔记只供自己做一个回顾和一些工业界实现的参考,所以不会完全参考原书的布局。

Chap2-4 描述了一些 smp 的原语和性能

并行编程的需求、基本性能与原语

并行编程在上世纪是个非常小众的领域,只有专业人士会写。但是随着 CPU 撞上功耗墙,这一小众领域现在已经成了大众卷 b 之地。现在有 tbbfolly 等库,也有 thread atomic 等编程工具/原语。此外,还有一些用户态线程、协程也在近年重新受到关注,它们可以减少线程调度和切栈或者申请栈的开销、和 async 等语法结合,用于优化代码和改善性能。

7BF7E021-3D1F-4F27-9BAB-EC772C386339

并行化的优势在于:

  1. Performance: 包括 scalability( performance per CPU) 和 efficiency (performance per watt), 这两个东西都重要,一起才能拼出个不错的最终结果。同时如果你的程序够快、没有性能问题,那串行就串行吧。

  2. Productivity: 通过你的东西,实现内容的生产力

  3. Generality: 方法的通用性。事实上我们之前讨论的 RCU 什么的 api 相对来说性能好,但是通用性确实差不少。

这里考察了 C++ POSIX Thread API(Performance、Generality),Java (相较 C++ 牺牲了一点性能)、SQL (不太泛用、领域专一,但有着好的性能和 Productivity)。

然后作者整了张图,再讲了下 OS/硬件 层面是如何对上述内容取舍和抽象的。因为 OS 本身是一个很「通用」的东西,所以它的 Performance/Generality 的要求是很高的,而越上层的应用可能对 Productivity 要求越高。

77B09222-65ED-4732-B215-09355B46E7A6

Alternatives to Parallel Programming

这一节的内容非常的工程师智慧,介绍了如何「不编写一个并行的系统,又用上硬件这些功能」:

  1. 运行多个串行系统的 instance,并能够对 workload partition
  2. 用已有的并行系统
  3. 串行上进行优化,优化到不用并行也运行的挺好

What Makes Parallel Programming Hard?

CE768DAC-77B4-4D12-8878-B0BF058C4302

因为这个地方要成功的切分任务,然后既然有切分,那运行之类的还有通信的事情要考虑,同时还要考虑资源管理、回收 etc… 本身这些任务都有相对的复杂性。

这一节相当于介绍了「并发编程为什么这么难」。其实可以看到,跟我个人的想象不同,这个领域是

Hardware and its Habits

一个现代的 CPU 大概在执行的时候会有:

  1. pipelines
  2. Superscalar
  3. Speculative execution

具体我感觉 15-418 给的材料比较好。

4C310521-6C51-4748-8D82-672D94AC3D52

关于现代 CPU,本身性能下降的原因,perfbook 列了:

  1. Pipeline 相关,比如 branch mispredict/structural hazard: https://blog.mwish.me/2020/10/21/SDS-Intro-RISC-V-Datapath-4-Pipeline/
  2. Memory Reference: 这里不光是访问内存的开销,更多的是,给出一个地址,然后访问其内存的开销。
  3. Atomic Operations: 这个地方不涉及 memory barrier, 应该指的就是对某段内存的 atomic 操作。原子操作有的地方是违背 CPU 假设的。atomic op 的操作经常要:
    1. 保证数据是这个 cacheline 所有的
    2. 操作完后数据还归属于这个 cacheline
    3. pipeline must be delayed or even flushed in order to perform the setup operations that permit a given atomic operation to complete correctly.
  4. Memory Barriers: memory barriers 在语言层面上经常和 atomic op 放在一起讨论,但是这个可能需要对 Writer Buffer 之类的东西做特殊处理,可能会影响应能
  5. Cache Miss: 懂得都懂。
  6. I/O Operations: 懂得都懂。可惜我不懂 io_uring,令人唏嘘。

按照我偷来的图:

F373D15D-0CE9-4427-80BE-B4545FAA35BE

硬件及其假设

734E9D9B-1488-432F-B534-2817C6FD327A

上面是 perfbook 的图,我从 15-418 偷了几张图:

3DB734F8-B806-4A45-91AA-97DBA2E3B272

0D78679C-E152-4396-AE71-8A643D8F7770

这几张图应该概述了 NUMA、SIMD 之类的。

perfbook 举了个例子,在某个架构中,如果 CPU (CPU0) 想要写一个跨 Socket 的 mem 地址,可能会:

  1. check local cache(L1, L2)
  2. 走目录协议,给 CPU0 或者别的 CPU 发送指令,然后让这个 Cacheline 搞到自己上头
  3. cacheline 到自己上,真正可写
  4. 具体写

下面一张图介绍了各种操作的开销:

FG9cf5AaIAAxxUI

FGkZD2DVgAAW6Ij

  1. 同一个 CPU 的 CAS 可以几 ns 内完成,流程基本上相当于在 cacheline 做标记。这里 cas 是 x86 的 lock;cmpxchg
  2. In-core 表示在同一个核心上,共享同一个 cache hierarchy;blind cas 表示「不读旧值,直接 CAS」,而 CAS 是「读旧值 + CAS」。后者可能访存两次

2D9BD427-C39E-43B2-AA69-C37BF71341B6

这里还有些 dark side 优化:

  1. large cacheline
  2. cache prefetching
  3. store buffer
  4. speculative execution
  5. large caches
  6. read-mostly replication(想下 MESI)

上面这些策略其实软件也是很常用的。

Tools

本节内容比较难,但我不太打算写。这里用的工具不太是 user-level 的 C++11 atomic 之类的,本节介绍了 Linux 常用的 WRITE_ONCE READ_ONCE,和 volatile 的语义。然后介绍了一下各种并发。

在这之前,chap4 介绍了一些不用上面方式的工具,比如 bash 脚本这一著名、经典的并发程序。详细内容我觉得可以参考 6.null。

POSIX 提供了 fork wait 等进程接口,但这些接口的使用通常和 shm、signal 结合。在 nptl 没实现的时候,PostgreSQL 这些只能依赖这套东西做。

下面介绍了一下 POSIX mutex、GNU 的 atomic 和__sync_。然后着重介绍了 volatile 的语义。我觉得别的地方介绍都没这里好。但是我感觉这段不如直接 atomic,所以感兴趣的自己回头看吧。

这里基本还可以参考一些 futex are trickey 什么的,看看对应接口的实现。webkit 实现了一套并发库:https://webkit.org/blog/6161/locking-in-webkit/ ,可以作为参考(不完全是 futex,相当于在用户态包装了一层 futex)

或者也可以看我 rsy 爹的 blog: https://github.com/rsy56640/triviality/tree/master/content/%E8%B0%88%E8%B0%88%E5%B9%B6%E5%8F%91

References

  1. https://www.quora.com/What-is-robust-mutex-in-Linux-What-are-some-use-cases-of-robust-mutex
  2. CMU 15-418