《Python高性能编程》——第1章 理解高性能Python 1.1 基本的计算机系统

第1章 理解高性能Python

读完本章之后你将能够回答下列问题

  • 计算机架构有哪些元素?
  • 常见的计算机架构有哪些?
  • 计算机架构在Python中的抽象表达是什么?
  • 实现高性能Python代码的障碍在哪里?
  • 性能问题有哪些种类?

计算机编程可以被认为是以特定的方式进行数据的移动和转换来得到某种结果。然而这些操作有时间上的开销。因此,高性能编程可以被认为是通过降低开销(比如撰写更高效的代码)或改变操作方式(比如寻找一种更合适的算法)来让这些操作的代价最小化。

数据的移动发生在实际的硬件上,我们可以通过降低代码开销的方式来了解更多硬件方面的细节。这样的练习看上去可能没什么用,因为Python做了很多工作将我们对硬件的直接操作抽象出来。然而,通过理解数据在硬件层面的移动方式以及Python在抽象层面移动数据的方式,你会学到一些编写高性能Python程序的知识。

1.1 基本的计算机系统

一台计算机的底层组件可被分为三大基本部分:计算单元,存储单元,以及两者之间的连接。除此之外,这些单元还具有多种属性帮助我们了解它们。计算单元有一个属性告诉我们它每秒能够进行多少次计算,存储单元有一个属性告诉我们它能保存多少数据,还有一个属性告诉我们能以多快的速度对它进行读写,而连接则有一个属性告诉我们它们能以多快的速度将数据从一个地方移动到另一个地方。

通过这些基本单元,我们就可以在各种不同的复杂度级别上描述一个标准工作站。例如,一个标准工作站可以被看作是具有一个中央处理单元(CPU)作为其计算单元,两个独立的存储单元,分别是随机访问内存(RAM)和硬盘(各自有不同的容量和读写速度),最后还有一个总线将所有这些连接在一起。然而,我们还可以深入CPU并发现其内部也有多个存储单元:L1、L2,有时甚至有L3和L4缓存,它们的容量较小(从几KB到十几MB)但速度非常快。这些额外的存储单元通过一个被称为后端总线的特殊总线连接CPU。另外,新的计算机架构通常会有新的配置(比如Intel的Nehalem架构的CPU用英特尔快速通道互联技术替换了前端总线并重新构建了很多连接)。最后,在上述案例的讨论中,我们还忽略了网络连接,这是一种慢速的连接,用于连接其他许多潜在的计算单元和存储单元。

为了帮助理清这些错综复杂的结构,让我们去浏览一下这些基本单元的简要描述。

1.1.1 计算单元

一台计算机的计算单元是其中央部件——它具有将接收到的任意输入转换成输出的能力以及改变当前处理状态的能力。CPU是最常见的计算单元;然而,最初被设计用于加速计算机图像处理的图形处理单元(GPU)现在变得更加适用于数值计算了,这是因为其自身的并行模式使得大量计算能并发进行。无论哪种类型,一个计算单元都会接收一系列比特(比如代表数字的比特)并输出另外一堆比特(比如代表这些数字之和的比特)。除了实数的基本算数操作和二进制的比特操作以外,一些计算单元还提供了非常特殊的操作,比如“乘加混合计算”,接收三个数字A、B、C并返回A * B + C的值。

计算单元的主要属性是其每个周期能进行的操作数量以及每秒能完成多少个周期。第一个属性通过每周期完成的指令数(IPC)[1]来衡量,而第二个属性则是通过其时钟速度衡量。当新的计算单元被制造出来时,它们的这两个参数总是互相竞争。比如Intel的Core系列具有非常高的IPC但时钟速度较低,而Pentium 4的芯片则完全相反。不过话又说回来,GPU的IPC和时钟速度都很高,但它们有别的问题,我们后面会提到。

另外,当时钟速度提高时,能够立即提高该计算单元上所有的程序运行速度(因为它们每秒能进行更多运算),而提高IPC则在矢量计算能力上有相当程度的影响。矢量计算是指一次提供多个数据给一个CPU并能同时被操作。这种类型的CPU指令被称为SIMD(单指令多数据)。

总之,计算单元在过去十年的进展颇为有限(图1-1)。时钟速度和IPC的提升都限于停滞,因为晶体管已经小到了物理的极限。结果就是芯片制造商开始依靠其他手段来获得更高的速度,包括超线程技术,更聪明的乱序执行和多核架构。

超线程技术为主机的操作系统(OS)虚拟了第二个CPU,而聪明的硬件逻辑则试图将两个指令线程交错地插入单个CPU的执行单元。如果成功插入,能比单线程提升30%。一般来讲,当两个线程的工作分布在不同的执行单元上时,这样做效果不错——比如一个操作浮点而另一个操作整数时。

