Python/Numpy 性能优化

Cython

将 Python 翻译成 c/c++ 再编译执行。
比原生 Python 快 1.5 倍左右。
缺点是要写个 makefile

pypy

优点是无需像 cython 一样需要修改代码,写 makefile 和 main,缺点是有些三方库不支持。

安装:

1
brew install pypy3

然后安装 pypy pip。注意 pypy pip 不支持 socks5 代理,可能需要关闭或指定 http 代理:

1
2
3
4
pypy3 -m ensurepip
export ALL_PROXY=
pypy3 -m pip install pip --upgrade
pypy3 -m pip install setuptools --upgrade

将 pypy3 path 加入 $PATH 不然安装 tables 的时候报 warning:

1
export PATH=$PATH:/usr/local/share/pypy3

安装依赖(举点例子):

1
2
3
4
5
6
7
8
pypy3 -m pip install numpy
pypy3 -m pip install TA-Lib
pypy3 -m pip install requests
pypy3 -m pip install ccxt
pypy3 -m pip install tables
pypy3 -m pip install matplotlib
pypy3 -m pip install coloredlogs
pypy3 -m pip install pandas

如果 macOS 遇到 pypy 安装 numpy 时提示:

1
2
3
4
Checking for cc... ld: library not found for -lgcc_s.10.4
clang: error: linker command failed with exit code 1 (use -v to see invocation)
...
RuntimeError: Broken toolchain: cannot link a simple C program

尝试下面命令后再次重试安装 numpy:

1
2
3
cd /usr/local/lib
sudo ln -s ../../lib/libSystem.B.dylib libgcc_s.10.4.dylib
cd -

Numpy

比原生 Python 快 10 倍左右。

numexpr

1
2
3
4
5
6
import numpy as np
import numexpr as ne
N = 10 ** 5
a = np.random.uniform(-1, 1, N)
b = np.random.uniform(-1, 1, N)
ne.evaluate('a ** 2 + b ** 2')

比 Numpy 快 2 到 10 倍。

Pandas: 避免大量 df.append() 或 df.iloc() 调用

如果有频繁的 append 操作,使用 list 而非 df,CPU 耗时、内存消耗都降低很多。3600 行的回测运行 10 次,df 改为 list,运行时间从 49 秒降低至 13.35 秒,性能提升 267%。
如果有频繁的 iloc 操作,想办法使用 list + dict 代替 iloc + key 取数,还是刚才那个 3600 行跑 10 次,14.5 秒缩短到 9 秒,性能提升 55%。

CPU 耗时分析

pprofile

1
2
pip3 install pprofile
pprofile hard_work.py | grep -v 0.00% > hard_work.log

结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Command line: hard_work.py
Total duration: 12.2936s
File: /Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/multiprocessing/pool.py
File duration: 2.11106s (17.17%)
Line #|      Hits|         Time| Time per hit|      %|Source code
------+----------+-------------+-------------+-------+-----------
(call)|         1|   0.00452113|   0.00452113|  0.04%|# <frozen importlib._bootstrap>:1009 _handle_fromlist
(call)|         8|   0.00171661|  0.000214577|  0.01%|# /Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/multiprocessing/process.py:72 __init__
(call)|         1|    0.0330989|    0.0330989|  0.27%|# /Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/multiprocessing/pool.py:250 _setup_queues
(call)|         1|    0.0355132|    0.0355132|  0.29%|# /Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/multiprocessing/pool.py:227 _repopulate_pool
(call)|         1|  0.000757217|  0.000757217|  0.01%|# /Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/threading.py:758 __init__
(call)|         1|   0.00308776|   0.00308776|  0.03%|# /Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/threading.py:829 start
(call)|         1|  0.000803709|  0.000803709|  0.01%|# /Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/threading.py:829 start
   218|         8|   0.00123382|  0.000154227|  0.01%|            worker = self._pool[i]
(call)|         8|   0.00212884|  0.000266105|  0.02%|# /Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/multiprocessing/pool.py:152 Process
(call)|         8|    0.0311823|   0.00389779|  0.25%|# /Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/multiprocessing/process.py:101 start
(call)|         1|   0.00201011|   0.00201011|  0.02%|# /Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/multiprocessing/pool.py:212 _join_exited_workers
(call)|         1|    0.0309622|    0.0309622|  0.25%|# /Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/multiprocessing/context.py:109 SimpleQueue
(call)|         1|   0.00208473|   0.00208473|  0.02%|# /Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/multiprocessing/context.py:109 SimpleQueue
(call)|         1|   0.00138021|   0.00138021|  0.01%|# /Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/multiprocessing/pool.py:650 get
   411|        19|  0.000627041|  3.30022e-05|  0.01%|        while thread._state == RUN or (pool._cache and thread._state != TERMINATE):
   412|        19|    0.0777183|   0.00409043|  0.63%|            pool._maintain_pool()
