我正在测试Numba JIT和PythonC扩展的性能.对于基于循环的函数,在计算2D数组中所有元素的总和时,C扩展似乎比Numba扩展快约3-4倍.

最新情况:

基于有价值的 comments ,我意识到了一个错误,我应该编译(调用)一次Numba JIT.我提供了修复后的测试结果以及额外的 case .But.问题仍然是何时以及如何考虑采用哪种方法.

下面是结果(time_s,value):

# 200 tests mean (including JIT compile inside the loop)
Pure Python: (0.09232537984848023, 29693825)
Numba: (0.003188209533691406, 29693825)
C Extension: (0.000905141830444336, 29693825.0)

# JIT once called before the test loop (to avoid compile time)
Normal: (0.0948486328125, 29685065)
Numba: (0.00031280517578125, 29685065)
C Extension: (0.0025129318237304688, 29685065.0)

# JIT no warm-up also no test loop (only calling once)
Normal: (0.10458517074584961, 29715115)
Numba: (0.314251184463501, 29715115)
C Extension: (0.0025091171264648438, 29715115.0)
  • 我的实现正确吗?
  • 为什么C扩展速度更快,有什么原因吗?
  • 如果我想要最好的性能,我应该总是使用C扩展吗?(非矢量化函数)

main.py

import numpy as np
import pandas as pd
import numba
import time
import loop_test # ext


def test(fn, *args):
    res = []
    val = None
    for _ in range(100):
        start = time.time()
        val = fn(*args)
        res.append(time.time() - start)
    return np.mean(res), val


sh = (30_000, 20)
col_names = [f"col_{i}" for i in range(sh[1])]
df = pd.DataFrame(np.random.randint(0, 100, size=sh), columns=col_names)
arr = df.to_numpy()


def sum_columns(arr):
    _sum = 0
    for i in range(arr.shape[0]):
        for j in range(arr.shape[1]):
            _sum += arr[i, j]
    return _sum


@numba.njit
def sum_columns_numba(arr):
    _sum = 0
    for i in range(arr.shape[0]):
        for j in range(arr.shape[1]):
            _sum += arr[i, j]
    return _sum


print("Pure Python:", test(sum_columns, arr))
print("Numba:", test(sum_columns_numba, arr))
print("C Extension:", test(loop_test.loop_fn, arr))

ext.c

#define PY_SSIZE_CLEAN
#include <Python.h>
#include <numpy/arrayobject.h>

static PyObject *loop_fn(PyObject *module, PyObject *args)
{
    PyObject *arr;
    if (!PyArg_ParseTuple(args, "O!", &PyArray_Type, &arr))
        return NULL;

    npy_intp *dims = PyArray_DIMS(arr);
    npy_intp rows = dims[0];
    npy_intp cols = dims[1];
    double sum = 0;
    PyArrayObject *arr_new = (PyArrayObject *)PyArray_FROM_OTF(arr, NPY_DOUBLE, NPY_ARRAY_IN_ARRAY);
    double *data = (double *)PyArray_DATA(arr_new);
    npy_intp i, j;
    for (i = 0; i < rows; i++)
        for (j = 0; j < cols; j++)
            sum += data[i * cols + j];
    Py_DECREF(arr_new);
    return Py_BuildValue("d", sum);
};

static PyMethodDef Methods[] = {
    {
        .ml_name = "loop_fn",
        .ml_meth = loop_fn,
        .ml_flags = METH_VARARGS,
        .ml_doc = "Returns the sum using for loop, but in C.",
    },
    {NULL, NULL, 0, NULL},
};

static struct PyModuleDef Module = {
    PyModuleDef_HEAD_INIT,
    "loop_test",
    "A benchmark module test",
    -1,
    Methods};

PyMODINIT_FUNC PyInit_loop_test(void)
{
    import_array();
    return PyModule_Create(&Module);
}

setup.py

from distutils.core import setup, Extension
import numpy as np

module = Extension(
    "loop_test",
    sources=["ext.c"],
    include_dirs=[
        np.get_include(),
    ],
)

