返回顶部
首页 > 资讯 > 后端开发 > Python >使用Cython中prange函数实现for循环的并行
  • 814
分享到

使用Cython中prange函数实现for循环的并行

2024-04-02 19:04:59 814人浏览 薄情痞子

Python 官方文档:入门教程 => 点击学习

摘要

目录楔子使用 prangeprange 的其它参数在reductions操作上使用prange并行编程的局限性小结楔子 上一篇文章我们探讨了 GIL 的原理,以及如何释放 GIL 实

楔子

上一篇文章我们探讨了 GIL 的原理,以及如何释放 GIL 实现并行,做法是将函数声明为 nogil,然后使用 with nogil 上下文管理器即可。在使用上非常简单,但如果我们想让循环也能够并行执行,那么该方式就不太方便了,为此 Cython 提供了一个 prange 函数,专门用于循环的并行执行。

这个 prange 的特殊功能是 Cython 独一无二的,并且 prange 只能与 for 循环搭配使用,不能独立存在。

Cython 使用 OpenMP api 实现 prange,用于多平台共享内存的处理。但 OpenMP 需要 C 或者 c++ 编译器支持,并且编译时需要指定特定的编译参数来启动。例如:当我们使用 GCc 时,必须在编译和链接二进制文件的时候指定一个 -fopenmp,以确保启用 OpenMP。

许多编译器均支持 OpenMP ,包括免费的和商业的。但 Clang/LLVM 则是一个最显著的例外,它只在一个单独的分支中得到了初步的支持,而为它完全实现的 OpenMP 还在开发当中。

而使用 prange,需要从 cython.parallel 中进行导入。但是在这之前,我们先来看一个例子:

import numpy as np
from cython cimport boundscheck, wraparound

cdef inline double nORM2(double complex z) nogil:
    """
    接收一个复数 z, 计算它的模的平方
    由于 norm2 要被下面的 escape 函数多次调用
    这里通过 inline 声明成内联函数
    :param z: 
    :return: 
    """
    return z.real * z.real + z.imag * z.imag


cdef int escape(double complex z,
                double complex c,
                double z_max,
                int n_max) nogil:
    """
    这个函数具体做什么, 不是我们的重点
    我们不需要关心
    """
    cdef:
        int i = 0
        double z_max2 = z_max * z_max
    while norm2(z) < z_max2 and i < n_max:
        z = z * z + c
        i += 1
    return i


@boundscheck(False)
@wraparound(False)
def calc_julia(int resolution,
               double complex c,
               double bound=1.5,
               double z_max=4.0,
               int n_max=1000):
    """
    我们将要在 python 中调用的函数
    """
    cdef:
        double step = 2.0 * bound / resolution
        int i, j
        double complex z
        double real, imag
        int[:, :: 1] counts
    counts = np.zeros((resolution + 1, resolution + 1), dtype="int32")

    for i in range(resolution + 1):
        real = -bound + i * step
        for j in range(resolution + 1):
            imag = -bound + j * step
            z = real + imag * 1j
            counts[i, j] = escape(z, c, z_max, n_max)

    return np.array(counts, copy=False)

我们手动编译一下,然后调用 calc_julia 函数,这个函数做什么不需要关心,我们只需要将注意力放在那两层 for 循环(准确的说是外层循环)上即可,这里我们采用手动编译的形式。

import cython_test
import numpy as np
import matplotlib.pyplot as plt
arr = cython_test.calc_julia(1000, 0.322 + 0.05j)
plt.imshow(np.log(arr))
plt.show()

那么 calc_julia 这个函数耗时多少呢?我们来测试一下:

使用 prange

对于上面的代码来说,外层循环里面的逻辑是彼此独立的,即当前循环不依赖上一层循环的结果,因此这非常适合并行执行。所以 prange 便闪亮登场了,我们只需要做简单的修改即可:

import numpy as np
from cython cimport boundscheck, wraparound
from cython.parallel cimport prange

cdef inline double norm2(double complex z) nogil:
    return z.real * z.real + z.imag * z.imag


cdef int escape(double complex z,
                double complex c,
                double z_max,
                int n_max) nogil:
    cdef:
        int i = 0
        double z_max2 = z_max * z_max
    while norm2(z) < z_max2 and i < n_max:
        z = z * z + c
        i += 1
    return i


@boundscheck(False)
@wraparound(False)
def calc_julia(int resolution,
               double complex c,
               double bound=1.5,
               double z_max=4.0,
               int n_max=1000):
    cdef:
        double step = 2.0 * bound / resolution
        int i, j
        double complex z
        double real, imag
        int[:, :: 1] counts
    counts = np.zeros((resolution + 1, resolution + 1), dtype="int32")
    # 只需要将外层的 range 换成 prange
    for i in prange(resolution + 1, nogil=True):
        real = -bound + i * step
        for j in range(resolution + 1):
            imag = -bound + j * step
            z = real + imag * 1j
            counts[i, j] = escape(z, c, z_max, n_max)

    return np.array(counts, copy=False)

