标签: python

  • python 源码路径组织

    python源码路径组织:

    • Include: 包含了python提供的头文件,如果需要自己编写扩展python,需要用到这里的头文件;
    • Lib: 包含python自带的标准库,都是用python写的。
    • Modules: 包含所有C语言编写的模块,都是对速度要求比较高的模块。
    • Parser: 包含解释器的Scanner和Parser的部分,对python源代码进行词法分析和语法分析的部分;
    • Objects: 该目录包含了所有Pyhotn的内建对象,包括整数、list、dict等。同时,还包括了python在运行时需要的所有内部使用对象的实现。
    • Python: 包含解释器中Compiler和执行部分,是python运行核心所在。
  • Python高性能编程 读书笔记

    主题

    在当python的函数越短,通常意味着性能越高!

    这不是笑话,在不改变算法的情况下,同一个功能是一定存在一个最短指令集的,当python代码变短意味着指令码变少,那多的指令码就在底层以CPU指令码的形式转移了。

    这叫做同算法指令集数量守恒。 ——本人

    第二章 通过性能分析找到瓶颈

    python可用的分析工具:

    • cProfile:自带的运行分析工具,可以计算函数的执行次数和时间;
    • runsnakerun: 可视化cProfile的内容;
    • link_profile: 分析的更加细致,但是执行效率会变慢,要增加修饰符;
    • memory_profiler:诊断内存的用量
    • heapy:调查堆上的对象
    • dis模块:生成CPython字节码

    heapy模块

    • guppy.hpy() – 获得heapy对象,作为操作接口。
    • heap() – 获得堆中的可见对象(reachable and visible),这些对象并不包含guppy库使用和创建的对象。
    • setref() – 设置一个参考点,下一次调用heap()函数会计算这个参考点之后创建的。
    • heapu() – 获得不可见(循环引用)的对象的状态和列表,heapu()返回一个状态(guppy.heapy.Part.Stat)标明不可见对象。

    dis模块

    获得python的字节码

    def myfunc(alist):
        return len(alist)
    
    >>> dis.dis(myfunc)
      2           0 LOAD_GLOBAL              0 (len)
                  3 LOAD_FAST                0 (alist)
                  6 CALL_FUNCTION            1
                  9 RETURN_VALUE
    
    >>> import math
    >>> def myfunc(x):
    ...     return math.sin(x)
    ...
    >>> import dis
    >>> dis.dis(myfunc)
      2           0 LOAD_GLOBAL              0 (math)
                  2 LOAD_ATTR                1 (sin)
                  4 LOAD_FAST                0 (x)
                  6 CALL_FUNCTION            1
                  8 RETURN_VALUE
    >>> from math import sin
    >>> def myfunc(x):
    ...     return sin(x)
    ...
    >>> dis.dis(myfunc)
      2           0 LOAD_GLOBAL              0 (sin)
                  2 LOAD_FAST                0 (x)
                  4 CALL_FUNCTION            1
                  6 RETURN_VALUE

    默认值不能用可变类型

    >>> def B():
    ...   def A(x=[]):
    ...     x.append(2)
    ...     return x
    ...   return A
    ...
    >>> import dis
    >>> dis.dis(B)
      2           0 BUILD_LIST               0
                  2 BUILD_TUPLE              1
                  4 LOAD_CONST               1 (<code object A at 0x7febe7e5de40, file "<stdin>", line 2>)
                  6 LOAD_CONST               2 ('B.<locals>.A')
                  8 MAKE_FUNCTION            1
                 10 STORE_FAST               0 (A)
    
      5          12 LOAD_FAST                0 (A)
                 14 RETURN_VALUE
    
    
    构建了一个函数,是B.<locals>.A,并且list作为默认值一开始就存进去了
    >>> def X(x=[]):
    ...   return x
    ...
    >>> X.__defaults__
    ([],)

    第三章 列表和元祖

    python使用魔法函数eq和lt来进行对象的比较。

    bisect模块是二分查找模块

    通用代码会比某个特定问题设计的代码慢很多。

    blist是比默认的list在存在大量修改的list场景下实现更快的数据结构。

    第四章 字典和集合

    python在va == vb的判断内部调用的是eq方法。

    可以被散列(哈希)的对象是同时实现了hash的魔法函数以及eq或者cmp两者之一的类型。

    这里一定要实现eq或者cmp为了防止两个对象的hash相同,那样就需要判断两个对象是否相等,如果相等则是同一个,不然则是不同的。

    python对dict本质就是一个hash表,底层对hash冲突的处理方式是开放寻址法

    同时在python的dict到达容量的2/3时会发生扩容,扩容的复杂度是O(n)的,需要将所有的对象都重新hash一次。

    每当python访问一个变量、函数或模块时,首先会查找locals()数组,其内保存了所有本地变量的条目,如果不在本地变量里那么搜索globals()字典,最后如果对象也不在则搜索builtin对象。这里可能为了搜一个函数或者变量要搜好几个dict。

    第五章 迭代器和生成器

    代码每次运行到yield该函数就会“发射”出一个值,然后外部代码需求另一个值时该函数才会继续运行(上下文会保存)。

    使用python的内置函数iter来创建迭代器,iter函数会返回对象的iter属性:

    # python的循环
    for i in object:
        do_work(i)
    
    # 等价于
    object_iterator = iter(object)
    while 1:
        try:
            i = object_iterator.next()
            do_work(i)
        except StopIteration:
            break

    如果只是计算满足某个条件的列表数量,这样可以节省一笔内存

    # 不要写:
    [i for i in xrange(n) if i % 3 == 0]
    # 要用:
    sum((1 for i in xrange(n) if i % 3 == 0))
    
    ----------------------
    项目里一些代码的鞭尸:
    level_aver = float(sum([member.rank_level for member in self.member_dict.values()])) / self.size
    
    total_num = sum([abs(value) for value in pop_result.itervalues()])
    
    sum([mgr.busy_count for mgr in self.combat_mgr.itervalues()])
    
    # 这里每次+可能会增加一次内存分配和复制
    # 用list(chain()) 在1000*1000的对象上操作要快396倍
    task_list = reduce(lambda x, y: x + y, task_level_list)
    
    # 而且原场景其实不需要这个list,拼接了之后只是一次性使用直接返回迭代器即可

    python里一些表达式:

    • 列表表达式: [f(xx) for xx in rr if xx else f(xx)]
    • 生成器表达式: (f(xx) for xx in rr if xx else f(xx))
    • 字典表达式: { key_expr: value_expr for value in collection if condition }
    • 集合生成器: {f(xx) for xx in rr if xx else f(xx)}

    在这里记录一下。

    标准库中的itertools库提供了内建函数map, reduce, filter和zip的生成器版本(分别是imap, ireduce, ifilter和izip),以及其他很多有用的函数,特别是:

    • islice 允许对一个无穷生成器进行切片
    • chain 讲多个生成器链接到一起
    • takewhile 给生成器添加一个终止条件
    • cycle 通过不断重复将一个有限生成器变成无穷

    顺便记录一下上面几个函数的功能:

    • map(function, iterable, …): 将参数以函数执行的生成器
    • reduce(function, iterable[, initializer]): 跟map类似,不过f的参数是两个,先对迭代器中的第 1、2 个元素进行操作,得到的结果再与第三个数据用 function 函数运算,最后得到一个结果。
    • filter(function, iterable): 对迭代器的对象执行f来进行过滤,为True才会返回
    • zip([iterable, …]): 将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的列表。
    • any(iterable), 如果 iterable 的任一元素为真值则返回 True。
    • all(iterable), 如果 iterable 的所有元素都为真值则返回 True。

    第六章 矩阵和矢量计算

    原生python并不支持矢量操作,主要的核心原因是python的列结构存储的内容是指向对象的指针,其次python的字节码没有对矢量操作做优化。

    result = 0
    for i xrange(num_iterations):
       result += i * sin(num_iterations)
    return result
    
    # 下面这个快过上面那个,虽然是废话,但是很实用
    result = 0
    for i xrange(num_iterations):
       result += i
    return result * sin(num_iterations)
    
    # -------------------------------------
    # damage_system
    for ratio in reduce_ratio_list:
        damage *= ratio
    
    # 比上面要快16%
    damage = reduce(operator.mul, reduce_ratio_list, damage)
    
    # -------------------------------------

    书中用numpy去优化一段代码,带来了40倍的效率提升,但是神奇的事情是,禁用矢量指令集优化,性能依然有极大的提高,用perf分析发现矢量操作近带来了40倍中的15%,剩下的是内存问题导致的效率低下。

    perf是一个进程级别的分析工具。

    numexpr

    将python能够分析执行顺序,非常大程度的优化矩阵,可以自动地利用多CPU的优势,并且考虑到CPU缓存的问题,会特意移动数据让各级CPU缓存拥有正确的数据。

    如果numpy可以将速度快几倍,那使用numexpr可能可以将速度提升至几十上百倍。

    但是如果矩阵特别小,会有负优化的效果。

    第七章 编译成C

    Cython、Shed Skin和Pythran是纯粹的基于C的编译方式,凭借Numba的基于LLVM的编译方式,还有替代虚拟机的pypy,内置了一个即时编译器(JIT)。

    无论使用哪条路线,都要在适用性和团队效率上作出权衡,

    image

    优化是永无止尽的,但是对一个模块的优化是有限的。

    顺便一提,pyc只是py编译成的python机器码,并不包含优化。

    对python的优化

    Cython

    可以直接把python代码编译成so,可以先用Cython在服务端优化一轮,顺便提前把模块化做好。

    Shed Skin

    将python翻译成C++的编译器,会自动去猜测类型,可以直接被import,不像Cython需要显式指定类型。

    Shed Skin会将python的list拷贝成Shed Skin环境的变量,这个是会花时间的,

    numba

    llvm编译器的python脚本编译,只需要在需要编译的函数增加@jit()修饰符即可,执行速度非常快

    Pythran

    有点像Numba和Cython——我注解函数的参数,接着它接管进一步的类型注解和代码特化,它会利用OpenMP的并行化可能性。

    pypy

    底层换了虚拟机,提供了JIT,执行时编译,所以第一次会很慢,编译好之后再执行非常快!

    pypy和CPython的垃圾回收机制不同。

    任何纯python的库pypy都能直接安装运行,但是有C扩展库的就可能无法工作。

    内存消耗比cpython多。

    写C/C++程序的优化

    ctypes

    C++只写一些函数,然后通过ctypes来”import”进python,需要在python层将所有的调用对象都转化成ctypes对应的C类型,然后执行。

    这里在使用list或者dict的时候,会非常非常消耗。

    cffi

    相比ctypes方便很多,并且支持一些很便利的语法糖。

    CPython模块

    我们项目里目前是用的模块,会很繁琐,很简单的代码也要写几十行来兼容,但是好处是控制颗粒度非常高,从无视GIL到引用计数,一切都可以直接去修改,但是对程序的要求也会高一点,因为需要了解python的一些细节。

    在大部分情况下,书中的建议是不使用这种方式。

    第八章 并发

    时间循环有两种方式:回调或者future。

    回调会带来很长的调用链,future+asyncio是一个很好的,不过我们用python2是永远没机会用了。

    分别使用了gevent,tomado和asyncio

    第九章 multiprocessing模块

    可以让程序基于进程和线程的并行处理,multiprocessing.Pool的功能感觉很强!

    对于使用这个模块,需要解决的问题:

    • 把工作拆分成独立的工作单元
    • 考虑是否能随机化工作序列
    • 尽量让同时在处理的任务数量和CPU数量相同

    第十章 集群和工作队列

    一个容易调适的系统可能胜过一个更快的系统。

    • 启动组件:cron任务,Circus或supervisord
    • 测试容灾的组件:ChaosMonkey
    • 部署系统:Fabric、Salt、Chef或Puppet

    下面几个都是集群化的方案

    Parallel Python模块

    • 几乎没有依赖性,接口简单;
    • 不是很强大,缺少通信机制

    Ipython Parallel

    IPython作为shell做并行任务控制,依赖ZeroMQ。

    NSQ

    是一个可随时投入生产的队列系统,有持久性和扩展性。

    其他集群化的工具

    • Celery
    • Gearman
    • PyRes
    • 亚马逊的简单队列服务(SQS)

    第十一章 使用更少的RAM

    基础对象开销高:100000000项的list要消耗760MB的内存,可以替换成array,array是连续的内存块,而且array可以直接传递给C++层不需要额外的转换。

    python3对字符串的存储效率相比python2提高了很多。

    image

    高效地存储文本,trie树和有向无环图 (DAWG)(双trie树结构)单词图:

    通过允许概率误差来减少数据集需要的内存空间:

    • Morris计数器
    • 布隆过滤器
    • loglog计数器

    End.