(call)|         1|   0.00205207|   0.00205207|  0.02%|# /Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/multiprocessing/pool.py:244 _maintain_pool
   413|        18|      2.02267|     0.112371| 16.45%|            time.sleep(0.1)
   422|         1|   0.00222588|   0.00222588|  0.02%|        for taskseq, set_length in iter(taskqueue.get, None):
(call)|         1|   0.00127983|   0.00127983|  0.01%|# /Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/threading.py:534 wait
(call)|         1|    0.0013001|    0.0013001|  0.01%|# /Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/multiprocessing/pool.py:647 wait

vprof

如果使用 cython,直接:

1
2
vprof -c h hard_work.py # code heatmap (first call below)
vprof -c p hard_work.py # code profiling (second call below)

如果不使用 cython 而是 pypy:

使用 pypy pip 安装 vprof。注意 pypy pip 不支持 socks5 代理,可能需要关闭或指定 http 代理:

1
2
export ALL_PROXY=
pypy3 -m pip install vprof

然后 pypy3 下要安装原有的 pip 各种依赖,最后跑 vprof:

1
2
pypy3 -m vprof -c h hard_work.py # code heatmap (first call below)
pypy3 -m vprof -c p hard_work.py # code profiling (second call below)

多线程与多进程并发

由于全局解释器的存在,多线程基本没用,用线程池最多优化个5%顶天了。

直接上进程池。进程池的好处是:
1) CPU 层面不受全局解释器的负面影响,可以发挥处理器的最大性能;
2) 内存层面,如果同一进程一直执行大内存操作(稍微大点的 DataFrame),进程会一直申请内存不释放(python 用完的对象的内存会留给后面的 python 对象使用,不会还给系统)。而单独的进程结束的时候,系统会释放掉进程分配的内存。参考以下两个链接:
* Why doesn’t Python release the memory when I delete a large object?
* https://stackoverflow.com/questions/15455048/releasing-memory-in-python

1
2
3
4
5
6
7
8
9
10
11
import multiprocessing

def single_job(param):
    print(param)

if __name__ == '__main__':
    cpu_count_m = multiprocessing.cpu_count()
    pool = multiprocessing.Pool(cpu_count_m)
    result_m = pool.map(single_job, [param])
    pool.close()
    pool.join()

在4代i5上(双核四线程),测试了一个原本单进程执行的287秒的操作,设置进程池数量为2,最终时间为187秒(提升53%),进程池数量为4,最终时间为127秒(提升126%)。也就是线程池数量设置直接用multiprocessing.cpu_count()即可,不必考虑物理核数。
内存从原本的峰值10+G降到了0.6G。4代i7(四核八线程)跑起来会更快(懒的测没测大概四倍多吧),内存峰值也稍微会多一点(1.2G)。老电脑没到期,8代i7要9月份才能换到,性能应该强更多了吧。。。。

matplot 无法在进程池绘制问题

如果弹 The process has forked and you cannot use this CoreFoundation functionality safely. You MUST exec(). 错误:

1
2
3
The process has forked and you cannot use this CoreFoundation functionality safely. You MUST exec().
Break on __THE_PROCESS_HAS_FORKED_AND_YOU_CANNOT_USE_THIS_COREFOUNDATION_FUNCTIONALITY___YOU_MUST_EXEC__() to debug.
The process has forked and you cannot use this CoreFoundation functionality safely. You MUST exec().

不用把fork进程的数据回主进程渲染。直接更新 matplot 到 3.0.3 以上版本:

1
pip3 install matplotlib --upgrade

CuPy

使用 CUDA 计算,直接将 numpy 替换成 cupy。
比原生 Python 快 250 倍左右。
问题就是 Macbook Pro 新款都是 A 卡,真、苹果与狗不得入内。

多显卡

使用 cupy.cuda.Device(cuda_index) 切换显卡设备:

1
2
with cupy.cuda.Device(1):
    x_on_gpu1 = cupy.array([1, 2, 3, 4, 5])

这里 x_on_gpu1 将在 GPU 1 上分配。

使用 Chainer 简化主存/显存切换

本小节内容摘自 在Chainer中使用GPU,更多详细信息请参考原文。

Chainer将CuPy的默认分配器更改为内存池,因此用户可以直接使用CuPy的功能而不需要处理内存分配器。

Chainer提供了一些方便的功能来自动切换和选择设备。例如,chainer.cuda.to_gpu()函数将numpy.ndarray对象复制到指定的设备:

1
2
x_cpu = np.ones((5, 4, 3), dtype=np.float32)
x_gpu = cuda.to_gpu(x_cpu, device=1)

它相当于使用CuPy的以下代码:

1
2
3
x_cpu = np.ones((5, 4, 3), dtype=np.float32)
with cupy.cuda.Device(1):
    x_gpu = cupy.array(x_cpu)

更多并发骚操作,参考Python并行编程

Over

Comments