前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >CPU简介

CPU简介

作者头像
Peter Lu
发布2018-06-20 10:35:31
1.3K0
发布2018-06-20 10:35:31
举报
文章被收录于专栏:LETLET

最近阅读了一些关于CPU的资料,自感收获颇丰。本文算是读后感,整理出来和大家分享。

CPU Pipeline

严格讲我不是CS专业,不清楚CS本科是否需要学习CPU架构。或者说,在这个软件高度集成的时代,软件工程师有必要掌握这些细节吗?我的答案是:学以致用的角度,不需要;如果你专注于性能优化,则有借鉴意义。

上图是一段简单的汇编以及个人注释,主要看气质。第一,所有的代码编译为汇编指令(Instruction),然后通过CPU执行,比如cmp(对比),add(加法),mulsd(浮点乘法)等。第二,在这个过程中,通过对应的寄存器,比如eax,xmm,将变量传递给CPU,寄存器有不同的类型,数目有限,通过mov指令,填补指令间的裂缝。

编译成汇编码后,运行阶段CPU会“逐条”(未必)执行每一条指令并返回结果,不同指令需要消耗不同的cycles,这个过程称之为Latency。下图是AMD K7中MOV指令对应的消耗:

CPU需要四个阶段来翻译每一条指令:

  • Fetch 从内存中获取该指令
  • Decode 解码
  • Execute 执行指令
  • Writeback 将结果写回

上图是执行一条指令的完整过程,CPI(Cycles Per Instruction) = 4。实际上,CPU也是流水线作业,同一时间执行多个指令。当这条指令将要进入execute阶段时,下一条指令也准备进入decode阶段,再下一条也开始fetch阶段,这样,单个指令花费的时间不变,但总体看,CPI = 1,性能提升了4倍,如下图所示:

人性是贪婪的,在这个基础上又不断优化,就是下面要介绍的Superpipelining & Superscalar。

不同指令的复杂度是不同的,但瓶颈总是那个最慢的指令。打个比方,马路上都是小汽车,路况流畅,突然来了一大卡,则容易阻滞。Superpipelining的思路是:以Logic gate(逻辑运算的基本组件)为最小单位,将复杂指令(大卡)拆分。结果是,CPU流水线变长(Deeper Pipeline),拆分后的指令可以达到CPI=1,且每秒能运行更多的Cycles。

Intel Pentium 4采用这种技术,将流水线扩展到31个stages,主频高达5k MHz。但在实际应用中,性能并没有太多提升,而Athlon XP和Motorola G4的性能反而更佳,因此Intel缩小到20 stages。这可以归咎于理论和现实的差距,首先,流水线变深是一种面向未来的设计方案,短时间内很难提升CPU的主频,因此性能提升有限;其次,流水线过长,会增大指令间的依赖关系,导致预判准确率下降(下面会提到)。

这样,流水线的Execute阶段实际上是功能单元的集合,各单元只负责自己的业务。这时自然会想到,是否可以并行执行指令,进而显著提升性能。但要做到这点,就要提高fetch和decode的能力,才能提供足够多的执行任务(execution resources)。实际上,Pentium 4和Athlon XP可以每个周期解码2.5个指令。而这种提供多个流水线的方式,就是superscalar。如下图,CPI = 1/3。

同样,这种策略也会有一些实际问题,比如内存存取数据性能的不足,无法获取足够多的数据,造成浪费,并行逻辑的复杂度,也容易造成预判准确率降低。

实际上还有一种VLIW(very long instruction word),如上所示,而实际中是对这几种策略的融合。

Dependencies& Prediction

按照上面的思路,扩展多条流水线,增加每条流水线的深度,不就可以提升性能吗?但实际应用则是另一个层面了。我们看下面这段代码:

a = b * c;

d = a + 1;

很简单的两行代码,第二行指令依赖第一行指令的结果。因此,处理器会挂起第二行指令,直到变量a的结果可用。这么宝贵的时间,白白浪费多么可惜,于是CPU决定调整程序的运行顺序,在挂起时找到其他合适指令来填补这些流水线bubbles。在技术上分为static(编译器)和dynamic(处理器)两种形式。

