有人在Twitter上谈到了自己对CPU的认识:
我记忆中的CPU模型还停留在上世纪80年代:一个能做算术、逻辑、移位和位操作,可以加载,并把信息存储在记忆体中的盒子。我隐约意识到了各种新发展,例如矢量指令(SIMD),新CPU还拥有了虚拟化支持(虽然不知道这在实际使用中意味着什么)。
我错过了哪些很酷的发展呢?有什么是今天的CPU可以做到而去年还做不到的呢?那两年,五年或者十年之前的CPU又如何呢?我最感兴趣的事是,哪些程序员需要自己动手才能充分利用的功能(或者不得不重新设计编程环境)。我想,这不该包括超线程/SMT,但我并不确定。我也对暂时CPU做不到但是未来可以做得到的事感兴趣。
本文内容除非另有说明,都是指在x86和Linux环境下。历史总在重演,很多x86上的新事物,对于超级计算机、大型机和工作站来说已经是老生常谈了。
现代CPU拥有更宽的寄存器,可寻址更多内存。在上世纪80年代,你可能已经使用过8位CPU,但现在肯定已在使用64位CPU。除了能提供更多地址空间,64位模式(对于32位和64位操作通过x867浮点避免伪随机地获得80位精度)提供了更多寄存器和更一致的浮点结果。自80年代初已经被引入x86的其他非常有可能用到的功能还包括:分页/虚拟内存,pipelining和浮点运算。
本文将避免讨论那些写驱动程序、BIOS代码、做安全审查,才会用到的不寻常的底层功能,如APIC/x2APIC,SMM或NX位等。
在所有话题中,最可能真正影日常编程工作的是内存访问。我的第一台电脑是286在,那台机器上,一次内存访问可能只需要几个时钟周期。几年前,我使用奔腾4,内存访问需要花费超过400时钟周期。处理器比内存的发展速度快得多,对于内存较慢问题的解决方法是增加缓存,如果访问模式可被预测,常用数据访问速度更快,还有预取——预加载数据到缓存。
几个周期与400多个相比,听起来很糟——慢了100倍。但一个对64位(8字节)值块读取并操作的循环,CPU聪明到能在我需要之前就预取正确的数据,在3Ghz处理器上,以约22GB/s的速度处理,我们只丢了8%的性能而不是100倍。
通过使用小于CPU缓存的可预测内存访问模式和数据块操作,在现代CPU缓存架构中能发挥最大优势。如果你想尽可能高效,这份文件是个很好的起点。消化了这100页PDF文件后,接下来,你会想熟悉系统的微架构和内存子系统,以及学习使用类似likwid这样的工具来分析和测验应用程序。
芯片里也有小缓存来处理各种事务,除非需要全力实现微优化,你并不需要知道解码指令缓存和其他有趣的小缓存。最大的例外是TLB——虚拟内存查找缓存(通过x86上4级页表结构完成)。页表在L1数据缓存,每个查询有4次,或16个周期来进行一次完整的虚拟地址查询。对于所有需要被用户模式内存访问的操作来说,这是不能接受的,从而有了小而快的虚拟地址查找的缓存。
因为第一级TLB缓存必须要快,被严重地限制了尺寸。如果使用4K页面,确定了在不发生TLB丢失的情况下能找到的内存数量。x86还支持2MB和1GB页面;有些应用程序会通过使用较大页面受益匪浅。如果你有一个长时间运行,且使用大量内存的应用程序,很值得研究这项技术的细节。
最近二十年,x86芯片已经能思考执行的次序(以避免因为一个停滞资源而被阻塞)。这有时会导致很奇怪的表现。x86非常严格的要求单一CPU,或者外部可见的状态,像寄存器和记忆体,如果每件事都在按照顺序执行都必须及时更新。
这些限制使得事情看起来像按顺序执行,在大多数情况下,你可以忽略OoO(乱序)执行的存在,除非要竭力提高性能。主要的例外是,你不仅要确保事情在外部看起来像是按顺序执行,实际上在内部也要真的按顺序。
一个你可能关心的例子是,如果试图用rdtsc测量一系列指令的执行时间,rdtsc将读出隐藏的内部计数器并将结果置于edx和eax这些外部可见的寄存器。
假设我们这样做:
foordtscbarmov %eax, [%ebx]baz其中,foo,bar和baz不去碰eax,edx或[