Python:

系统 1986 0

《python源码剖析》阅读笔记

第一章 python的内建对象

python中一切都是对象。在PyIntObject中定义了很多函数指针,这些函数指针对应着类型对象所定义的操作。其中有三组非常重要的操作族,tp_as_number, tp_as_sequence, tp_as_mapping,分别对应着PyNumberMethods, PySequenceMethods, PyMappingMethods函数族,这三个函数都是分别定义着一个整数对象、序列对象、关联对象应该有的一些操作。在python中对于任何一种类型来说,都可以同时定义三个函数族中的所有操作,也就是说,我们可以在继承python固有的类型时,又同时重写python的special method,从而实现我们自己定义的特殊类型。比如可以实现一个对象,既可以表现出数值特性,也可以表现出关联对象的属性。

第二章 python中的整数对象

python的整数对象。在python中会有一个整数对象池,里面维护着-5到256之间的整数,当我们需要使用这些范围的数字时,直接到对象池中去提取。这样设计是因为在实际的开发中,这些小数可能会被频繁的使用,频繁的创建和湮灭会导致效率非常低。对于超出这些范围的数字,python底层则是通过单独申请一块内存块来给这些大整数轮流使用,当某个对象的引用次数为零的时候,会把该对象所占用的这块内存添加到空闲区(注意,这里python并没有将释放的内存还给系统,所以当出现很多大数要被使用的时候,而这些数又是使用一次,可能就会占用系统的所有内存。)

第三章 python中的字符串对象

  1. 在python中,PyStringObject是字符串对象的实现,它是一个可变长度内存的对象,同时又是一个不可变对象(字符串定义之后不可再改变)。python字符串对象中的intern机制,类似于前面的python整数对象池。其关键是在于在系统中有一个(key, value)的映射关系集合,集合名字叫做Intered。在这个集合中记录过被intern机制处理过的字符串。当python在创建一个字符串时,首先会建立一个PyStringObject对象a,然后利用intern机制处理,如果当前这个字符串对象已经在interned中存在,记为b,则将指向a的对象的指针指向b,然后intern机制会将对象a的引用计数减为0而被销毁。这样的话,可以达到减少内存的目的。
    也许会有一个问题,为什么一定要先创建一个临时PyStringObject对象呢?
    这是因为intern机制只能应用在PyStringObject对象之上,别的任何对象都不可以。因此这里必须要先创建一个临时对象。
  2. python中也为一个字节的字符设计了字符缓冲池,与小整数对象池不一样的是,小整数对象池在python初始化的时候就会被创建,而字符串对象中的字符缓冲池是以静态变量形式存在的,在python初始化完成之后,字符缓冲池是空的。当python在创建一个PyStringObject的时候,如果字符串长度为1(即单个字符串),则将会对这个字符进行intern操作,将intern的这个结果缓存到字符缓冲池中。当通过PyString_FromStringandSize(const char *str, int size)来创建字符对象时,如果size等于1,并且在字符缓冲池中存在,则直接返回。
  3. python中字符串的连接效率问题。当我们采用+号来连接多个字符串时,效率是非常低的。其根源在于,python中的字符串对象是一个不可变对象,因此在连接两个字符串的时候,则要去新建一个PyStringObject对象,当我们用‘+’来连接N个字符串时,实际上会进行N-1次内存申请和内存搬运,因而效率很低。 解决方案是,我们采用字符串对象的join操作来实现多个字符串的连接,当使用join操作的时候,会一次性计算好需要的总的内存,也就是只需要执行一次内存申请,因此效率会比用‘+’号连接的效率更高
            
              strs_list = ['hello', 'fay', 'welcome', 'to', 'python ']
' '.join(strs_list)
            
          

https://www.cnblogs.com/megachen/p/9156646.html

Python源码读后小结

 

 


 

《改善Python程序的91个建议》

访问flyai.club,一键创建你的人工智能项目。

作者 | 笑虎

来源 | 代码湾

自己写Python也有四五年了,一直是用自己的“强迫症”在维持自己代码的质量,除了Google的Python代码规范外,从来没有读过类似的书籍。偶然的机会看到这么一本书,读完之后觉得还不错,所以做个简单的笔记。有想学习类似知识的朋友,又懒得去读完整本书籍,可以参考一下。

1:引论

建议1、理解Pythonic概念—-详见Python中的《Python之禅》

建议2、编写Pythonic代码

避免不规范代码,比如只用大小写区分变量、使用容易混淆的变量名、害怕过长变量名等。有时候长的变量名会使代码更加具有可读性。

深入学习Python相关知识,比如语言特性、库特性等,比如Python演变过程等。 深入学习一两个业内公认的Pythonic的代码库,比如Flask等。

https://angelteng.github.io/blog/2019/07/25/Flask源码学习/

 

建议3:理解Python与C的不同之处,比如缩进与{},单引号双引号,三元操作符?,Switch-Case语句等。

建议4:在代码中适当添加注释

建议5:适当添加空行使代码布局更加合理

建议6:编写函数的4个原则

函数设计要尽量短小,嵌套层次不宜过深

函数声明应该做到合理、简单、易用

函数参数设计应该考虑向下兼容

一个函数只做一件事,尽量保证函数粒度的一致性

建议7:将常量集中在一个文件,且常量名尽量使用全大写字母

2:编程惯用法

建议8:利用assert语句来发现问题,但要注意,断言assert会影响效率

建议9:数据交换值时不推荐使用临时变量,而是直接a, b = b, a

建议10:充分利用惰性计算(Lazy evaluation)的特性,从而避免不必要的计算

 

建议11:理解枚举替代实现的缺陷(最新版Python中已经加入了枚举特性)

建议12:不推荐使用type来进行类型检查,因为有些时候type的结果并不一定可靠。如果有需求,建议使用isinstance函数来代替

