里的open函数_Python里CPU密集型任务提速试验

众所周知,纯Python跑起来真的很慢。那得有多慢呢?本篇文章我们以mandelbrot set为例,来探讨一下Python在CPU密集型任务的运行速度,以及优化方法。

Mandelbrot set

网上查了查,好像有翻译为曼德勃罗集合,曼德布洛特集合等等。大概意思就是一堆满足如下条件的复数集合。

d7b54c9e94e7d10a79488e4dab8fa305.png

并且

c9606d19bfacff5da45edaebdc79f090.png

理不理解这个概念其实没那么重要,反正就是要用Python运行下面这项任务。

def compute_mandelbrot_py(N_max, some_threshold, nx, ny):
    # A grid of c-values
    xs = np.linspace(-2, 1, nx)
    ys = np.linspace(-1.5, 1.5, ny)
    mandelbrot_set = np.zeros((len(xs), len(ys)))
    for i in range(len(xs)):
        for k in range(len(ys)):
            x = xs[i]
            y = ys[k]
            c = x + 1j*y
            z = c
            is_inside = 1
            for j in range(N_max):
                z = z**2 + c
                if abs(z) >= some_threshold:
                    is_inside = 0
                    break
            mandelbrot_set[i, k] = is_inside

    return mandelbrot_set

为了更好地观察和比较运行速度,我们把N_max与some_threshold统一分别取500和4。nx和ny分别代表了实数和虚数部分的样本,总共我们会计算nx * ny个复数来分别检验他们在不在这个集合里。

先取nx = 500, ny = 500来看看python会花多少时间。

电脑配置

Processor: 2.9 GHz Intel Core i7

Memory: 16 GB 2133 MHz LPDDR3

Graphics: Intel HD Graphics 630 1536 MB

开始运行

mandelbrot_set = compute_mandelbrot_py(500, 4., 500, 500)

花了9.8秒,嗯,真的是很慢,250000个数据点居然就要差不多10秒了,那1000 * 1000岂不是要炸?

尝试了下,果然是x4,花了41秒。那开始想办法提速吧。

1. 用Numpy把运算向量化

我们都知道Numpy向量运算代替循环可以优化计算,上面的头两个for loop因为里面的东西不取决于上一次循环的结果,所以自然的可以用矩阵表示并进行向量化运算。

c = x[:,np.newaxis] + 1j*y[np.newaxis,:]

z = c
for j in range(N_max):
    z = z**2 + c

mandelbrot_set = (abs(z) < some_threshold)

这里的唯一缺点是不能像之前提前离开for j in range(N_max): 这个循环,但是结果显示还是比纯Python暴力循环方法快了不少,500 500花了0.47秒,1000 * 1000花了4.4秒。

2. 把密集运算部分放到Cython执行

Cython是结合了Python和C的语法的一种语言,可以简单认为就是给Python加上了静态类型后的语法,由于会直接编译成二进制程序,所以性能较Python而言会有很大提升。

我们先把mandelbrot运算部分的代码用cython编写然后方法一个pyx后缀的文件里。

cpdef mandelbrot(
        double [:] xs,
        double [:] ys,
        double [:, :] mandelbrot_set,
        int N_max,
        int some_threshold):
    cdef:
       int i, k, j, is_inside
       double x, y
       complex c, z

    for i in range(len(xs)):
        for k in range(len(ys)):
            x = xs[i]
            y = ys[k]
            c = x + 1j*y
            z = c
            is_inside = 1
            for j in range(N_max):
                z = z**2 + c
                if abs(z) >= some_threshold:
                    is_inside = 0
                    break
            mandelbrot_set[i, k] = is_inside

和纯Python比,这个代码把一些变量都加上了静态类型,我们把这个文件命名为mandelbrot_pyx.pyx。接下来要建立一个setup.py文件

from distutils.core import setup
from Cython.Build import cythonize
import numpy

