几句话说清楚10:Skylake微架构(Microarchitecture)剖析(2)

前端

处理器在前端这一部分的时候还是顺序(in-order)处理的,主要是也确实没什么乱序的空间。虽然说是顺序,但前端因为贴近业务,所以受人写的代码的影响也比较大。如果仅仅只是“取指令->解码”,恐怕需要写程序的人是个非常聪明的程序员。前端很多组件的工作其实都是在填程序员的坑,这也是我比较心疼前端的地方。

Fetch

前端的任务,首先是从内存中取得指令。同读取数据类似,CPU通过查询页表获得指令所在的内存地址,同时把指令塞到CPU的L1指令缓存里。

具体要把哪个地址上的指令数据送到L1I$里,这是分支预测器(Branch predictor)的工作。作为CPU的核心技术,Intel并没有透露太多信息,我们这里也只好一笔带过。不过它的细节也许很复杂,但它的脾气很好掌握:和我们很多人不喜欢自己的工作一样,它的工作就是处理分支,但它最不喜欢分支。

在Skylake架构里,L1I$大小为32KB,组织形式为8-way set associative(关于CPU缓存组织形式的讲解可以参照这篇),每个Cycle可以取16Byte长度(fetch window)的指令。如果你开了Hyper-thread,那么同一个物理核上的两个逻辑核均分这个fetch window,每个Cycle各占用一次。

所以没事别开Hyper-thread,不过我这么说没有任何技术依据,单纯是帮Intel多卖几个核。

在L1I$里的指令还都是变长的x86 macro-ops,也就是我们看到的那些编译之后的汇编指令。如果熟悉这些指令的话,就会知道这些指令的长度(就是那些二进制数字)都不一样,同时一条指令有时可以由好几个操作组成。

这种指令对CPU的执行单元来说是很不友好的,同时如果想要通过乱序执行提高指令的并行度,减小指令的粒度也是必须的步骤。因此需要把这些marco-ops“解码”为“micro-ops”。

当然具体的解码工作还在后面。从L1I$中取得指令数据后,首先要进入“预解码”阶段,在这里需要识别出在一个fetch window中取得的这16个Byte的数据里面有多少个指令。除此之外,还需要对一些特殊指令,比如分支转跳打上一些标记。

但因为指令变长的原因,16个Byte往往并不对应固定的指令数,还有可能最后一个指令有一半在这16Byte里,另一边还在后面。另外就是pre-decode在一个Cycle最多识别出6个指令,或者这16Byte的数据都解析完。如果你这16个Byte里包含有7个指令,那么第一个Cycle识别出前6个之后,还需要第二个Cycle识别最后一个,然后才能再读取后面16Byte。

那么pre-decode的效率就变成了3.5 instruction / cycle,比最理想的情况6 instruction / cycle降低了41%,现实就是这么残酷。

经过pre-decode之后,才真正从16Byte的二进制数据中识别出了指令,这些指令下一步要被塞到一个队列里(Instruction Queue)看看有没有什么能被优化的地方。一个最常见的优化方式就是macro-op fusion,就是把两个相邻的,且能被一个指令表示的指令,用那一个指令替换掉。比如:

1
2
cmp eax, [mem]
jne loop

直接换成

1
cmpjne eax, [mem], loop

当然既然决定这么替换,新指令对流水线的开销肯定小于被替换的两个指令之和。如此可以减轻一部分后端执行单元的工作负荷和资源开销。

OK, 在取得了指令数据,识别出了数据中的指令,并对指令做了一些优化合并之后,就该开始正儿八经地解码了,这部分在后面的文章中介绍。

© 2020 DecodeZ All Rights Reserved. 本站访客数人次 本站总访问量
Theme by hiero