我们只需要将外层循环的 range 换成 prange 即可,里面指定 nogil=True,便可实现并行的效果,至于这个函数的其它参数以及用法后面会说。而且一旦使用了 prange,那么在编译的时候,必须启用 OpenMP,下面看一下编译脚本。

from distutils.core import setup, Extension
from Cython.Build import cythonize

ext = [Extension("cython_test",
                 sources=["cython_test.pyx"],
                 extra_compile_args=["-fopenmp"],
                 extra_link_args=["-fopenmp"])]

setup(ext_modules=cythonize(ext, language_level=3))

编译测试一下:

我们看到效率大概是提升了两倍,因为我 windows 上使用的不是 gcc,所以这里是在 Centos 上演示的。而我的 CentOS 服务器只有两个核,因此效率提升大概两倍左右。

所以只是做了一些非常简单的修改,便可带来如此巨大的性能提升,简直妙啊。prange 是要搭配 for 循环来使用的,如果 for 循环内部的逻辑彼此独立,即第二层循环不依赖第一层循环的某些结果,那么不妨使用 prange 吧。

注意还没完,我们还能做得更好,下面就来看看 prange 里面的其它的参数,这样我们能更好利用 prange 的并行特性。

prange 的其它参数

prange 函数的原型如下:

# 第一个参数 self 我们不需要管
# prange 实际上是类 CythonDotParallel 的成员函数
# 因为 Cython 内部执行了下面这行逻辑
# sys.modules['cython.parallel'] = CythonDotParallel()
# 所以它将一个实例对象变成了一个模块

def prange(self, start=0, stop=None, step=1, 
           nogil=False, schedule=None, 
           chunksize=None, num_threads=None):

我们先来看前三个参数,start、stop、step。

  • prange(3): 相当于 start=0、stop=3;
  • prange(1, 3): 相当于 start=1、stop=3;
  • prange(1, 3, 2): 相当于 start=1、stop=3、step=2;

类似于 range,同样不包含结尾 stop。

然后是第四个参数 nogil,它默认是 False,但事实上我们必须将其设置为 True,否则会报出编译错误。

然后剩下的三个参数,如果我们不指定的话,那么 Cython 编译器采取的策略是将整个循环分成多个大小相同的连续块,然后给每一个可用线程一个块。然而这个策略实际上并不是最好的,因为每一层循环用的时间不一定一样,如果一个线程很快就完成了,那么不就造成资源上的浪费了吗?

我们修改一下,将 schedule 指定为 "static",chunksize 指定为 1:

for i in prange(resolution + 1, nogil=True, 
                schedule="static", chunksize=1):

其它地方不变,只是加两个参数,然后重新测试一下。

我们看到效率上是差不多的,原因是我的机器只有两个核,如果核数再多一些的话,那么速度就会明显地提升。

下面来解释一下剩余的三个参数的含义,首先是 schedule,它有以下几个选项:

"static"

整个循环在编译时会以一种固定的方式分配给多个线程,如果 chunksize 没有指定,那么会分成 num_threads 个连续块,一个线程一个块。如果指定了 chunksize,那么每一块会以轮询调度算法(Round Robin)交给线程进行处理,适用于任务均匀分布的情况。

"dynamic"

线程在运行时动态地向调度器申请下一个块,chunksize 默认为 1,当任务负载不均时,动态调度是最佳的选择。

"guided"

块是动态分布的,就像 dynamic 一样,但这与 dynamic 还不同,chunksize 的比例不是固定的,而是和 剩余迭代次数 / 线程数 成比例关系。

"runtime"

不常用。

控制 schedule 和 chunksize 可以方便地探索不同的并行执行策略、以及工作负载分配,通常指定 schedule 为 "static",加上设置一个合适的 chunksize 是最好的选择。而 dynamic 和 guided 适用于动态变化的执行上下文,但会导致运行时开销。

当然还有最后一个参数 num_threads,很明显不需要解释,就是使用的线程数量。如果不指定,那么 prange 会使用尽可能多的线程。所以我们只是做了一点修改,便可以带来巨大的性能提升,这种性能提升与 Cython 在纯 Python 上带来的性能提升成倍增关系。

在reductions操作上使用prange

我们经常会循环遍历数组计算它们的累和、累积等等,这种数据量减少的操作我们称之为 reduction 操作。而 prange 对这样的操作也是支持并行执行的,我们举个例子:

from cython cimport boundscheck, wraparound

@boundscheck(False)
@wraparound(False)
def calc_julia(int [:, :: 1] counts,
               int val):
    cdef:
        int total = 0
        int i, j, M, N
    N, M = counts.shape[: 2]
    for i in range(M):
        for j in range(N):
            if counts[i, j] == val:
                total += 1

    return total / float(counts.size)

显然我们是希望计算一个数组中值 val 的元素的个数,下面测试一下:

如果改成 prange 的话,会有什么效果呢?代码的其它部分不变,只需要导入 prange,然后将 range(M) 改成 prange(M, nogil=True) 即可。

速度比原来快了两倍多,还是很可观的,如果你的 CPU 是多核的,那么效率提升会更明显。

这里我们没有使用 schedule 和 chunksize 参数,你也可以加上去。当然啦,如果占用内存过大的话,它可能无法像预期的一样显著地提升性能,因为 prange 的优化重点是在 CPU 上面。

但是可能有人会有疑问,多个线程同时对 total 变量进行自增操作,这么做不会造成冲突吗?答案是不会的,因为加法是可交换的,即无论是 a + b 还是 b + a,结果都是相同的。Cython(通过 OpenMP)生成线程代码,每个线程计算循环子集的和,然后所有线程再将各自的和汇总在一起。

如果是交给 Numpy 来做的话,那么等价于如下:

np.sum(counts == val) / float(counts.size)

但是效率如何呢?我们来对比一下:

我们采用并行计算用的是 6.13 毫秒,Numpy 用的是 20 毫秒,看样子是我们赢了,并且 CPU 核心数越多,差距越明显,这便是并行计算的威力。当然对于这种算法来说,还是直接交给 Numpy 吧,毕竟人家都帮你封装好了,一个函数调用就可以解决了。

因此有效利用计算机硬件资源确实是最直接的办法。

并行编程的局限性

虽然 Cython 的 prange 容易使用,但其实还是有局限性的,当然这个局限性和 Cython无关,因为理想化的并行扩展本身就是一个难以实现的事情。我们举个例子:

def filter(nrows, ncols):
    for i in range(nrows):
        for j in range(ncols):
            b[i, j] = (a[i, j] + a[i - 1, j] + a[i + 1, j] +
                       a[i, j - 1] + a[i, j + 1]) / 5.0)

假设我们要做一个过滤器,计算每一个点加上它周围的四个点的平均值。但如果这里将外层的 range 换成 prange,那么它的整体性能不会明显提升。因为内层循环访问的是不连续的数组元素,由于缺乏数据本地性,CPU 的缓存无法生效,反而导致 prange 变慢。

那么我们什么时候使用 prange 呢?遵循以下法则即可:

  • 1. prange 能够很好的利用 CPU 并行操作, 这一点我们已经说过了;
  • 2. 非本地读写的那些和内存绑定的操作很难提高速度;
  • 3. 用较少的线程更容易实现加速, 因为对于 CPU 密集而言, 即便指定了超越核心数的线程也是没有意义的;
  • 4. 使用优化的线程并行库是将 CPU 所有核心都用于常规计算的最佳方式;

当然,其实我们在开发的时候是可以随时使用 prange 的,只要循环体不和 Python 对象进行交互即可。

小结

Cython 允许我们绕过全局解释器,只要我们把和 Python 无关的代码分离出来即可。对于那些不需要和 Python 交互的 C 代码,可以轻松地使用 prange 实现基于线程的并行。

在其它语言中,基于线程的并行很容易出错,并且难以正确处理。而 Cython 的 prange 则不需要我们在这方面费心,能够轻松地处理很多性能瓶颈。

到此这篇关于使用Cython中prange函数实现for循环的并行的文章就介绍到这了,更多相关Cython prange for循环并行内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

--结束END--

本文标题: 使用Cython中prange函数实现for循环的并行