乱序执行允许编译器检测出一个线性程序中某部分可以不依赖于之前的工作,也就是说两个工作能够以各种顺序执行或同时执行。只要两个工作的成果都能够在正确的时间点上依次得到,哪怕它们的计算次序跟程序设计不同,程序也能继续正确运行。这使得当一些指令被阻塞时(比如等待一次内存访问),另一些指令得以执行,以此来提升资源的利用率。

最后也是对于高级程序员来说最重要的是多核架构的普及。这些架构在同一个计算单元中包含了多个CPU,提高了总体计算能力,而且无须等待内存屏障,让单个核心可以跑得更快。这就是为什么现在已经很难找到少于双核的计算机了——双核意味着计算机有两个互连的物理计算单元。虽然这增加了每秒可以进行的操作总数,但是想要让两个计算单元都达到最大利用率的话还需要考虑很多错综复杂的因素。

给CPU增加更多的核心并不一定能提升程序运行的速度。这是由阿姆达尔定律决定的。简单地说,阿姆达尔定律认为如果一个可以运行在多核上的程序有某些执行路径必须运行在单核上,那么这些路径就会成为瓶颈导致最终速度无法通过增加更多核心来提高。

比如,假设我们有一个调查需要100个人参与,该调查需要花费1分钟,如果我们只有一位提问者(该提问者向第一位参与者提问,等待回答,然后移向下一位参与者),那么我们可以用100分钟完成这个任务。这个单人提问并等待回答的流程就是一个顺序执行的过程。对于一个顺序执行的过程,我们每次只能完成一个操作,后面的操作必须等待前面的操作完成。

然而,如果我们有两位提问者,他们就可以同时进行测试,用50分钟完成任务。这是由于两位提问者之间不需要互相了解,没有依赖关系,所以整个任务就很容易划分。

增加更多提问者可以进一步提速,直到我们有100位提问者。此时,整个流程仅需要1分钟就可以完成,仅取决于参与者回答问题所需要的时间。继续增加提问者将不会带来任何速度提升,因为这些多余的提问者无事可干——所有的参与者都已经在接受调查!此时,唯一能够降低整体时间的办法是降低单个参与者完成调查的时间,也就是降低顺序部分所需要的执行时间。同样,对于CPU,我们可以增加更多的核心直到某个必须单核执行的任务成为瓶颈。也就是说,任何并行计算的瓶颈最终都会落在其顺序执行的那部分任务上。

另外,对于Python来说,充分利用多核性能的阻碍主要在于Python的全局解释器锁(GIL)。GIL确保Python进程一次只能执行一条指令,无论当前有多少个核心。这意味着即使某些Python代码可以使用多个核心,在任意时间点仅有一个核心在执行Python的指令。以前面调查的例子来说,即使我们有100位提问者,然而一次仅有一位可以提问和接受回答,并没有什么用!这看上去是个严重的阻碍,特别是当现在计算机发展的趋势就是拥有更多而非更快的计算单元的时候。好在这个问题其实可以通过一些方法来避免,比如标准库的multiprocessing,或numexpr、Cython等技术,或分布式计算模型等。

1.1.2 存储单元

计算机的存储单元被用于保存比特。这些比特可能代表程序中的变量,或一幅图片的像素。存储单元的概念包括了主板上的寄存器、RAM以及硬盘。所有这些不同类型的存储单元的主要区别在于其读写数据的速度。更复杂的问题在于,其读写数据的速度还与数据的读写方式息息相关。

比如,大多数存储单元一次读一大块数据的性能远好于读多次小块数据(顺序读取VS随机数据)。将这些存储单元中的数据想象成一本书中的书页,大多数存储单元的读写速度在连续翻页时高于经常从一张随机页跳至另一张随机页。

所有的存储单元或多或少都受到这一影响,但不同类型存储单元受到的影响却大不相同。

除了读写速度以外,存储单元还有一个延时的属性,表示了设备为了查找到需要的数据所花费的时间。一个旋转硬盘的延时可能较高,因为磁盘必须物理旋转到一定速度且读取磁头必须移动到正确的位置。而从另一方面来说,RAM的延时就比较小,因为一切都是固态的。下面是一个标准工作站内常见的各类存储单元的简短描述,以读写速度排序:

旋转硬盘

计算机关机也能保持的长期存储。读写速度通常较慢,因为磁盘必须物理旋转和等待磁头移动。随机访问性能下降但容量很高(TB级别)。

固态硬盘

类似旋转硬盘,读写速度较快但容量较小(GB级别)。

RAM

用于保存应用程序的代码和数据(比如用到的各种变量)。具有更快的读写速度且在随机访问时性能良好,但通常受限于容量(GB级别)。

