计算机底层的秘密读书笔记之二

发布时间 2023-06-17 14:59:40作者: 济南小老虎

计算机底层的秘密读书笔记之二


内存部分

内存部分之前自己看过很多资料了:
主要是虚拟内存以及TLB相关的内容

本书的角度与之前角度都不太一样,更加新颖一些. 
这次总结想倒着来写. 

1. SSD的带宽和延迟都比较好了, 但是是无法代替内存的
内存的寻址可以到字节,然后都是按照内存的位宽来获取内存中的值
然后逐步放到 Cache 里面中去. 
SSD的读写以block/Page为单元, 一次要读取4KB的内容. 
SSD而且还有一个写放大的问题. 并且寿命也非常有限,无法像内存一样使用. 
SSD因为没有机械组件, 所以他的延迟是比较低的. 但是就算是使用了NvmE也是10us级别. 
比内存的速度慢了至少两个数量级. 

2. 内存有很多经典问题
栈溢出, 内存泄漏, 内存溢出,错误指针, 除零, 其他类的安全问题. 
包括Intel的meltdown和幽灵等都是内存泄漏导致的问题. 
回到自己的工作中, 内存泄漏以及内存溢出是浪费大家时间最多的严重问题
会导致系统宕机. 
因为使用的是Java, java的虚拟机内存的使用其实是强烈依赖GC的. 
但是如果有一个死循环或者是大量的内存变量的话会超过内存xmx的上限, 就会导致问题. 

3. 内存分配与内存碎片
上一章总结时说到了进程是操作系统资源分配的最小单元.
其实进程的资源主要就是 CPU时间, 寄存器, 还有内存. 
时间的话就是time slice 一般是linux系统的HZ的倒数, 一般为20ms到800ms之间. 
寄存器的话 就是一些快速存取用的组件了, 一般的说法RISC比CISC的寄存器要多一些. 
内存的话就是一个比寄存器,缓存慢两个数量级, 但是比存储快六七个数量级的存储池
他的大小和性能基本上决定了大部分程序的上限. 
内存的使用主要就是 分配->使用->释放
分配和使用的性能其实很比较类似. 都是通过电路板进行获取坐标和状态的过程. 
但是释放如果释放的不好, 或者是出现太多的碎片会影响极大的影响性能. 
在解决Redis的问题时 一般都是人为如果内存随便过多, 会严重影响性能, 甚至会导致卡顿. 

4. 虚拟内存以及栈区和堆区内存的管理
虚拟内存是操作系统管理内存的核心. 
现代操作系统一般采用段页式内存管理
段式管理主要是区分 内存的使用类型和权限. 
比如文本代码区,数据区,堆区,栈区
数据区的命令无法执行, 代码区的代码是可以执行的. 堆区是进程自己申请和释放进行管理
栈区是先进后出顺序的存储机制. 
每个区域的位置都是有严格的控制,超过了都会报错,或者是抛出异常. 

页管理主要是在于寻址, 现在硬件TLB都是快速页表结构, 根据程序的空间局限性, 可以快速的查询和使用最近经常使用的内存地址. 
随着内存的越来越大,除了之前基本的4KB的与外面,还有更大的2MB和1GB的内存页面大小. 
单个页内存越大, 对应的查询页表就越小, index的效果越好. 
现在硬件上其实有多级页表. 32为一般是两级页表结构: 10-10-12的结构. 
64位的结构比较复杂, Linux下一般四级页表结构 9-9-9-9-12 其中页面都是 12位长度,也就是 4KB的大小.
其实根据这个 四级页表的特性也可以看出来为啥 HugePage都是 512倍的倍数进行递增了. 

CPU部分

内存固然重要.但是就像是人得躯干一样, 真正决定一个服务器性能的还是CPU为主. 
这周6.16 周五时测试验证申威3231的性能时是一样的.
服务器使用的是DDR4-3200的镁光内存,但是他比相同甚至更低内存的Golden6150测试结果要差很多.

所以一个服务器的性能其实是短板原理. 最短的一个板来决定整个机器的性能. 