建议13:尽量将变量转化为浮点类型后再做除法(Python3以后不用考虑)

建议14:警惕eval()函数的安全漏洞,有点类似于SQL注入

建议15:使用enumerate()同时获取序列迭代的索引和值

建议16:分清==和is的适用场景,特别是在比较字符串等不可变类型变量时

建议17:尽量使用Unicode。在Python2中编码是很让人头痛的一件事,但Python3就不用过多考虑了

建议18:构建合理的包层次来管理Module

3:基础用法

建议19:有节制的使用from…import语句,防止污染命名空间

建议20:优先使用absolute import来导入模块(Python3中已经移除了relative import)

建议21:i+=1不等于++i,在Python中,++i前边的加号仅表示正,不表示操作

建议22:习惯使用with自动关闭资源,特别是在文件读写中

建议23:使用else子句简化循环(异常处理)

建议24:遵循异常处理的几点基本原则

注意异常的粒度,try块中尽量少写代码

谨慎使用单独的except语句,或except Exception语句,而是定位到具体异常

注意异常捕获的顺序,在合适的层次处理异常

使用更加友好的异常信息,遵守异常参数的规范

建议25:避免finally中可能发生的陷阱

建议26:深入理解None,正确判断对象是否为空。Python中下列数据会判断为空:

Python:_第1张图片

 

建议27:连接字符串应优先使用join函数,而不是+操作

建议28:格式化字符串时尽量使用.format函数,而不是%形式

建议29:区别对待可变对象和不可变对象,特别是作为函数参数时

建议30:[], {}和():一致的容器初始化形式。使用列表解析可以使代码更清晰,同时效率更高

 

建议31:函数传参数,既不是传值也不是传引用,而是传对象或者说对象的引用

建议32:警惕默认参数潜在的问题,特别是当默认参数为可变对象时

建议33:函数中慎用变长参数*args和**kargs

这种使用太灵活,从而使得函数签名不够清晰,可读性较差

如果因为函数参数过多而是用变长参数简化函数定义,那么一般该函数可以重构

建议34:深入理解str()和repr()的区别

两者之间的目标不同:str主要面向客户,其目的是可读性,返回形式为用户友好性和可读性都比较高的字符串形式;而repr是面向Python解释器或者说Python开发人员,其目的是准确性,其返回值表示Python解释器内部的定义

 

在解释器中直接输入变量,默认调用repr函数,而print(var)默认调用str函数

repr函数的返回值一般可以用eval函数来还原对象

两者分别调用对象的内建函数__str__()和__repr__()

建议35:分清静态方法staticmethod和类方法classmethod的使用场景

4:库

建议36:掌握字符串的基本用法

建议37:按需选择sort()和sorted()函数

sort()是列表在就地进行排序,所以不能排序元组等不可变类型。

sorted()可以排序任意的可迭代类型,同时不改变原变量本身。

建议38:使用copy模块深拷贝对象,区分浅拷贝(shallow copy)和深拷贝(deep copy)

建议39:使用Counter进行计数统计,Counter是字典类的子类,在collections模块中

建议40:深入掌握ConfigParser

建议41:使用argparse模块处理命令行参数

建议42:使用pandas处理大型CSV文件

Python本身提供一个CSV文件处理模块,并提供reader、writer等函数。

Pandas可提供分块、合并处理等,适用于数据量大的情况,且对二维数据操作更方便。

建议43:使用ElementTree解析XML

建议44:理解模块pickle的优劣

优势:接口简单、各平台通用、支持的数据类型广泛、扩展性强

劣势:不保证数据操作的原子性、存在安全问题、不同语言之间不兼容

建议45:序列化的另一个选择JSON模块:load和dump操作

建议46:使用traceback获取栈信息

建议47:使用logging记录日志信息

建议48:使用threading模块编写多线程程序

建议49:使用Queue模块使多线程编程更安全

5:设计模式

建议50:利用模块实现单例模式

 

建议51:用mixin模式让程序更加灵活

建议52:用发布-订阅模式实现松耦合

建议53:用状态模式美化代码

 

6:内部机制

建议54:理解build-in对象

建议55:__init__()不是构造方法,理解__new__()与它之间的区别

 

建议56:理解变量的查找机制,即作用域

局部作用域

全局作用域

嵌套作用域

内置作用域

建议57:为什么需要self参数

建议58:理解MRO(方法解析顺序)与多继承

 

建议59:理解描述符机制

建议60:区别__getattr__()与__getattribute__()方法之间的区别

 

建议61:使用更安全的property

建议62:掌握元类metaclass

建议63:熟悉Python对象协议

Python:_第2张图片

 

建议64:利用操作符重载实现中缀语法

 

建议65:熟悉Python的迭代器协议

建议66:熟悉Python的生成器

建议67:基于生成器的协程和 greenlet ,理解协程、多线程、多进程之间的区别

 

建议68:理解GIL的局限性

建议69:对象的管理和垃圾回收

7:使用工具辅助项目开发

建议70:从PyPI安装第三方包

建议71:使用pip和yolk安装、管理包

建议72:做paster创建包

建议73:理解单元测试的概念

建议74:为包编写单元测试

建议75:利用测试驱动开发(TDD)提高代码的可测性

建议76:使用Pylint检查代码风格

代码风格审查

代码错误检查

发现重复以及不合理的代码,方便重构

高度的可配置化和可定制化

支持各种IDE和编辑器的集成

能够基于Python代码生成UML图

能够与Jenkins等持续集成工具相结合,支持自动代码审查

建议77:进行高效的代码审查

建议78:将包发布到PyPI

8:性能剖析与优化

建议79:了解代码优化的基本原则

建议80:借助性能优化工具

建议81:利用cProfile定位性能瓶颈

建议82:使用memory_profiler和objgraph剖析内存使用

建议83:努力降低算法复杂度

建议84:掌握循环优化的基本技巧

减少循环内部的计算