程序员应该了解编译器常见的优化选项,这个好处是编译阶段,可以提供足够多的资源和时间,编译器可以针对整段代码认真分析。但这种方案不可能做到准确的预测未来。

另外一种则是运行时的优化策略,称为OOO(out of order execution)。要做到运行时的乱序,要有能力记录指令间的依赖关系。为了简化变量间的依赖关系,一个有效的办法就是对变量重命名(Register Renaming)。这样,指令间的变量都对应独立的寄存器,进而实现并行化。这种策略的好处是动态的,同样的程序,不需重编译,自动新CPU的性能提升,但提高的CPU的逻辑复杂度。

我们再看另一种逻辑判断:

if(a>0){

...

}else{

...

}

在这,有两个branch,我们无法预知应该执行哪一个,但为了保证流水线的效率,CPU决定提前猜一个branch,猜对了就继续,猜错了就要flush,之前的准备白费。所以,问题的关键就是提高预测的准确度。

同样,这种预测分为static编译器和dynamic运行时两类。比如if逻辑则预判进入第一个分支forward,而while则预判是返回到循环体backward,但这种static预测通常只对循环较为有效。另一种则是运行时策略,比如记录上一次执行的分支,或者引用计数,作为预判的依据等,因此,需要占据一点芯片资源,随着流水线深度的加大,mispredict的可能性也加大。据统计,Pentium Pro/II/III中,30%的性能都浪费在预测失败上。因此,现代处理器分配更多的硬件资源来进行分支预测,比如不同分支间的关联,历史记录,多分支预测等,但即便如此,准确度只能达到95%。

另一种方案是提供条件判断指令(predicated instruction),在此就不介绍了。

Parallelism

讲到这,就是我自认为最有意思的讨论,提升CPU性能有两个思路,提高计算能力,让它能搬更多的砖,或者让它变得更聪明,提高效率。

我们不妨看看Intel的发展历程。早期因为CISC指令过于复杂,只能让CPU更聪明,后来,出于和AMD的竞争的需要,则专注于计算速度。同时,在IA-64上进行了大量编译器优化策略。面对IA-64的失败,Pentium 4过于追求速度带来的能耗,发热问题,以及AMD在低主频下良好的性能,Intel又重拾智慧路线。另外,在这个过程中,Intel在运行时期间对X86的指令进行简化,分解为RISC风格的微指令,称为μops。

可见,大家都属于局部激进,整体中庸的发展模式。因为大家心里都明白,在理论上,每一种方式都有一堵无法逾越的墙。

  • Thepower wall 目前,运算速度提升30%,则需要两倍的电压和发热,并且这种设计思路无法满足移动设备,也不可能长久
  • TheIPL wall 目前多数应用并没有很好的并行化设计(指令级别)
  • Thememory wall 内存和CPU在性能上的差距拉大,供不应求,后面会提到。

老一点的程序员应该都读过一篇文章《The Free Lunch Is Over》,不妨重读一下,里面有一句话“Chipdesigners are under so much pressure to deliver ever-faster CPUs that they’llrisk changing the meaning of your program, and possibly break it, in order tomake it run faster”。或者可以说,工程师在这三面墙上取得了巨大的进展,软件不做任何改进就可以等到性能的提升,但现在接近临界点了。面对窘境,Intel率先在Pentium 4支持SMT(Simultaneous multi-threading)技术,开启了多线程,多核方面的尝试。

事情的发展并非一帆风顺,多线程的价值是建立在一种假设之上:当前有很多应用程序在运行,或者一个程序内,有很多线程同时运行。然后,并不是所有场景都能符合如上的假设,比如数据库,三维图形渲染或科学运算,对并行运算有较高的要求,能很好的发挥多线程的优势(其实,也受限于带宽),但其他应用,比如浏览器,并不需要多线程或没有多线程的设计,则无法体现其价值。同时,因为是在一个Core中模拟多线程,线程切换的状态管理也是一种成本,因此,早期的SMT表现很诡异,性能跟具体程序有强连接。有时候,仿佛是两个高效的Core,有时候,就好比两个慵懒的Core,有时候,还不如一个Core实在。SMT在P4的速度的提升在-10%到30%之间,差强人意。在不断的探索后,目前Intel的Core i系列的设计方式是2^n个Cores每个Core支持两线程。在这个过程中,我们意识到,不同的应用程序,对多核和多线程的依赖程度是不一样的,因此,可以针对不同的应用场景,推出适合的CPU方案。