因为低碳要求, 很多服务器其实都有节能模式, 节能模式会让机器的巅峰性能下降. 
以为晶体管的体质不同, 不同的CPU型号下不同的基础频率和睿频也不尽相同. 
Intel的睿频能够从3.5到5.5进行浮动. 全核心睿频和单核心的睿频数值一般不一样. 

前期研究过虚拟化的乳品, 发现在ESXi上面可以看到睿频的信息,但是在虚拟机层次是没有的. 
所以物理机器的极限性能与虚拟机的极限新能是不相同的. 

其实查看CPU的性能还有一点是查看机器的功率. 如果功率不达标,风扇转速不高,机器一般不是在最高负载的情况. 
读书笔记的第一部分里面其实说到了CPU的使用与中断, 其实物理中断和逻辑中断, 对应时间片耗尽的中断其实不尽相同.
对机器的影响也不一样. 
所以中断绑定和CPU的核心,基于node节点的绑定都是不一样的. 

书中也讲了原码,反码,以及补码. 其实位运算是很多算法里面的一种核心处理逻辑. 
CPU性能的提升其实除了主频之外 更重要是架构. 流水线的加入也是一种很大的创新. 
当CPU遇到一个判断语句时会自主的进行一下预判 predict 如果预测成功,那么不需要再进行IF,ID memory等操作性能会有很大提升.
如果很多都是无序的会导致流水线hang住 冒泡,性能就会下降. 
其实架构里面还有多发射, 与乱序执行还有乱序的buffer. CPU性能的提升一方面看主频一方面看IPC (instruction per cycle)
IPC还有一种说法是CPI (cycle per instruction) IPC值越高 一般CPI的值越低, 基本上是倒数的关系. 
最早的RISC没有流水线, 每个周期肯定只能执行一个命令或者是不到(等待缓存/内存/译码/解码等)
随着流水线多发射的加入. 理论上一个指令周期内最多可以用有超标量流水线的 pipeline 宽度大小.
因为还有一些类似于乱序,寄存器重命名等手段. CPU的性能也是越来越好.  
其实CPU也就是AMD比较弱这几年因为intel受到制程工艺的影响举足不前, 其他时间每年的提升都不低于10% 
最近因为AIGC的促进和AMD的Chiplet的发展. CPU的多核心性能有了非常非常大得的进步,导致整个Intel都渐渐失去了领先地位. 

感觉 RISC和CISC的关系值得很好的复习一下:
其实现在虽然x86号称复杂指令集,但是实际上仅是他的前端是复杂指令集. 但是在CPU的后端部分, 将macro ops 转换成 uops 时
已经完全是RISC的玩法了. 

但是因为他有很多不定长的复杂指令, x86编译的二进制一般比ARM等RISC的二进制要小很大的空间.
也就导致他的code cache占用空间也会小很多. 
看鲲鹏社区时就有过讲解. x86上面够用的codecache 到了ARM上面可能不够用. 
对象以及基础对象的内存占用. x86和arm几乎没有区别, 但是text代码区的大小其实是有很大区别的. 

uops 的指令执行效率, 其实就决定了机器的性能好坏. 
理论上越新的架构往往会使用更新的制程工艺. 他的性能和能好都是螺旋上升的. 

CISC和RISC 谁胜谁败其实不仅仅是技术, 或者说技术永远都不是决定性因素
CISC对编译器要求不高, 但是对硬件设计要求很高. RISC对编译器要求非常高,但是硬件设计相对来说就比较简单. 
CISC因为一直以为的复杂特性,导致减淡命令执行时总是有很多晶体管空闲, 所以为了提升性能. Intel提供了HT超线程技术.
AMD对应的是SMT simultaneous Multi Threads. 现在大部分ARM机器没有超线程.
但是IBM的POWER8开启 其实是支持SMT8的就是一个核心支持8个线程. 对应的OpenPower支持SMT4
所以超线程跟指令集没关系,跟硬件设计和实现以及取舍有关系.
超线程并不一定能够提高性能. 他的比较科学的说法是通过增加5%的晶体管, 提高最多15%的整体吞吐量. 
所以虽然是两个线程, 但是实际上的性能不会超过1.15核心, 并且在数据库这种并非科学运算 Predict miss比较高的场景下
超线程可能会有负面的作用. 

