《Mastering Python High Performance》阅读笔记

本文是《Mastering Python High Performance》的读书笔记。python

做者(出版社)开源的代码地址:c++

《Mastering Python High Performance》一书大体分为两部分,第一部分讲了profile的方法论,介绍了cProfileline_profile的使用。第二部分介绍了一些提升性能的方法。这篇文章只讲第二部分的第四章提到的一些方法。编程

我对本文中出现的一些例子作了必定修改。sass

0x01 Memoization

对于一些耗时、输入参数大体固定的函数,若是你可以保证必定的输出必定能够获得相同的结果,能够把结果保存起来,以后调用的时候就无需作重复计算。书中给出了一个修饰器:bash

class Memoized:
    def __init__(self, fn):
        self.fn = fn
        self.results = {}

    def __call__(self, *args, **kwargs):
        key = ''.join([str(arg) for arg in args] + ["{}:{}".format(k, v) for k, v in kwargs.items()])
        try:
            return self.results[key]
        except KeyError:
            self.results[key] = self.fn(*args)
        return self.results[key]
复制代码

利用这个能够更快速地计算斐波那契数列:微信

@Memoized
def fib(n):
    if n == 0: return 0
    if n == 1:
        return 1
    else:
        return fib(n - 1) + fib(n - 2)
复制代码

测试一下性能:计算fib(40)。使用了Memoized修饰器地时候花了0.0005秒,不用的时候个人垃圾 macbook air 跑了106.4099秒,可见性能差距之大。并发

0x02 Defult Arguments

第二个(优化过的)factor参数会在函数建立时就被进行计算。app

import math

def degree_sin(deg):
    return math.sin(deg * math.pi / 180.0) * math.cos(deg * math.pi / 180.0)

def degree_sin_opt(deg, factor=math.pi / 180.0, sin=math.sin, cos=math.cos):
    return sin(deg * factor) * cos(deg * factor)
复制代码

每一个函数执行1000次而后计算平均值,发现第二种地性能有了明显提高。缘由在于第二种参数只须要在函数第一次建立的时候计算一次异步

可是这种方法若是不作很好地文档说明的话,有可能会带来问题。除了可以代理显著的性能提高,不建议使用。async

0x03 List comprehension & Generators

用列表生成器比 for 循环性能要高。

# 类别生成器
multiples_of_two = [x for x in range(100) if x % 2 == 0]

# for 循环
multiples_of_two = []
for x in range(100):
    if x % 2 == 0:
        multiples_of_two.append(x)
复制代码

要搞清楚问什么,须要看看底层的 bytecodes,这里使用到了dis模块。同时,timeit.timeit() 方法默认执行 code 1000000次,你也能够经过 number指定运行次数。

import dis
import timeit

programs = dict(
    comprehension_code=""" [x for x in range(100) if x % 2 == 0] """,
    forloop_code=""" multiples_of_two = [] for x in range(100): if x % 2 == 0: multiples_of_two.append(x) """
)

for program, code in programs.items():
    dis.dis(code)
    print("{} spent {} seconds".format(program, timeit.timeit(stmt=code)))
    print()
复制代码

你不须要彻底看懂每一行 bytecode,简单来讲,list comprehension有更少的 bytecode,同时用了优化过的LIST_APPEND指令,而 for 循环中用的是LOAD_ATTR, LOAD_NAME, CALL_FUNCTION这三条指令。

2           0 LOAD_CONST               0 (<code object <listcomp> at 0x102e5f420, file "<dis>", line 2>)
              2 LOAD_CONST               1 ('<listcomp>')
              4 MAKE_FUNCTION            0
              6 LOAD_NAME                0 (range)
              8 LOAD_CONST               2 (100)
             10 CALL_FUNCTION            1
             12 GET_ITER
             14 CALL_FUNCTION            1
             16 RETURN_VALUE

Disassembly of <code object <listcomp> at 0x102e5f420, file "<dis>", line 2>:
  2           0 BUILD_LIST               0
              2 LOAD_FAST                0 (.0)
        >>    4 FOR_ITER                20 (to 26)
              6 STORE_FAST               1 (x)
              8 LOAD_FAST                1 (x)
             10 LOAD_CONST               0 (2)
             12 BINARY_MODULO
             14 LOAD_CONST               1 (0)
             16 COMPARE_OP               2 (==)
             18 POP_JUMP_IF_FALSE        4
             20 LOAD_FAST                1 (x)
             22 LIST_APPEND              2
             24 JUMP_ABSOLUTE            4
        >>   26 RETURN_VALUE