L1/L2缓存

极快的读写速度。进入CPU的数据必须经过这里。很小的容量(KB级别)。

图1-2展示了当今市面上可以见到的这几类存储单元的区别。

一个清晰可见的趋势是读写速度和容量成反比——当我们试图加快速度时,容量就下降了。因此,很多系统都实现了一个分层的存储:数据一开始都在硬盘里,部分进入RAM,然后很小的一个子集进入L1/L2缓存。这种分层使得程序可以根据访问速度的需求将数据保存在不同的地方。在试图优化程序的存储访问模式时,我们只是简单优化了数据存放的位置、布局(为了增加顺序读取的次数),以及数据在不同位置之间移动的次数。另外,异步I/O和缓存预取等技术还提供了很多方法来确保数据在被需要时就已经存在于对应的地方而不需要浪费额外的计算时间——这些过程可以在进行其他计算时独立进行!

1.1.3 通信层

最后,让我们看看这些基本单元如何互相通信。通信有很多模式,但它们都是同一样东西的变种:总线。

比如说,前端总线是RAM和L1/L2缓存之间的连接。它将已经准备好被处理器操作的数据移入一个集结场所以备计算所需,又将计算结果移出。除此之外还有其他总线,如外部总线就是硬件设备(如硬盘和网卡)通向CPU和系统内存的主干线。该总线通常比前端总线慢。

事实上,L1/L2缓存的很多好处实际上是来自更快的总线。因为可以将需要计算的数据在慢速总线(连接RAM和缓存)上攒成大的数据块,然后以非常快的速度从后端总线(连接缓存和CPU)传入CPU,这样CPU就可以进行更多计算而无须等待这么长的时间。

同样,使用GPU的不利之处很多都来自它所连接的总线:因为GPU通常是一个外部设备,它通过PCI总线通信,速度远远慢于前端总线。结果,GPU数据的输入输出就像是一种抽税操作。异质架构,一种在前端总线上同时具有CPU和GPU的计算机架构的兴起就是为了降低数据传输成本,使得GPU能够被使用在需要传输大量数据的计算上。

除了计算机内部的通信模块,网络可以被认为是另一种通信模块。不过这个模块就比之前讨论的更为灵活,一个网络设备可以直接连接至一个存储设备,如网络连接存储(NAS)设备或计算机集群中的另一台计算机节点。但是网络通信通常要比之前讨论的其他类型的通信慢很多。前端总线每秒可以传输数十GB,而网络则仅有数十MB。

现在我们清楚,一条总线的主要属性是它的速度:在给定时间内它能传输多少数据。该属性由两个因素决定:一次能传输多少数据(总线带宽)和每秒能传输几次(总线频率)。需要说明的是一次传输的数据总是有序的:一块数据先从内存中读出,然后被移动到另一个地方。这就是为什么总线的速度可以被拆分为两个因素,因为这两个因素分别独立影响计算的不同方面:高的总线带宽可以一次性移动所有相关数据,有助于矢量化的代码(或任何顺序读取内存的代码),而另一方面,低带宽高频率有助于那些经常随机读取内存的代码。有意思的是,这些属性是由计算机设计者在主板的物理布局上决定的:当芯片之间相距较近时,它们之间的物理链路就较短,就可以允许更高的传输速度。而物理链路的数量则决定了总线的带宽(带宽这个词真的具有物理上的意义!)。

由于物理接口可以针对某个特定应用优化,所以我们不会奇怪世上存在成百上千种不同类型的连接。图1-3显示了一些常见接口的比特率。注意这图上完全没提到连接的延时,延时决定了一个连接响应数据请求花费的时间(虽然延时跟具体的计算机系统息息相关,但是有来自物理接口的一些基本限制)。

时间: 2024-11-03 21:48:48

《Python高性能编程》——第1章 理解高性能Python 1.1 基本的计算机系统的相关文章

《Python游戏编程入门》——1.2 初识Python

1.2 初识Python Python既是一个软件工具包,也是一种语言.Python软件包包含了一个名为IDLE的编辑器.Idle是一个人的名字,而不是集成开发(integrated development-)的缩写,尽管IDLE看上去有点像是缩写.这个人的名字是Eric Idle,他是Monty Python的创始成员之一,而Monty Python则是Python语言的名称的由来,Python是向British TV的一部电视剧致敬.Python语言也很奇怪,因此,它这个名字是很合适的.当然

《Python硬件编程实战》——1.3 Python的特点