将显式循环改为隐式循环,当然这会牺牲代码的可读性

在循环中尽量引用局部变量

关注内层嵌套循环

建议85:使用生成器提高效率

建议86:使用不同的数据结构优化性能

建议87:充分利用set的优势

建议88:使用multiprocessing模块克服GIL缺陷

建议89:使用线程池提高效率

建议90:使用C/C++模块扩展提高性能

建议91:使用Cythonb编写扩展模块

访问 flyai.club ,一键创建你的人工智能项目。

End


 

《Python 面向对象编程指南》

 

Python 面向对象编程指南(高清版) PDF

 

百度网盘

链接: https://pan.baidu.com/s/1SbD4gum4yGcUruH9icTPCQ

提取码: fzk5

 

内置方法     说明
 __init__(self,...)     初始化对象,在创建新对象时调用
 __del__(self)     释放对象,在对象被删除之前调用
 __new__(cls,*args,**kwd)     实例的生成操作
 __str__(self)     在使用print语句时被调用
 __getitem__(self,key)     获取序列的索引key对应的值,等价于seq[key]
 __len__(self)     在调用内联函数len()时被调用
 __cmp__(stc,dst)     比较两个对象src和dst
 __getattr__(s,name)     获取属性的值
 __setattr__(s,name,value)     设置属性的值
 __delattr__(s,name)     删除name属性
 __getattribute__()     __getattribute__()功能与__getattr__()类似
 __gt__(self,other)     判断self对象是否大于other对象
 __lt__(slef,other)     判断self对象是否小于other对象
 __ge__(slef,other)     判断self对象是否大于或者等于other对象
 __le__(slef,other)     判断self对象是否小于或者等于other对象
 __eq__(slef,other)     判断self对象是否等于other对象
 __call__(self,*args)     把实例对象作为函数调用

 

https://blog.csdn.net/huangyimo/article/details/50561841

 


 

 

 

  

 

 


 

《Python 高性能编程》

python高性能编程第一章读书笔记

计算机底层组件分为三大基本部分:计算单元、存储单元以及两者之间的连接。

计算单元:具有将接收到的任意输入转换成输出的能力以及改变当前处理状态的能力。CPU是最常见的计算单元。它的主要属性是其每个周期能进行的操作数量以及每秒能完成多少个周期。第一个属性通过每周期完成的指令数(IPC)来衡量。第二个属性通过其时钟速度来衡量。时钟速度的提高,可以使得每秒进行更多的计算,提高该计算单元所有程序的运行速度。IPC的提高则在矢量计算能力上有相当程度的影响。矢量计算指的是一次提供多个数据给一个CPU并能同时被操作。这种类型的CPU指令被称为SIMD(单指令多数据)。

         由于时钟速度和IPC提升陷入停滞,开始 依靠超线程技术,乱序执行和多核架构来提高速度 。超线程技术为主机的操作系统(OS)虚拟了第二个CPU,硬件逻辑则试图将两个指令线程交错地插入单个CPU的执行单元。乱序执行允许编译器检测出一个线性程序中某部分可以不依赖于之前的工作,也就是说两个工作能够以各种顺序执行或同时执行。使得当一些指令被阻塞(比如等待一次内存访问),另一些指令得以执行。多核架构指的是给CPU增加更多的核心。但是不一定多核就会更快,阿姆达尔定律认为:如果一个可以运行在多核上的程序有某些执行路径必须运行在单核上,那么这些路径就会成为瓶颈导致最终速度无法加快。

         对于python来说,python的全局解释器锁(GIL),确保Python进程一次只能执行一条指令,无论当前有多少个核心。使得无法使用多核,但是我们可以 使用标准库的multiprocessing,或numexpr、Cython技术,或分布式计算模型来避免

         存储单元,用于保存比特。比如主板上的寄存器、RAM以及硬盘。所有这些不同类型的存储单元主要区别在于读写数据的速度。且速度与读写方式有关,如顺序读取要比随机读取快得多。此外还有延时来表示设备查找数据所花费的时间。

         比如说判断一个数是否为质数,首先将number的值保存在RAM中。为了计算sqrt_number和number_float,将该值传入CPU中,理想情况下只需要传一次,它将被保存在CPU的L1/L2缓存中,然后CPU进行两次计算并将结果传回RAM保存。在循环部分,我们 更希望一次就将 number——float和 多个i的值传入CPU进行检查

         Python虚拟机抽象层的影响之一就是矢量操作变得不是直接可用 。而numpy这样的外部库可以 通过增加矢量化数学操作 来帮助我们解决这个问题。

         Python抽象还影响了任何需要为下一次计算保存L1/L2缓存中相关数据的优化。首先是python对象不再是内存中最优化的布局。这是因为python是一种垃圾收集语言----内存会被自动分配并在需要时释放。这会导致内存碎片并影响CPU缓存的传输。

         Python的优势在于可以轻易调用其他系统, 正确运用库可以使python代码在速度上和c媲美。

 

 

Python 高性能编程第4章 字典和集合

对次序未知的列表/元组的最优查询时间O(logn),字典和集合 基于键的查询 则可以带给我们O(1)的查询时间。除此之外,和列表/元组一样,字典和集合的插入时间O(1)。为了达到O(1)的查询时间,在底层使用的数据结构是一个开放地址散列表。然而,使用字典和集合有其代价,首先通常会占用更多的内存。同时虽然插入/查询的复杂度是O(1),但实际的速度极大取决于使用的散列函数。如果散列函数的运行速度较慢,那么在字典和集合上进行的任何操作也会相应变慢。

集合保证了它包含的键的唯一性,如果尝试添加一个已有的项,该项不会被添加进集合,这一操作的代价是O(1),而列表进行相应操作的代价是O(n)

