一些经典的系统idea

类似于 CS61C 提出的体系结构经典 idea我也想在这里总结一下我在学习/读论文过程中看到的可复用的 idea很多文章就是对这些 idea 进行排列组合用于实现 tradeoff 的一部分实现整体的性能目标

分层以利用局部性

数据的访问存在局部性指令的局部性体现在循环和经常调用的函数上数据的局部性体现在数组上存在局部性的时候就可以用少量快速昂贵的存储上层和大量慢速廉价的存储下层进行组合实现既快速又大量的存储这里纯粹是讲利用局部性达到的提升假如上层($t_1$)比下层($t_2$)快20倍但是直接访问上层内容命中率为95%那么访问数据期望的时间是$0.95t_1+0.05t_2=1.95t_1$命中率更高时效果更好

内存层级系统是最经典的例子寄存器L1i/L1d Cache, L2 Cache, L3 Cache, 内存虽然机制有所不同但是可以加上硬盘一层一层叠起来实现接近寄存器读写速度和硬盘容量的存储这中间还可以再添加层级例如固态硬盘的控制芯片一般会自带一些缓存块和一个缓存系统类似的例子还有用于地址翻译的 TLB 会带有页表的 CacheDPDK 的内存池会在每个核设置 Cache操作系统会缓存文件系统的元信息/文件内容存储敏感的应用程序如数据库会自行建立对硬盘数据的缓存等等无论在硬件还是软件中Cache 的思想都是十分常用的

Fast Path

这是一种优化复杂逻辑的方法软件中经常出现这种情况某部分的逻辑很复杂但是如果加以一定的假设/理想条件逻辑就能得到大幅的简化如果这种假设经常能够被满足那么就可以为满足这种假设的情况单独编写一个简单的分支这就是 Fast Path而只在假设不满足的时候进入复杂分支

存在这种优化的信号通常是为了解决某个不经常出现的问题引入了某种新的数据结构/逻辑而新逻辑的 overhead 非常高

例如TCP 为了解决不经常出现的乱序包问题而引入了重排 buffer 这种数据结构于是每收到一个包都需要在重排 buffer 中寻找它的位置就多了一份访问链表/红黑树的 overhead如果不认真加以解决这份开销会在每次收到数据包时发生影响严重解决的方案就是对经常出现的情况数据包顺序到来进行特殊的检查这里就是存储一个待接收的序列号发现序列号等于待接收的号时就直接放在重排buffer的开头另一个例子来自 SRNIC(NSDI 23)RDMA 为了解决不经常出现的乱序问题而在硬件中引入了 bitmap 结构重传队列结构和复杂的重传逻辑带来较高内存开销解决方案就是只在硬件实现顺序到来的部分逻辑监测到乱序时就通知软件进行处理以节约硬件资源由于数据中心网络中乱序很少发生对整体性能的影响很小

批处理

批处理通常用于将一些较高的重复的 overhead 均摊到多次操作中例如 mTCP 与 I/O 库的交互就是批处理式的网卡一次传递给 mTCP 多个数据包mTCP 再生成一批事件传递给应用均摊了 mTCP 与 I/O 库交互的上下文切换等开销I/O 库与网卡的交互也是批处理式的均摊了一些昂贵的 PCIe 操作开销再例如 TCP delayed ACK 是希望 TCP 实现不要收到每个数据包都发送 ACK平均每 2 个包发送一个 ACK 是推荐的量这个量的选取还考虑到了 ACK 承载的拥塞控制功能如果没有这方面考虑可以将发送频率设置得更低TCP Nagle 是希望 TCP 不要发送过多的小数据包而是攒成更大的数据包进行发送显存对不同区域内容的写入顺序要求并不严格因此可以将需要写入显存的数据暂存起来再一起成批写入显存的硬件以均摊与显存交互的开销这些都可以看做批处理思想的例子

批处理涉及到平均处理时间和响应时间的 tradeoff批处理时平均处理时间比较短但是最早到来的请求需要等待其他请求的到来才能被写入所以响应时间会变慢如果能取得平衡就能得到较好的效果

抽象生成与中间层

编译器的诞生可以说是计算机发展史上的一次工业革命它将人们从手写汇编的繁重劳动中解放出来开始用高级语言编程这里要说的概念都是从编译器中得到的启示

抽象

编译器的工作就是根据抽象而人类易懂的高级语言生成功能简单易于执行的低级语言抽象从来没有增加程序员能做的事情它只是对程序员进行了更多的限制但这种限制降低了程序员的心智模型一旦理解高级语言后高级语言编写的程序就比低级语言更加易懂高级语言可以针对其要表达的内容简化语法从编译器和程序员共同认同的假定中推断信息编程语言的例子不必再举举一些其他抽象的例子

  1. 操作系统将对硬件的操作抽象为系统调用这里的低级语言相当于操作硬件的寄存器高级语言就是系统调用例如将对硬盘/网络的访问抽象为文件只允许程序员把它当成流来读取/写入从编译器和程序员共同认同的假定中推断出按流操作的能力已经足够简化了程序员对硬盘/网卡的操作十分易懂同时还实现了对硬件统一高效安全的管理
  2. Rubik 是一个用于生成网络中间件的领域特定语言将连接抽象为 id, sequence, 状态机和数据的集合只允许程序员操作这些数据结构降低了心智开销这样的数据结构设计以及优化过程中用到的假设都从编译器和程序员共同认同的假定中推断出非常多的信息
  3. 极端一点的例子导航系统的起点终点语言就是一种抽象语言补充语法有选择交通工具红灯少/价格便宜/时间短的偏好选择等

生成

从高级语言生成低级语言的过程是自动化的特别地对代码的优化过程也可以自动化进行这说明编写低级语言的过程中隐含了许多重复性的劳动它们被自动化后减少了程序员的心智负担而且机器通常做得比程序员还要好

中间层

在源语言和目标语言中间可以插入中间语言将源语言先编译成中间语言再编译成目标语言通常这里的中间语言是对各种目标语言共性的提炼因此语言前端只需编译到同一种中间语言中间语言再编译成各种目标语言前端的工作便无需针对多种目标语言重复进行减少了重复的工作量同时中间语言比源语言更加具体更加接近机器执行的语义因此便于进行优化这些几乎是照着 LLVM 说的此外Rubik 也用到了中间语言进行优化目标语言是 C 语言目前看来没有多目标语言的必要DPDK 的 EALSQL Server 的 SQLPAL 用中间层的方式实现了跨操作系统Nginx 自己实现的事件处理层实现了多种系统调用epoll/kqueue/IOCP之上的事件处理