1.3 Python的特点 1.3.1 作为脚本语言的优缺点上面已经介绍过,Python是一门脚本语言,也是一门解释型语言.下面就来简单解释一下作为解释型语言的Python有哪些特点.1.作为脚本语言的Python的优点快速开发:不需要编译即可运行 正如前面的解释,写完Python脚本后直接就可以运行而省去编译的步骤,使用起来相对省事和高效.2.作为脚本语言的Python的缺点性能相对不是特别强Python的性能相对一些其他语言(比如C.C++等)来说不是特别强.对于性能要求比较苛刻的某些领域不

《Python硬件编程实战》——2.2 Python的两大版本

2.2 Python的两大版本 目前Python主要有两个大的版本: Python 2 Python 3 Python两大版本的不同写法 关于两种版本的对比,也常写成为 Python 2.x VS Python 3.x Python 2 VS Python 3 py2 VS py3 读者以后看到类似写法,明白其指的是Python 2和Python 3就可以了.2.2.1 Python版本历史 为了更深入地理解Python 2和Python 3的区别,此处先简要介绍Python版本的发展历史. P

《Python硬件编程实战》——1.4 Python的应用

1.4 Python的应用 了解了Python是什么以及它的众多特点后,读者很自然地就会想到一个问题:Python能干什么? 而此处普通的这一问句"Python能干什么"的背后其实有着更深层的含义.严格地说至少包含如下三层含义. 1. Python能干什么 从语言本身的层面来说,Python这门编程语言可以实现哪些功能以及不能实现哪些 功能. 2. Python更适合做些什么 作为其中一种编程语言,Python语言根据其语言的特点和优势更适合做哪些事情. 3.你能用Python干什么

Python树莓派编程第3章

第3章 Python介绍 你可能还记得我们在第1章中提到,制作树莓派的初衷是为了让每个人(尤其是孩子们)都拥有编程的环境.为了实现该目的,树莓派的创造者们想要推出一台价格相对便宜但性能十分强劲的计算机,每个人都可以将这台计算机连接至键盘.鼠标.显示器进行编程. 创造树莓派的另一个原因是希望简化编程.为此,Eben Upton和他的同伴决定将Python语言集成到树莓派的操作系统中.他们认为,Python是一种强大的编程语言,那些没有编程经验的人也可以轻松快速地学会. 在本章,我将对Python进

《Python硬件编程实战》——1.5 Python的必备常识

1.5 Python的必备常识 有一些和Python相关的常识性的知识,即使是Python初学者也需要先了解清楚.下面就来介绍这些内容.1.5.1 Python文件的后缀 在计算机的世界里,多数文件的类型都是通过文件的后缀来区分的.Python文件的后缀是.py.换句话说,我们见到的Python代码文件的文件名就是类似于xxx.py的.1.5.2 Python的缩写和简称 正是由于Python文件的后缀是.py,所以很多人也常用py来指代Python.比如后文中将会看到的,有些人把Python

《Python高性能编程》——1.2 将基本的元素组装到一起

1.2 将基本的元素组装到一起 仅理解计算机的基本组成部分并不足以理解高性能编程的问题.所有这些组件的互动与合作还会引入新的复杂度.本段将研究一些样本问题,描述理想的解决方案以及Python如何实现它们. 警告:本段可能看上去让人绝望--大多数问题似乎都证明Python并不适合解决性能问题.这不是真的,原因有两点.首先,在所有这些"高性能计算要素"中,我们忽视了一个至关重要的要素:开发者.原生Python在性能上欠缺的功能会被迅速开发出来.另外,我们会在本书中介绍各种模块和原理来帮助减

《Python高性能编程》——导读

前 言 Python很容易学.你之所以阅读本书可能是因为你的代码现在能够正确运行,而你希望它能跑得更快.你可以很轻松地修改代码,反复地实现你的想法,你对这一点很满意.但能够轻松实现和代码跑得够快之间的取舍却是一个世人皆知且令人惋惜的现象.而这个问题其实是可以解决的. 有些人想要让顺序执行的过程跑得更快.有些人需要利用多核架构.集群,或者图形处理单元的优势来解决他们的问题.有些人需要可伸缩系统在保证可靠性的前提下酌情或根据资金多少处理更多或更少的工作.有些人意识到他们的编程技巧,通常是来自其他语言

python核心编程--笔记(不定时跟新)

的解释器options: 1.1 –d   提供调试输出 1.2 –O   生成优化的字节码(生成.pyo文件) 1.3 –S   不导入site模块以在启动时查找python路径 1.4 –v   冗余输出(导入语句详细追踪) 1.5 –m mod 将一个模块以脚本形式运行 1.6 –Q opt 除法选项(参阅文档) 1.7 –c cmd 运行以命令行字符串心事提交的python脚本 1.8 file   以给定的文件运行python脚本 2 _在解释器中表示最后一个表达式的值. 3 prin