setup(
    ext_modules = cythonize("mandelbrot_pyx.pyx"),
    include_dirs=[numpy.get_include()]
)

保存好这个文件后就可以开始编译cython代码了

python setup.py build_ext --inplace

这样在我们的python主程序里可以调用mandelbrot_pyx这个模块以及里面的mandelbrot函数。

mandelbrot_set = np.zeros((len(xs), len(ys)))
mandelbrot_pyx.mandelbrot(xs, ys, mandelbrot_set, N_max, some_threshold)
return mandelbrot_set

带入M = 1000, N = 1000, 发现只花了0.51秒,这比纯Python解法已经足足快了80倍了。于是我们再次增加M和N的值,让M = N = 2000,结果运算花了2.03秒,可以说是线性增加。

Cython里还有prange可以支持并行计算,在执行时得释放全局解释锁,可以有效实现多线程并行运算,把循环改成。

for i in prange(m, schedule="static", chunksize=1, num_threads=4, nogil=True):

如果只在本地跑的话随着M和N增大,运行速度还是会有点慢。那么怎么才能继续大幅度提高这个代码的效率呢?下面就来试试GPU并行吧。

3. GPU并行计算

由于我的电脑显卡并不是nvdia,所以这里不用CUDA,来试试opencl好了。

python部分,首先是启动opencl前的一些准备。

import opencl as cl

context = cl.Context(cl.get_platforms()[0].get_devices())
queue = cl.CommandQueue(context, context.devices[0])
program = cl.Program(context, open('mandelbrot.cl').read()).build(options='')

然后把input拷贝到gpu内存

gpu_real = cl.Buffer(context, cl.mem_flags.READ_ONLY, len(xs) * 4)
gpu_imag = cl.Buffer(context, cl.mem_flags.READ_ONLY, len(ys) * 4)
gpu_output = cl.Buffer(context, cl.mem_flags.WRITE_ONLY, len(xs) * len(ys) * 4)

cl.enqueue_copy(queue, gpu_real, xs, is_blocking=False)
cl.enqueue_copy(queue, gpu_imag, ys, is_blocking=False)

接下来是opencl部分,其实语法就是c语言的语法

__kernel void
mandelbrot(__global __read_only float *coords_real,
           __global __read_only float *coords_imag,
           __global __write_only int *mandelbrot_set,
           int w, int h, int max_iter)
{
    const int x = get_global_id(0);
    const int y = get_global_id(1);

    float c_real, c_imag;
    float z_real, z_imag;
    int iter;
    if ((x < h) && (y < w)) {
        float c_real = coords_real[x], c_imag = coords_imag[y];
        float z_real = c_real, z_imag = c_imag;
        bool is_inside = true;
        for(unsigned i=0; i<max_iter; ++i) {
            float z_real2 = z_real * z_real, z_imag2 = z_imag * z_imag;
            if (z_real2 + z_imag2 >= 4) {
                is_inside = false;
                break;
            }
            z_imag = 2 * z_real * z_imag + c_imag;
            z_real = z_real2 - z_imag2 + c_real;
        }
        if (is_inside) {
            mandelbrot_set[x * w + y] = 1;
        }
    }
}

把cl部分命名为mandelbrot.cl后,下面这句话就把这段代码编译了。

cl.Program(context, open('mandelbrot.cl').read()).build(options='')

把代码跑起来

program.mandelbrot(....)

再通过以下代码把结果从gpu拷贝回来。

cl.enqueue_copy(queue, mandelbrot_set, gpu_counts, is_blocking=True)

惊喜地发现2000 * 2000的集合代如运算只花了不到0.2秒!

总结

以上就是加速Python运行效率的一些小试验,并不一定适用于所有CPU密集型任务,但是希望有一定借鉴意义。下面附上一张运行速度的折线图。

7eb26174d3b74f0a0daa173e149795cc.png

cd809d02080480c376b46197f49dc52e.png
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值