对于散列表,新插入数据的位置取决于数据的两个属性:键的散列值以及该值如何跟其他对象比较。这是因为当我们插入数据时,首先需要计算键的散列值并掩码来得到一个有效的数据索引。掩码是为了保证一个可能是任意数字的散列值最终能落入分配的桶中。所以,如果我们分配了8个块的内存,而我们的散列值是28957,那么它将落入的桶的索引是28957&0b111 = 7,如果我们的字典增长到了需要512块内存,那么掩码就变成了0b11111111(此时我们会使用28957&0b11111111的桶)。现在我们检查这个桶是否已经被使用,如果是空桶,那么可以将键和值插入这一内存块。我们保存键是为了在获取时确保获得的是正确的值。如果桶已经被使用,且桶内的值和我们希望插入的值相等,说明这一键值对已经被保存,则直接返回。如果值不相等,则找一个新的位置来保存位置。

为了找到新的索引,我们用一个 简单的线性函数计算出一个新的索引,这一方法称为嗅探 Pyhon的嗅探机制使用了原始散列值的高位比特 (对于之前那个长度为8的散列表,由于使用的掩码mask=0b111=bin(8-1),我们只用了最后3个bit作为初始索引)。使用这些 高位比特使得每一个散列值生成的下一可用散列序列都是不同的 ,这样就能帮助防止未来的碰撞。

当我们在查询某个键时也有一个类似的过程:给出的键会被转化为一个索引进行检索。如果和该索引指向的位置中的键符合(在插入操作时我们会保存原始的键),那么我们就会返回那个值。如果不符合,我们用同一方案继续创建新的索引,直到我们找到数据或找到一个空桶。如果我们找到一个空桶,我们就可以认为表里不存在该数据。

当越来越多的项目被插入列表时,表本身必须改变大小来适应。研究显示 一个不超过三分之二满的表在具有最佳空间节约的同时,依然具有不错的散列碰撞避免率 。因此,当一个表到达关键点时,它就会增长。为了做到这一点,需要分配一个更大的表(也就是在内存中预留更多的桶),将掩码调整为适合新的表,旧表中的所有元素被重新插入新表。这需要重新计算索引,因为改变后的掩码会改变索引计算结果。结果就是, 改大散列表的代价非常大! 不过因为我们只在表太小时而不是在每一次操作时进行这一操作,分摊后每一次插入的代价依然是O(1)。值得注意的是,当一个散列表变大或变小时都可能发生改变大小。也就是说,如果散列表中足够多的元素被删除,表可能会被改小,但是 改变大小仅发生在插入时

每当Python访问一个变量、函数或模块时,都有一个体系来决定它去哪里查找这些对象。首先Python查找 locals()数组 ,其内保存了所有本地变量的条目。这是整条链上唯一一个不需要字典查询的部分。如果它不再本地变量里,那么会搜索 globals()字典 。最后如果对象也不在那里,则会搜索 __builtin__对象 。要注意locals()和globals()是显式的字典而__builtin__则是模块对象,在搜索__builtin__中的一个属性时,我们其实在搜索它的locals(字典)(对所有的模块对象和类对象都是如此)。


 

《Python高性能编程》笔记

2019-07-23

| In Python

性能分析

  1. 基本技术如 IPython 的 timeit 魔法函数、time.time()、以及一个计时修饰器,使用这些技术来了解语句和函数的行为。
  2. 内置工具如 cProfile,了解代码中哪些函数耗时最长,并用 runsnake 进行可视化。
  3. line_profiler 工具,对选定的函数进行逐行分析,其结果包含每行被调用的次数以及每行花费的时间百分比。
  4. memory_profiler 工具,以图的形式展示RAM的使用情况随时间的变化,解释为什么某个函数占用了比预期更多的 RAM。
  5. Guppy 项目的 heapy 工具,查看 Python 堆中对象的数量以及每个对象的大小,这 对于消灭奇怪的内存泄漏特别有用
  6. dowser 工具,通过Web浏览器界面审查一个持续运行的进程中的实时对象。
  7. dis 模块,查看 CPython 的字节码,了解基于栈的 Python 虚拟机如何运行。
  8. 单元测试,在性能分析时要避免由优化手段带来的破坏性后果。(参见原文,实用)

数据结构影响

  1. 列表和元组就类似于其它编程语言的数组,主要用于存储具有内在次序的数据;

    1. 高效搜索必需的两大要素是排序算法和搜索算法。Python 列表有一个内建的排序算法使用了Tim排序。
    2. 动态数组支持 resize 操作,可以增加数组的容量。当一个大小为N的列表第一次需要添加数据时,Python会创建一个新的列表,足够存放原来的N个元素以及额外需要添加的元素。
    3. 元组固定且不可变。这意味着一旦元组被创建,和列表不同,它的内容无法被修改或它的大小也无法被改变。(

      元组缓存于Python运行时环境,这意味着我们每次使用元组时无须访问内核去分配内存。)

    4. bi-sect 模块:提供了一个简便的函数让你可以在保持排序的同时往列表中添加元素,以及一个高度优化过的二分搜索算法函数来查找元素。

字典和集合就类似其它编程语言的哈希表/散列集,主要用于存储无序的数据。

    1. 新插入数据的位置取决于数据的两个属性:键的散列值以及该值如何跟其他对象比较。这是因为当我们插入数据时,首先需要计算键的散列值并掩码来得到一个有效的数组索引。
    2. 如果被占用,那么要找到新的索引,我们用一个简单的线性函数计算出一个新的索引,这一方法称为嗅探。Python的嗅探机制使用了原始散列值的高位比特。使用这些高位比特使得每一个散列值生成的下一可用散列序列都是不同的,这样就能帮助防止未来的碰撞。
    3. 当一个值从散列表中被删除时,我们不能简单地写一个NULL到内存的那个桶里。这是因为我们已经用NULL来作为嗅探散列碰撞的终止值。所以,我们必须写一个特殊的值来表示该桶虽空,但其后可能还有别的因散列碰撞而插入的值。
    4. 不超过三分之二满的表在具有最佳空间节约的同时依然具有不错的散列碰撞避免率 改大散列表的代价非常昂贵 ,但因为我们只在表太小时而不是在每一次插入时进行这一操作。
    5. Python 对象通常以散列表实现,因为它们已经有 内建的 hash cmp 函数
    6. 每当 Python 访问一个变量、函数或模块时,都有一个体系来决定它去哪里查找这些对象。
      1. locals()数组
      2. globals()字典
      3. __builtin__对象( builtin 中的一个 属性时,我们其实是在搜索它的 locals()字典)