本文链接: https://lsjlt.com/news/119710.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • 使用Cython中prange函数实现for循环的并行
    目录楔子使用 prangeprange 的其它参数在reductions操作上使用prange并行编程的局限性小结楔子 上一篇文章我们探讨了 GIL 的原理,以及如何释放 GIL 实...
    99+
    2024-04-02
  • 怎么在R语言中使用for循环实现并行处理
    怎么在R语言中使用for循环实现并行处理?很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。什么是R语言R语言是用于统计分析、绘图的语言和操作环境,属于GNU系统的一...
    99+
    2023-06-14
  • python如何在for循环语句中使用Reversed()函数
    这篇文章主要介绍了python如何在for循环语句中使用Reversed()函数,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。在for循环语句中使用Reversed()函数r...
    99+
    2023-06-27
  • R语言中for循环的并行处理方式
    前言 本文用于记录笔者在将R语言中的for语句并行化处理中的一些问题。 实验 这里使用foreach和doParallel包提供的函数实现for语句的并行处理。 for语句脚本 ...
    99+
    2024-04-02
  • python中for循环的多种使用实例
    目录前言for循环迭代字符串for打印数字注意for循环不能迭代数值类型for循环打印数字的话要借用range函数for循环可用来初始化列表简单的往列表里添加数据列表推导式总结前言 ...
    99+
    2024-04-02
  • Golang函数的for循环迭代器用法
    作为一门具有高效性和高并发性的编程语言,Golang在函数的设计和实现上面有很多值得我们学习的地方。其中,for循环迭代器的使用就是Golang函数中一个重要的特性。本文将会从以下几个方面来详细介绍Golang函数的for循环迭代器用法。一...
    99+
    2023-05-17
    函数 Golang for循环
  • js如何使用for循环实现求和
    这篇文章主要介绍了js如何使用for循环实现求和,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。使用for循环,累加求和function sum(arr) ...
    99+
    2023-06-17
  • Python3中的for循环怎么使用
    本篇内容介绍了“Python3中的for循环怎么使用”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!一·ra...
    99+
    2024-04-02
  • 如何使用Python中的for循环
    如何使用Python中的for循环Python是一种简单易用的编程语言,其中的for循环是非常常用的工具之一。通过使用for循环,我们可以循环遍历一系列的数据,进行有效的处理和操作,提高代码的效率。下面,我将通过具体的代码示例,介绍如何使用...
    99+
    2023-10-25
    Python 使用 for循环
  • Oracle中for循环的使用方法
    Oracle for in loop 循环的一些实例,以作学习和加强使用熟练度及场景应用. 一些技巧 for 语句后面的 loop end l...
    99+
    2024-04-02
  • 在matlab中实现for循环的方法
    简单for循环 for循环用来循环处理数据。 例:输出1~100的和 >> clear >> sum = 0; >> for i = 1:10...
    99+
    2024-04-02
  • Python 用for循环实现猜数字游戏
    据说Python语言是装逼神器,我也来学下Python。 Python的语法确实比较优美,都不要{},连标点符号都很少,下面我们用Python来写个猜数字游戏练练手。 """ Created on Sun Oct 23 19:35:01...
    99+
    2023-01-31
    数字 游戏 Python
  • js中for循环的实例用法
    这篇文章主要介绍“js中for循环的实例用法”,在日常操作中,相信很多人在js中for循环的实例用法问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”js中for循环的实例用法”...
    99+
    2024-04-02
  • python如何在for循环语句中使用Enumerate()枚举函数
    小编给大家分享一下python如何在for循环语句中使用Enumerate()枚举函数,希望大家阅读完这篇文章之后都有所收获,下面让我们一起去探讨吧!在for循环语句中使用Enumerate()枚举函数用enumerate()函数获取可迭代...
    99+
    2023-06-27
  • Go语言中的for循环使用实例分析
    这篇文章主要介绍“Go语言中的for循环使用实例分析”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“Go语言中的for循环使用实例分析”文章能帮助大家解决问题。问题案例一:取地址符在 Go 语言中,我...
    99+
    2023-07-04
  • Python 如何用一行代码实现for循环初始化数组
    我就废话不多说了,大家还是直接看代码吧~ # 用一行代码实现for循环初始化数组 o = 10 b = [ o + u for u in range( 10 ) ] print...
    99+
    2024-04-02
  • Shell脚本中使用for循环和cat命令实现按顺序合并文件
    工作目录下面有mydoc1.txt,mydoc2.txt...mydoc41.txt,本来想用sed排列依次取值排序,然后用cat来合并这些文件,发现达不到预期效果,合并令如下所示: ls -lF *.t...
    99+
    2022-06-04
    脚本 顺序 命令
  • 怎么在Linux中利用shell 实现for循环
    本篇文章为大家展示了怎么在Linux中利用shell 实现for循环,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。C语言风格for ((i=1; i<=100; ...
    99+
    2023-06-09
  • Javascrip的for循环和数组怎么使用
    这篇文章主要介绍“Javascrip的for循环和数组怎么使用”,在日常操作中,相信很多人在Javascrip的for循环和数组怎么使用问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Javascrip的for...
    99+
    2023-06-22
  • Java中break、continue、return在for循环中的使用
    引言:在使用循环的时候,循环里面带有break、continue、return的时候经常弄混,今天特意整理了下,以待后用... for (int i = 1; i < 5; i++) { System.out.println...
    99+
    2023-05-30
    java for break
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作