setup(
    name="loop_test",
    version="1.0",
    description="This is a test package",
    ext_modules=[module],
)
python3 setup.py install

推荐答案

我想补充一下John Bollinger的回答:

首先,C扩展倾向于在Linux上使用GCC编译(可能是Windows上的MSVC和MacOS AFAIK上的Clang),而内部有Numba uses the LLVM个编译工具链.如果你想比较两者,那么you should use Clang是基于LLVM工具链的.事实上,你还应该使用与Numba相同的LLVM版本,这样比较才公平.Clang、GCC和MSVC优化代码的方式不同,因此生成的程序可能具有非常不同的性能.

此外,Numba是JIT,所以它不关心不同平台之间的兼容性(指令集扩展).这意味着它可以使用AVX-2SIMD instruction set,如果您的机器上有AVX-2SIMD instruction set,而主流编译器出于兼容性的考虑,默认情况下不会这样做.事实上,Numba确实做到了这一点.您可以指定Clang和GCC来优化目标机器的代码,而不关心编译标志为-march=native的机器之间的兼容性.因此,生成的包肯定会更快,但也可能在旧机器上崩溃(或者可能会慢得多).您还可以启用某些特定的指令集(带有类似-mavx2的标志).

此外,Numba uses an aggressive optimization level默认情况下,而AFAIK C扩展使用-O2标志,这在GCC和Clang上都是 *not auto-vectorize the code by default(即不使用打包的SIMD指令).如果还没有这样做的话,你当然应该手动指定使用-O3标志.在MSVC上,等效标志是/O2(AFAIK还没有/O3).

请注意,Numba functions can be compiled eagerly(而不是默认的懒惰)通过提供特定的signature(可能是多个)来实现.这意味着您应该知道输入参数的类型,并且应用程序的启动时间可能会显著增加.Numba functions can also be cached,这样就不会在同一个平台上反复重新编译函数.这可以用标志cache=True来完成.但是,对于您的特定用例,它可能并不总是有效的.

最后但并非最不重要的是,the two codes are not equivalent人.这当然是最重要的一点.Numba代码处理int32类型的arr并将值累加到64位整数_sum中,而C扩展将值累加到double精度浮点类型中.浮点类型不是关联的(除非您使用标志-ffast-math告诉编译器假定它们是关联的,该标志在默认情况下是不启用的,因为它不安全),因此是accumulating floating-point numbers is far more expensive than integers due to the high latency of the FMA unit on most platform. 此外,我真的想知道PyArray_FROM_OTF是否执行了正确的转换,但如果它执行了,那么我预计转换将非常昂贵.为了公平起见,您应该在两个代码中使用相同的类型(可能是两个代码中的64位整数).

欲了解更多信息,请阅读相关文章:

Python相关问答推荐

在函数内部使用eval(),将函数的输入作为字符串的一部分

如何在Deliveryter笔记本中从同步上下文正确地安排和等待Delivercio代码中的结果?

如果条件为真,则Groupby.mean()

Python json.转储包含一些UTF-8字符的二元组,要么失败,要么转换它们.我希望编码字符按原样保留

发生异常:TclMessage命令名称无效.!listbox"

实现自定义QWidgets作为QTimeEdit的弹出窗口

创建可序列化数据模型的最佳方法

将pandas导出到CSV数据,但在此之前,将日期按最小到最大排序

如何禁用FastAPI应用程序的Swagger UI autodoc中的application/json?

根据客户端是否正在传输响应来更改基于Flask的API的行为

什么是一种快速而优雅的方式来转换一个包含一串重复的列,而不对同一个值多次运行转换,

获取PANDA GROUP BY转换中的组的名称

使用polars. pivot()旋转一个框架(类似于R中的pivot_longer)

为什么后跟inplace方法的`.rename(Columns={';b';:';b';},Copy=False)`没有更新原始数据帧?

Django在一个不是ForeignKey的字段上加入'

EST格式的Azure数据库笔记本中的当前时间戳

在MongoDB文档中仅返回数组字段

Pandas:使列中的列表大小与另一列中的列表大小相同

来自任务调度程序的作为系统的Python文件

根据边界点的属性将图划分为子图