迭代器、生成器

  1. 节约内存
  2. 延迟估值

矩阵与矢量计算

原生 Python 并不支持矢量操作,因为 Python 列表存储的不是实际的数据,而是对实际数据的引用 且Python 字节码并没有针对矢量操作进行优化,所以for 循环无法预测何时使用矢量操作能带来好处 在矢量和矩阵操作时,这种存储结构会造成极大的性能下降

 

6.3 内存碎片:

 

数据被分成小片,你只能对每一片分别进行传输,而不是一次性传输整个块。这意 味着你引入了更多的内存传输开销,且强制 CPU 在数据传输的过程中等待。我们 可以用 perf 看到在缓存失效的情况下这个问题会有多严重。

这个在正确的时候将正确的数据传输给 CPU 的问题被称为“ 冯诺伊曼瓶颈 ”。意思 是 现代计算机所使用的层次化的内存架构会导致 CPU 和内存之间的带宽受到限 制 。如果我们数据传输的速度可以无限快,我们就不需要任何缓存,因为 CPU 可 以立即获得任何它需要的数据。此时瓶颈就不再存在。

由于数据传输的速度不可能无限快,我们必须从 RAM 中预取数据并将其保存在 一个更小但更快的 CPU 缓存中,并希望当 CPU 需要某个数据时,它可以从中更 快读取到。虽然这已经是一个严重理想化了的场景,我们依然可以看到其中的一 些问题— 我们如何知道未来需要哪些数据?CPU 内部的 分支预测和流水线技 术会试图在处理当前指令的同时预测其下一条指令并将相应的内存读进缓存 。但 是 减少瓶颈最好的方法是让代码知道如何分配我们的内存以及如何使用我们的 数据进行计算

探测内存移动至 CPU 的性能相当困难 ,不过, Linux 上的 perf 工具可以让我们 洞察 CPU 如何处理运行中的程序 。比如,我们可以对例 6-6 的纯 Python 代码运 行 perf 并看到 CPU 运行我们代码的效率。结果见例 6-8。注意该例以及之后的perf 例子的输出都被截取以适应页面边界。被删除的数据包括各测量值的方差, 用于表示测量值在几次测量中发生了多大的变化。这有助于看到测量值在多大程 度上依赖于实际的程序特性以及多大程度上受到来自系统的其他干扰,比如其他 正在使用系统资源的程序。

 
   

理解 Perf:

 

让我们花一秒钟来理解 perf 告诉我们的各种性能指标以及它们跟我们代码的关 系。task-clock 指标告诉我们的 任务花了多少个时钟周期 。这跟总体的运行时 间不同,因为如果我们的程序花了一秒钟来运行但是使用了两个 CPU,那么task-clock 将是 1000(task-clock 的单位是毫秒)。方便的是,perf 会帮我 们计算并在该指标旁边告诉我们有多少个 CPU 被使用了。这个数字不完全等于 1是因为进程有一段时间依赖于其他子系统的指令(比如分配内存时)。

context-switches 和 CPU-migrations 告诉我们 程序在等待内核操作 (如 I/O操作)完成时,为了让其他进程得以运行, 被挂起或迁移到另一个 CPU 核心上执 行的次数 。当一个 context-switch 发生时程序的执行会被挂起,让另一个程序 得以执行。这是一个对时间要求非常精细的任务,也是我们需要尽量避免的,但是 我们对它的发生无能为力。只要进程允许切换,内核就会接手;不过,我们可以做 一些事来抑制内核切换我们的程序。 总的来说,内核会在程序进行 I/O(比如读取 内存、磁盘或网络)时将其挂起 。我们在后续章节会看到,我们 可以用异步操作来 确保我们的程序在等待 I/O 时继续使用 CPU,这会让我们的进程继续运行而不被切 换出去 。另外,我们还可以 设置程序的 nice 值来给我们的程序更高的优先级以防 止内核将它切换出去 。类似的, CPU-migrations 会发生在进程被挂起并迁移到 另一个 CPU 上继续执行的情况,这是为了让所有的 CPU 都有同样程度的利用率 。 这可以被认为是一个特别糟糕的进程切换,因为我们的程序不仅被暂时挂起,而且 丢失了 L1 缓存内所有的数据(每个 CPU 都有它自己的 L1 缓存)。

page-fault 是现代 UNIX 内存分配机制的一部分。分配内存时,内核除了告诉 程序一个内存的引用地址以外没做任何事。但是,之后在这块内存第一次被使用时, 操作系统会抛出一个缺页小中断,这将暂停程序的运行并正确分配内存。这被称为 延迟分配系统 。虽然这种手段相比以前的内存分配系统是一个很大的优化,缺页小 中断本身依然是一个相当昂贵的操作,因为大多数操作都发生在你的程序外部。另 外还有一种缺页大中断,发生于当你的程序需要从设备(磁盘、网络等)上请求还 未被读取的数据时。这些操作更加昂贵,因为他们不仅中断了你的程序,还需要读 取数据所在的设备。这种缺页不总是影响 CPU 密集的工作,但是,它会给任何需 要读写磁盘或网络的程序带来痛苦。

 


