【问题标题】:Optimizing simple CPU-bound loops using Cython and replacing a list使用 Cython 优化简单的 CPU 绑定循环并替换列表
【发布时间】:2015-08-18 07:29:32
【问题描述】:

我正在尝试评估一些方法,但在性能方面遇到了绊脚石。

为什么我的 cython 代码这么慢?我的期望是代码的运行速度会快很多(对于只有 256 ** 2 个条目的 2d 循环来说可能是纳秒),而不是毫秒。

这是我的测试结果:

$ python setup.py build_ext --inplace; python test.py
running build_ext
        counter: 0.00236220359802 sec
       pycounter: 0.00323309898376 sec
      percentage: 73.1 %

我的初始代码如下所示:

#!/usr/bin/env python
# encoding: utf-8
# filename: loop_testing.py

def generate_coords(dim, length):
    """Generates a list of coordinates from dimensions and size
    provided.

    Parameters:
        dim -- dimension
        length -- size of each dimension

    Returns:
        A list of coordinates based on dim and length
    """
    values = []
    if dim == 2:
        for x in xrange(length):
            for y in xrange(length):
                values.append((x, y))

    if dim == 3:
        for x in xrange(length):
            for y in xrange(length):
                for z in xrange(length):
                    values.append((x, y, z))

    return values

这可以满足我的需要,但速度很慢。对于给定的暗淡,长度 = (2, 256),我在 iPython 上看到的时间约为 2.3 毫秒。

为了加快速度,我开发了一个 cython 等价物(我认为它是一个等价物)。

#!/usr/bin/env python
# encoding: utf-8
# filename: loop_testing.pyx
# cython: boundscheck=False
# cython: wraparound=False

cimport cython
from cython.parallel cimport prange

import numpy as np
cimport numpy as np


ctypedef int DTYPE

# 2D point updater
cpdef inline void _counter_2d(DTYPE[:, :] narr, int val) nogil:
    cdef:
        DTYPE count = 0
        DTYPE index = 0
        DTYPE x, y

    for x in range(val):
        for y in range(val):
            narr[index][0] = x
            narr[index][1] = y
            index += 1

cpdef DTYPE[:, :] counter(dim=2, val=256):
    narr = np.zeros((val**dim, dim), dtype=np.dtype('i4'))
    _counter_2d(narr, val)
    return narr

def pycounter(dim=2, val=256):
    vals = []
    for x in xrange(val):
        for y in xrange(val):
            vals.append((x, y))
    return vals

以及调用时机:

#!/usr/bin/env python
# filename: test.py
"""
Usage:
    test.py [options]
    test.py [options] <val>
    test.py [options] <dim> <val>

Options:
    -h --help       This Message
    -n              Number of loops [default: 10]
"""

if __name__ == "__main__":
    from docopt import docopt
    from timeit import Timer

    args = docopt(__doc__)
    dim = args.get("<dim>") or 2
    val = args.get("<val>") or 256
    n = args.get("-n") or 10
    dim = int(dim)
    val = int(val)
    n = int(n)

    tests = ['counter', 'pycounter']
    timing = {}
    for test in tests:
        code = "{}(dim=dim, val=val)".format(test)
        variables = "dim, val = ({}, {})".format(dim, val)
        setup = "from loop_testing import {}; {}".format(test, variables)
        t = Timer(code, setup=setup)
        timing[test] = t.timeit(n) / n

    for test, val in timing.iteritems():
        print "{:>20}: {} sec".format(test, val)
    print "{:>20}: {:>.3} %".format("percentage", timing['counter'] / timing['pycounter'] * 100)

作为参考,用于构建 cython 代码的 setup.py:

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

include_path = [numpy.get_include()]

setup(
    name="looping",
    ext_modules=cythonize('loop_testing.pyx'),  # accepts a glob pattern
    include_dirs=include_path,
)

编辑: 工作版本链接:https://github.com/brianbruggeman/cython_experimentation

【问题讨论】:

  • 你的 cython 代码很不错。除了 narr[index][0] = x 实际上并没有执行分配(并且会减慢 C python API 调用),请改用 narr[index,0] = x(对于纯 numpy 也是如此)。另外,尝试在您的setup.py 中设置extra_compile_args=['-O3', '-march=native']extra_link_args=['-O3', '-march=native'],这样可以加快速度。
  • 谢谢!我会试试这个。
  • @rth narr[index, 0] 绝对解决了这个问题。我现在的速度大约是 100 倍。我没有看到额外的编译/链接选项有太大变化。但是,我不介意在这一点上留下这些。万分感谢!要添加答案吗?

标签: performance loops numpy cython


【解决方案1】:

看起来您的 Cython 代码正在使用 numpy 数组做一些奇怪的事情,并且没有真正利用 C 编译。要检查生成的代码,请运行

cython -a loop_testing.pyx

如果您避免使用 numpy 部分并直接对 Python 函数进行 Cython 翻译,会发生什么?

编辑:看起来你可以完全避免 Cython 以获得相当不错的加速。 (在我的机器上约为 30 倍)

def npcounter(dim=2, val=256):
  return np.indices((val,)*dim).reshape((dim,-1)).T

【讨论】:

  • 那是我的下一步。如果可以的话,我真的很想避免 malloc 。我正在使用 numpy 块来分配内存。
  • 您可以使用 Cython 制作列表并附加到它们。从那里开始,在您涉足 malloc 之前。
  • 我以为我试图避免使用列表...在 python 中附加意味着我正在使用 python 解释器添加到 python 对象。从我一直在阅读/看到的内容来看,我不想使用这些对象。 gist.github.com/brianbruggeman/625e488777722e852e6c 没有明显区别。
  • @perimosocordiae numpy 数组没有任何问题,在这种特殊情况下,它们应该比使用 python 列表快得多。
  • @rth:奇怪的索引对我来说看起来很可疑,但我想这并没有什么问题。
【解决方案2】:

这个 Cython 代码很慢,因为 narr[index][0] = x 分配严重依赖 Python C-API。使用 narr[index, 0] = x 代替,被翻译成纯 C,并解决了这个问题。

正如@perimosocordiae 所指出的,使用带有注释的cythonize 绝对是调试此类问题的方法。

在某些情况下,还值得在setup.py 中为 gcc 显式指定编译标志,

setup(
   [...]
   extra_compile_args=['-O2', '-march=native'],
   extra_link_args=['-O2', '-march=native'])

这不应该是必要的,假设合理的默认编译标志。但是,例如,在我的 Linux 系统上,默认设置似乎根本没有优化,添加上述标志会显着提高性能。

【讨论】:

    猜你喜欢
    • 2018-05-20
    • 2014-02-18
    • 2016-07-02
    • 1970-01-01
    • 1970-01-01
    • 2012-08-21
    • 2021-04-19
    • 2014-08-05
    • 1970-01-01
    相关资源
    最近更新 更多