Cache

公司内的前期很多优化其实就只有两个:
增加索引, 增加堆区
现在想起来其实挺low的. 这些其实不是调优, 这些其实是bug. 不管是多表关联,索引失效还是索引的设计不合理
或者是将过多的内容添加进内存, 其实这些都是Bug 而非需要性能调优的点. 

真正的性能调优应该是将硬件压榨到极致. 而不是通过垃圾代码将机器爆掉100%的CPU. 
内存和CPU在提高整体性能和吞吐量方向功勋卓著, 但是他俩的发展却一直存在较大的鸿沟. 
内存因为是比较单纯的矩阵样式, 并且需要不停的充电来保证内存数据不丢失. 他的速度比CPU一般会有两个数量级的差异. 
而且内存的带宽, 就算是8通道DDR3200. 也不会超过30GB的总带宽. 
CPU 如果使用512字节的 AVX512指令集, 如果是 32核心.2GHZ的机器他的带宽里路上可以达到 32*2*512/8 2TB的总带宽. 
内存不仅速度慢, 也无法喂饱CPU
所以这个时候就需要在CPU和内存之间提高 cache来提高性能. 

Cache一般也分级, 一般是L1 L2 L3 三级缓存结构, 从2x 8x 到 1nx  的性能劣化. 内存比CPU一般会慢100倍左右. 
缓存为了提高利用率一般会使用组相连的技术进行与内存关联. 
因为L1和L2一般是每个核心独占的. L3一般是socket共享(AMD是每个CCD共享L3,一个socket内部可能有8个互相独立的L3)
最开会时因为Intel都是ring环形结构.所有的核心共享L3, AMD是chiplet结构. 三级缓存需要有跟复杂缓存一致性算法.
所以ZEN1,ZEN+,ZEN2的时代时AMD的多线程性能不如Intel优秀,但是随着AMD的逐渐优化. numa节点之间的延迟大大缩减.已经比intel性能要好了. 

刚才提到CPU内的高速缓存是使用组相连的技术, 他的最小单元其实是 cache line. 一般x86的cache的 cache line 大小是64bytes
所以如果变量能够在64字节内放满.他的性能是非常好的. 如果要跨越cache line 性能会极具衰退. 

本章中对自己帮助最大的一部分就是 多线程性能杀手的部分
cache 乒乓问题 以及 cache 伪共享问题. 
多核心下一般需要使用硬件的MESI等算法来保证不同CPU缓存内的变量值统一 
避免一个核心修改了一个变量, 其他核心没有收到通知,导致数据丢失更新后者是异常错误等. 
因为这套逻辑的存在, 其实严重影响了性能. 这也是为上堆上面变量的设置比栈上面的变量设置速度慢的根本原因
栈上面的变量都是独占的. 不需要考虑一致性的问题. 堆上的变量必须考虑一致性,避免出现问题. 

内存屏障
之前一直对内存凭证没啥了解.
只知道x86是强内存序. arm是弱内存序.
经过本书, 我对内存序有了更大一点的了解. 
其实因为前面提高的流水线. 所有的指令集都把访存操作改成了 load和store两步.
因为这两步的速度最慢, 其他步骤基本上都是可以一个周期内完成. 
说道内存序. Intel只有一个store load 可能需要使用专门的语义进行避免.
其他的都可以自主进行实现避免 比如 load load 和 load store 以及 store store的问题. 

其实说到这一块跟jvm里面的volatile 修饰符也有一定的关系. 
这个参数其实就是类似添加了一个latch 轻量级的闩锁. 避免并发时出现ABA以及其他问题. 

其实科学计算和AI这一块更多的讲究的是趋势而非100%的严格正确. 
对AI对大量计算趋势运算的调优和 java这种业务系统的调优完全是两个极端.
AI可以用FP8 INT8这样的半精度四分之一精度进行运算, 主要是取趋势
但是JAVA的应用需要使用 Decimal 这种精确的金额类型进行运算. 保证数据不丢失.