这样就有一个权衡:典型的SMT设计,要支持Superscalar,支持OOO,设计复杂度的提高,自然会占用更多的空间,因此,一个聪明的Core所占用的空间,可以装得下6个简单的Core(领导曾对我说:一个老员工的工资可以招三个应届生)。如上图,同样10亿个晶体管,左侧是Intel酷睿4核,共8个线程,后者是Sun的UltraSPARC,16核共128个线程。索尼PS3主机在设计上大胆创新,一个聪明的Core配N个简单的Cores,但产生很多兼容问题;再比如针对移动环境低能耗,便携和低性能,实现了CPU,GPU,网卡,IO等一体化的芯片设计。

经历了Instruction,Thread并行化后,还有Data并行化的方式,这也是本次我的一大收获:SIMD(single instruction, multiple data),也称为向量化处理。

对于C++程序员,不妨了解一下,目前Intel和Github上都有一些资料和开源库,可以学习参考,如果精力允许,不妨测试一下性能提升是否显著,特别是结合OpenMP等多线程机制,可以考虑对部分函数进行vectorization改造。

Memory Cache

上面基本上涵盖了CPU的主要知识点,还留下一个小尾巴,就是缓存。从处理器的角度而言,Cache就是处理器和内存之间一块空间小,但速度快的内存。因为人们发现程序在存取内存时,具有重复性(循环利用)和连续性(内存相邻),于是通过一种策略,将“更可能选中”的数据放在缓存中,从来缓解Memory Wall造成的Bubbles。

C++程序员不妨看一下《STL源码剖析》,里面也提到了STL的内存池概念,三级缓存的方式设计内存池。比如你看书,会把最近看的,重要的书放在桌子上,唾手可得;另外一些还不错的书放在书架上,挪一下屁股就可以找到;其他书放在箱子里,就需要折腾一翻。

一分钱一分货,给你这些空间了,如何有效的利用缓存,提高命中率,就是一件严肃的问题了。如下是时间成本的数学公式,而我们能优化的空间就是让tpp三变量尽可能小:

先从最简单的direct map,也称为One-way set associative。首先,我们看一下缓存存储方式,类似hashtable:

如上,根据内存地址的后三位,来指定映射关系,这样,缓存中有8个block,内存中平均4个block对应Cache中的一个block。

这样,就有一个问题,我只知道后三位,那如何知道当前缓存的数据对应的是四个block中的哪一个呢?这里引入一个tag概念,对应00,01,10,11四种可能,在查找时,如下图所示,判断Address的低位找到对应的Index,根据tag和高位判断是否命中。

该设计的好处是直接定位,缺点是易冲突。而且以动态的眼光来看,CPU寻址的速度会越来越快,这种设计的优势就越来越次要,而内存会越来越大,hash冲突的概率会越来越明显。这样,在Cache空间不变的情况下,如何降低冲突的可能?这里就要介绍一下目前主流的缓存设计方案:setassociative。

如上,思路就是扁平化,比如单路下的冲突属于设计问题,没有缓冲空间;而在这种情况下,N way就说明冲突的数据还有N个block并存,比如采用Least-Recently used(LRU)来提高命中率。

总结

坦白说,本文所有的思路和要点都不是我的,但每个字已融入我的血液中。而让我欣慰的是,能够把CPU的这些看似破碎的知识点连贯起来,从中窥探CPU发展的来龙去脉;再者结合软件开发中的一些经历,对一些问题的理解更深刻了,比如SIMD和Memory Cache。写到这,我很满足。

纵观CPU的发展,我觉得高速公路的例子形象。早期潜力大,我们优化路况,让每一辆车可以更快的到达目的地。当这一方式接近极限或应用成本无法接受时,我们开始扩宽路面,这样,每辆车的时间不会减少,但可以承载更多的车。其实,很多事物,甚至个人的发展,也都有共性。

同样的工资,你需要的是一个大学教授,还是三个搬砖工?当你的领导已经在考虑这个问题时,我只想说,你走你的路 直到我们无法接触。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-03-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 LET 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • CPU Pipeline
  • Parallelism
  • Memory Cache
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档