Numpy 能够将 数据连续存储 在内存中并支持数据的矢量操作,在数据处理方面,它是高性能编程的最佳解决方案之一。
Numpy 带来性能提升的关键在于,它使用了高度优化且特殊构建的对象,取代了通用的列表结构来处理数组,由此 减少了内存碎片 ;此外, 自动矢量化 的数学操作使得矩阵计算非常高效。
Numpy 在矢量操作上的 缺陷是一次只能处理一个操作 。例如,当我们做 A B + C 这样的矢量操作时,先要等待 A B 操作完成,并保存数据在一个临时矢量中,然后再将这个新的矢量和 C 相加。

编译器

让你的代码运行更快的最简单的办法就是让它做更少的工作。编译器把代码编译成机器码,是提高性能的关键组成部分。

  • Cython ——这是编译成C最通用的工具,覆盖了Numpy和普通的Python代码(需要一些C语言的知识)。
  • Shed Skin —— 一个用于非Numpy代码的,自动把Python转换成C的转换器。
  • Numba —— 一个专用于Numpy代码的新编译器。
  • Pythran —— 一个用于Numpy和非numpy代码的新编译器。
  • PyPy —— 一个用于非Numpy代码的,取代常规Python可执行程序的稳定的即时编译器。
   

7.6.1 使用Cython编译纯Python版本开始写一个扩展编译模块的简单方法涉及 3 个文件。使用我们的 Julia 作为例子,

它们是:

  • 调用它的 Python 代码(来自前面的 Julia 代码块)。

  • 在一个新.pyx 文件中要被编译的函数。

  • 一个 setup.py,包含了调用 Cython 来制作扩展模块的指令。

    使用这个方法,setup.py 脚本调用 Cython 把.pyx 文件编译成一个编译模块。在类UNIX 系统上,编译模块可能会是一个.so 文件;在 Windows 上应该是一个.pyd(类DLL 的 Python 库)。

    对于 Julia 的例子,我们会用:

  • julia1.py,构建输入列表并调用计算函数。

  • cythonfn.pyx,包含了我们能注解的 CPU 密集型函数。

  • setup.py,包含了构建指令。

 
   

如果你的 CPU 密集型代码在频繁解引用的循环中,尝试禁止边界检 查和外围检查。

   

一般情况下,可能最消耗 CPU 时间的代码行是下面这些:• 在紧凑的内循环内。
• 解引用 list、array 或者 np.array 这些项。
• 执行数学运算。

 
cdef 关键字  
   

我们在图 7-4 中可以看到 while 循环还是相当耗时的(黄色部分)。耗时的调用在 于 Python 对复数 z 的  abs 函数 中。Cython 没有对复数提供原生的 abs 函数。作 为替代,我们可以提供自己的本地扩展。

 

这个改变有巨大的效果——通过减少在最内循环中 Python 调用的次数,我们大大 降低了函数的运算时间。这个新版本只用 0.25 秒就执行完毕了,具有超过原版本40 倍的速度提升,令人惊叹。

 

 

 

 

 

密集型任务

  1. I/O 密集型:异步编程 
    • Gevent
    • Tornado
    • Asyncio
  2. CPU 密集型:多核 CPU 进行多进程
    • Multiprocessing
      • multiprocessing.Pool
      • 内存共享
        • multiprocessing.Manager()
        • redis等中间件
        • mmap

集群与现场教训

  1. 集群带来的问题:
    1. 机器间信息同步的延迟
    2. 机器间配置与性能的差异
    3. 机器的损耗与维护
    4. 其它难以预料的问题
  2. 集群化解决方案:
    1. Parallel Python
    2. IPython Parallel 
    3. NSQ

 


 

Python猫荐书系列之五:Python高性能编程

Python猫

稍微关心编程语言的使用趋势的人都知道,最近几年,国内最火的两种语言非 Python 与 Go 莫属,于是,隔三差五就会有人问:这两种语言谁更厉害/好找工作/高工资……

对于编程语言的争论,就是猿界的生理周期,每个月都要闹上一回。到了年末,各类榜单也是特别抓人眼球,闹得更凶。

其实,它们各有对方所无法比拟的优势以及用武之地,很多争论都是没有必要的。身为一个正在努力学习 Python 的(准)中年程序员,我觉得吧,先把一门语言精进了再说。没有差劲的语言,只有差劲的程序员,等真的把语言学好了,必定是“山重水复疑无路,柳暗花明又一村”。

铺垫已了,进入今天的正题,Python 猫荐书系列之五——

Python高性能编程

 

本书适合已入门 Python、还想要进阶和提高的读者阅读。

所有计算机语言说到底都是在硬件层面的数据操作,所以高性能编程的一个终极目标可以说是“高性能硬件编程”。然而,Python 是一门高度抽象的计算机语言,它的一大优势是开发团队的高效,不可否认地存在这样或那样的设计缺陷,以及由于开发者的水平而造成的人为的性能缺陷。

本书的一大目的就是通过介绍各种模块和原理,来促成在快速开发 Python 的同时避免很多性能局限,既减低开发及维护成本,又收获系统的高效。

1、性能分析是基础

首先的一个关键就是性能分析,借此可以找到性能的瓶颈,使得性能调优做到事半功倍。

性能调优能够让你的代码能够跑得“足够快”以及“足够瘦”。性能分析能够让你用最小的代价做出最实用的决定。

书中介绍了几种性能分析的工具:

(1)基本技术如 IPython 的 %timeit 魔法函数、time.time()、以及一个计时修饰器,使用这些技术来了解语句和函数的行为。

(2)内置工具如 cProfile,了解代码中哪些函数耗时最长,并用 runsnake 进行可视化。

(3)line_profiler 工具,对选定的函数进行逐行分析,其结果包含每行被调用的次数以及每行花费的时间百分比。

(4)memory_profiler 工具,以图的形式展示RAM的使用情况随时间的变化,解释为什么某个函数占用了比预期更多的 RAM。