comprehension_code spent 10.200395356 seconds

  2           0 BUILD_LIST               0
              2 STORE_NAME               0 (multiples_of_two)

  3           4 SETUP_LOOP              38 (to 44)
              6 LOAD_NAME                1 (range)
              8 LOAD_CONST               0 (100)
             10 CALL_FUNCTION            1
             12 GET_ITER
        >>   14 FOR_ITER                26 (to 42)
             16 STORE_NAME               2 (x)

  4          18 LOAD_NAME                2 (x)
             20 LOAD_CONST               1 (2)
             22 BINARY_MODULO
             24 LOAD_CONST               2 (0)
             26 COMPARE_OP               2 (==)
             28 POP_JUMP_IF_FALSE       14

  5          30 LOAD_NAME                0 (multiples_of_two)
             32 LOAD_METHOD              3 (append)
             34 LOAD_NAME                2 (x)
             36 CALL_METHOD              1
             38 POP_TOP
             40 JUMP_ABSOLUTE           14
        >>   42 POP_BLOCK
        >>   44 LOAD_CONST               3 (None)
             46 RETURN_VALUE
forloop_code spent 15.124301844 seconds
复制代码

若是你不须要一次性所有生成全部的元素,只是想一个一个遍历,List comprehension能够进一步优化成 generators:

def gene_multiples_of_two(most):
    x = 0
    while x <= most:
        if x % 2 == 0:
            yield x
        x += 1
复制代码

0x04 ctypes

注意只有cpython能够用ctypes扩展,由于cpython是用 c 写的,可是其余的好比JythonPyPy不行。你能够引入已经编译过的 c code。好比 Windows 的kernel32.dll,msvcrt.dll和Linux 的libc.so.6

如今咱们要找出一百万之内的全部质数,纯python的写法以下(这里不用关心质数判断函数的原理,若是你想了解,点这里):

from math import sqrt
from itertools import count, islice

def is_prime(n):
    return n > 1 and all(n % i for i in islice(count(2), int(sqrt(n) - 1)))
[x for x in range(1000000) if is_prime(x)]
复制代码

一共花了14秒。

用 c 写一个 Shared library:check_prime.c

#include <stdio.h>
#include <math.h>

int check_prime(int a)
{
     int c;
     for ( c = 2 ; c <= sqrt(a) ; c++ ) {
       if ( a%c == 0 )
        return 0;
     }
     return 1;
}
复制代码
gcc -shared -o check_prime.so -fPIC check_prime.c
复制代码

在 Python 代码中引入:

check_primes_types = ctypes.CDLL('./check_prime.so').check_prime
[x for x in range(1000000) if check_primes_types(x)]
复制代码

如今耗时2s,性能提高了7倍。

0x05 String concatenation

cpython 对于字符串的处理和其余语言不太同样,若是有两个变量ab,值都是hello world,那么在内存中,他们实际上指向的是同一个东西,若是修改 b 的值,会将 b 指向另一个字符串:

若是再把 a的值修改一下,内存中的hello world将会被 gc 掉。一开始 Python 的这种方式看起来很奇怪,可是这不是没有道理的:

That being said, immutable objects are not all that bad. They are actually good for performance if used right, since they can be used as dictionary keys, for instance, or even shared between different variable bindings (since the same block of memory is used every time you reference the same string). This means that the string hey there will be the same exact object every time you use that string, no matter what variable it is stored in (like we saw earlier).

字符串能够被用于字典的 keys。不一样的变量可能会指向同一个字符串,因此这些内存就能够共享,从而在必定程度上节省了内存。

这样的设计对于咱们开发者而言有什么影响呢?看下面这个例子。这个例子内存使用是有问题的,你看出来了吗?每次循环,都会建立一个新的字符串。

full_doc = ""
words = [str(x) for x in range(1000000)]
for word in words:
    full_doc += word
复制代码

用列表生成器会更加高效:

full_doc = "".join(world_list)
复制代码

相似的还有:

document = title + introduction + main_piece + conclusion
复制代码

会建立没必要要的中间变量,用下面这种方法会好一点:

document = "%s%s%s%s" % (title, introduction, main_piece, conclusion)
复制代码

0x06 并发

初学者每每对并发并行两个概念搞不清,认为只有并行才能并发。这是一个很大的话题,同时我以为《Mastering Python High Performance》这本书这部分讲得并没什么特别好的地方,能够看一下我前面写的几篇文章:

0x07 其余

  • Membership testing : 能用字典就别用列表。
  • 使用内置库:Python 自带的通常就是最优的,好比map(operator.add, list1, list2会比map(lambda x, y: x+y, list1, list2)高效。
  • collections.deque,当须要频繁使用popinsert的时候,deque会比list更加高效,由于deque有 O(1)的 pop(0)pop(-1)append性能。
  • 使用 key 而不是比较函数进行排序:
l.sort(key=lambda a: a[1])
复制代码

要比

l.sort(cmp=lambda a,b: cmp(a[1], b[1]))
复制代码

更加高效。

若是你像我同样真正热爱计算机科学,喜欢研究底层逻辑,欢迎关注个人微信公众号:

相关文章
相关标签/搜索