(5)Guppy 项目的 heapy 工具,查看 Python 堆中对象的数量以及每个对象的大小,这对于消灭奇怪的内存泄漏特别有用。

(6)dowser 工具,通过Web浏览器界面审查一个持续运行的进程中的实时对象。

(7)dis 模块,查看 CPython 的字节码,了解基于栈的 Python 虚拟机如何运行。

(8)单元测试,在性能分析时要避免由优化手段带来的破坏性后果。

作者强调了性能分析的重要性,同时也对如何确保性能分析的成功提了醒,例如, 将测试代码与主体代码分离、避免硬件条件的干扰(如在BIOS上禁用了TurboBoost、禁用了操作系统改写SpeedStep、只使用主电源等) 运行实验时禁用后台工具如备份和Dropbox、多次实验、重启并重跑实验来二次验证结果 ,等等。

性能分析对于高性能编程的作用,就好比复杂度分析对于算法的作用,它本身不是高性能编程的一部分,但却是最终有效的一种评判标准。

2、数据结构的影响

高性能编程最重要的事情是了解数据结构所能提供的性能保证。

高性能编程的很大一部分是了解你查询数据的方式,并选择一个能够迅速响应这个查询的数据结构。

书中主要分析了 4 种数据结构:列表和元组就类似于其它编程语言的数组,主要用于存储具有内在次序的数据;而字典和集合就类似其它编程语言的哈希表/散列集,主要用于存储无序的数据。

本书在介绍相关内容的时候很克制,所介绍的都是些影响“速度更快、开销更低”的内容,例如:内置的 Tim 排序算法、列表的 resize 操作带来的超额分配的开销、元组的内存滞留(intern机制)带来的资源优化、散列函数与嗅探函数的工作原理、散列碰撞带来的麻烦与应对、Python 命名空间的管理,等等。

 

散列碰撞的结果

理解了这些内容,就能更加了解在什么情况下使用什么数据结构,以及如何优化这些数据结构的性能。

另外,关于这 4 种数据结构,书中还得出了一些有趣的结论:对于一个拥有100 000 000个元素的大列表,实际分配的可能是112 500 007个元素;初始化一个列表比初始化一个元组慢5.1 倍;字典或集合默认的最小长度是8(也就是说,即使你只保存3个值,Python仍然会分配 8 个元素)、对于有限大小的字典不存在一个最佳的散列函数。

3、矩阵和矢量计算

矢量计算是计算机工作原理不可或缺的部分,也是在芯片层次上对程序进行加速所必须了解的部分。

然而,原生 Python 并不支持矢量操作,因为 Python 列表存储的不是实际的数据,而是对实际数据的引用。在矢量和矩阵操作时,这种存储结构会造成极大的性能下降。比如, grid[5][2]  中的两个数字其实是索引值,程序需要根据索引值进行两次查找,才能获得实际的数据。

同时,因为数据被分片存储,我们只能分别对每一片进行传输,而不是一次性传输整个块,因此,内存传输的开销也很大。

减少瓶颈最好的方法是让代码知道如何分配我们的内存以及如何使用我们的数据进行计算。

Numpy 能够将数据连续存储在内存中并支持数据的矢量操作,在数据处理方面,它是高性能编程的最佳解决方案之一。

Numpy 带来性能提升的关键在于,它使用了高度优化且特殊构建的对象,取代通用的列表结构来处理数组,由此减 少了内存碎片 ;此外, 自动矢量化的数学操作使得矩阵计算非常高效

Numpy 在矢量操作上的缺陷是一次只能处理一个操作。例如,当我们做 A * B + C 这样的矢量操作时,先要等待 A * B 操作完成,并保存数据在一个临时矢量中,然后再将这个新的矢量和 C 相加。

Python:_第3张图片

 

Numexpr 模块可以将矢量表达式编译成非常高效的代码,可以 将缓存失效以及临时变量的数量最小化 。另外,它还能利用多核 CPU 以及 Intel 芯片专用的指令集来将速度最大化。()

书中尝试了多种优化方法的组合,通过详细的分析,展示了高性能编程所能带来的提升效果。

What is NumExpr?

NumExpr is a fast numerical expression evaluator for NumPy. With it, expressions that operate on arrays (like  '3*a+4*b' ) are accelerated and use less memory than doing the same calculation in Python.

In addition, its multi-threaded capabilities can make use of all your cores – which generally results in substantial performance scaling compared to NumPy.

Last but not least, numexpr can make use of Intel’s VML (Vector Math Library, normally integrated in its Math Kernel Library, or MKL). This allows further acceleration of transcendent expressions.

How NumExpr achieves high performance

The main reason why NumExpr achieves better performance than NumPy is that it avoids allocating memory for intermediate results. This results in better cache utilization and reduces memory access in general. Due to this, NumExpr works best with large arrays.

NumExpr parses expressions into its own op-codes that are then used by an integrated computing virtual machine. The array operands are split into small chunks that easily fit in the cache of the CPU and passed to the virtual machine. The virtual machine then applies the operations on each chunk. It’s worth noting that all temporaries and constants in the expression are also chunked. Chunks are distributed among the available cores of the CPU, resulting in highly parallelized code execution.

The result is that NumExpr can get the most of your machine computing capabilities for array-wise computations. Common speed-ups with regard to NumPy are usually between 0.95x (for very simple expressions like  'a + 1' ) and 4x (for relatively complex ones like  'a*b-4.1*a > 2.5*b' ), although much higher speed-ups can be achieved for some functions and complex math operations (up to 15x in some cases).

NumExpr performs best on matrices that are too large to fit in L1 CPU cache. In order to get a better idea on the different speed-ups that can be achieved on your platform, run the provided benchmarks.

 
   
   

4、编译器

书中提出一个观点: 让你的代码运行更快的最简单的办法就是让它做更少的工作。

编译器把代码编译成机器码,是提高性能的关键组成部分。

Python:_第4张图片

 

不同的编译器有什么优势呢,它们对于性能提升会带来多少好处呢?书中主要介绍了如下编译工具:

  • Cython ——这是编译成C最通用的工具,覆盖了Numpy和普通的Python代码(需要一些C语言的知识)。
  • Shed Skin —— 一个用于非Numpy代码的,自动把Python转换成C的转换器。
  • Numba —— 一个专用于Numpy的新编译器。
  • Pythran —— 一个用于Numpy和非numpy代码的新编译器。
  • PyPy —— 一个用于非Numpy代码的,取代常规Python可执行程序的稳定的即时编译器。

书中分析了这几种编译器的工作原理、优化范围、以及适用场景等,是不错的入门介绍。此外,作者还提到了其它的编译工具,如Theano、Parakeet、PyViennaCL、ViennaCL、Nuitka 与 Pyston 等,它们各有取舍,在不同领域提供了支撑之力。

5、密集型任务

高性能编程的一个改进方向是提高密集型任务的处理效率,而这样的任务无非两大类:I/O 密集型与 CPU 密集型。

I/O 密集型任务主要是磁盘读写与网络通信任务,占用较多 I/O 时间,而对 CPU 要求较少;CPU 密集型任务恰恰相反,它们要消耗较多的 CPU 时间,进行大量的复杂的计算,例如计算圆周率与解析视频等。

改善 I/O 密集型任务的技术是 异步编程  ,它使得程序在 I/O 阻塞时,并发执行其它任务,并 通过“事件循环”机制 来管理各项任务的运行时机,从而提升程序的执行效率。

书中介绍了 三种异步编程的库:Gevent、Tornado 和 Asyncio ,对三种模块的区别做了较多分析。

改善 CPU 密集型任务的主要方法是 利用多核 CPU 进行多进程 的运算。

Multiprocessing 模块 基于进程和基于线程 的并行处理, 在队列上共享任务,以及在进程间共享数据 ,是处理CPU密集型任务的重要技术。

书中没有隐瞒它的局限性:Amdahl 定律揭示的优化限度、适应于单机多核而多机则有其它选择、全局解释锁 GIL 的束缚、以及进程间通信(同步数据和检查共享数据)的开销。针对进程间通信问题,书中还分析了多种解决方案,例如 Less Naïve Pool、Manager、 Redis 、RawValue、MMap 等。

6、集群与现场教训

集群是一种多服务器运行相同任务的结构,也就是说,集群中的各节点提供相同的服务,其优点是系统扩展容易、具备容灾恢复能力。

集群需要克服的挑战有:机器间信息同步的延迟、机器间配置与性能的差异、机器的损耗与维护、其它难以预料的问题。书中列举了两个惨痛的教训:华尔街公司骑士资本由于软件升级引入的错误,损失4.62亿美元;Skype 公司 24 小时全球中断的严重事故。

书中给我们重点介绍了 三个集群化解决方案:Parallel Python、IPython Parallel 和 NSQ。引申也介绍了一些普遍使用的方案,如 Celery、Gearman、PyRes、SQS。

Python:_第5张图片

 

关于现场教训,它们不仅仅是一些事故或者故事而已,由成功的公司所总结出来的经验更是来之不易的智慧。书中单独用一章内容分享了六篇文章,这些文章出自几个使用 Python 的公司/大型组织,像是Adaptive Lab、RadimRehurek、Smesh、PyPy 与 Lanyrd ,这些国外组织的一线实践经验,应该也能给国内的 Python 社区带来一些启示。

7、写在最后

众所周知,Python 应用前景大、简单易学、方便开发与部署,然而与其它编程语言相比,它的性能几乎总是落于下风。如何解决这个难题呢?本期荐书的书目就是一种回应。

《Python高性能编程》全书从微观到宏观对高性能编程的方方面面做了讲解,主要包含以下主题:计算机内部结构的背景知识、列表和元组、字典和集合、迭代器和生成器、矩阵和矢量计算、 编译器、并发、集群和工作队列 等。这些内容为编写更快的 Python 指明了答案。

本篇文章主要以梳理书中的内容要点为主,平均而兼顾地理清了全书脉络(PS:太面面俱到了,但愿不被指责为一篇流水账的读书笔记才好……)。我认为,鉴于书中谈及的这些话题,它就足以成为我们荐书栏目的一员了。除去某些句段的糟糕翻译、成书时间比较早(2014年)而造成的过时外,这本书总体质量不错,可称为是一份优秀的高性能编程的指引手册。

关于荐书栏目,我最后多说几句。本栏目原计划两周左右出一篇,但由于其它系列文章花费了我不少时间,而要写好一篇荐书/书评也特别费劲,最后生生造成了现在两月一更的尴尬局面……这篇文章是个错误的示范,我不该试图全面通读与概括其内容的。因此,我决定今后选一些易读的书目,在写作上也尽量走短小精悍风,希望能持续地将本栏目运作下去。若你有什么建议(如书目推荐、书评推荐、写作建议、甚至是投稿),我随时欢迎,先行致谢啦。


 

 

 

 


 

 


更多文章、技术交流、商务合作、联系博主

微信扫码或搜索:z360901061

微信扫一扫加我为好友

QQ号联系: 360901061

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请用微信扫描下面二维码支持博主2元、5元、10元、20元等您想捐的金额吧,狠狠点击下面给点支持吧,站长非常感激您!手机微信长按不能支付解决办法:请将微信支付二维码保存到相册,切换到微信,然后点击微信右上角扫一扫功能,选择支付二维码完成支付。

【本文对您有帮助就好】

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请用微信扫描上面二维码支持博主2元、5元、10元、自定义金额等您想捐的金额吧,站长会非常 感谢您的哦!!!

发表我的评论
最新评论 总共0条评论