Cython 中数组的总和

2023-12-12

我正在尝试找到使用 Cython 水平求和 numpy 数组数组的最快方法。首先,假设我有一个随机浮点数 10 x 100,000 的二维数组。我可以创建一个object数组,每一列作为数组中的值,如下所示:

n = 10 ** 5
a = np.random.rand(10, n)
a_obj = np.empty(n, dtype='O')
for i in range(n):
    a_obj[i] = a[:, i]

我想做的就是找到每一行的总和。它们都可以简单地计算如下:

%timeit a.sum(1)
414 µs ± 24.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

%timeit a_obj.sum()
113 ms ± 7.01 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

对象数组慢 250 倍。

在尝试总结之前,我想对每个元素的访问进行计时。当直接迭代每个项目时,Cython 无法加速对对象数组的每个成员的访问:

def access_obj(ndarray[object] a):
    cdef int i
    cdef int nc = len(a)
    cdef int nr = len(a[0])
    for i in range(nc):
        for j in range(nr):
            a[i][j]

%timeit access_obj(a_obj)
42.1 ms ± 665 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

但是,我相信我可以使用以下命令访问底层 C 数组data属性是memoryview对象并获得更快的访问:

def access_obj_data(ndarray[object] a):
    cdef int i
    cdef int nc = len(a)
    cdef int nr = len(a[0])
    cdef double **data = <double**>a.data
    for i in range(nc):
        for j in range(nr):
            data[i][j]

我认为这会在计时时缓存结果,所以我必须执行一次。

%timeit -n 1 -r 1 access_obj_data(a_obj)
8.17 µs ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)

问题是你不能直接投射a.data转换为 C 语言的双精度数组。如果我打印出第一列,我会得到以下内容。

5e-324
2.1821467023e-314
2.2428810855e-314
2.1219957915e-314
6.94809615162997e-310
6.94809615163037e-310
2.2772194067e-314
2.182150145e-314
2.1219964234e-314
0.0

有类似的问题,但没有明确回答使用有效代码快速执行此操作的方法。


我想有两个不同的问题:

  1. Why is a_obj.sum()太慢了?
  2. 为什么 cython 函数代码很慢?

我们将看到缓慢的a_obj.sum()可以跟踪Python调度+缓存未命中的开销。然而,这是一个相当多余的分析结果,可能还有更微妙的原因。

第二个问题不太有趣:我们会看到,Cython 对数组中的对象了解不够,无法加快函数速度。

还有一个问题“最快的方法是什么?”。答案是“这取决于您的数据和硬件”,因为几乎总是针对此类问题。

但是,我们将利用获得的见解对现有版本进行轻微改进。


让我们从“a_obj.sum()是慢的”。

作为我的机器上的示例的基线(是的,因素200):

>>> %timeit a.sum(1)
510 µs ± 14.6 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
>>> %timeit a_obj.sum()
90.3 ms ± 494 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

首先发生了什么a_obj.sum()?缩减算法如下:

result=a_obj[0]+a_obj[1]+a_obj[2]+...

然而,算法不知道a_obj[i]是 numpy 数组,所以它必须使用 Python 调度来知道,a_obj[i]+a_obj[i+1]实际上是两个 numpy 数组的加法。总结起来这是相当大的开销10我们必须支付的费用加倍10**5次。我回答了一个question前段时间关于Python-dispatch的成本,如果你对更多细节感兴趣的话。

如果我们的数组的形状是10**5x10,我们只需支付间接费用10与工作量相比,这并不大10**5补充:

>>> b=np.random.rand(n,10)
>>> b_obj=to_obj_array(b) # see listing at the end for code
>>> %timeit b.sum(1)
2.43 ms ± 13.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)    
>>> %timeit b_obj.sum()
5.53 ms ± 17.1 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

正如您所看到的,现在因子(大约 2)小得多。然而,Python 调度开销理论并不能解释一切。

让我们看一下以下数组的大小:

>>> c=np.random.rand(10**4,10**4)
>>> c_obj=to_obj_array(c)
>>> %timeit c.sum(1)
52.5 ms ± 354 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
>>> %timeit c_obj.sum()
369 ms ± 2.53 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

这次因子约为 8!原因是 obj 版本中的缓存未命中。这是一项内存密集型任务,因此基本上内存速度是瓶颈。

在 numpy 数组中,元素一行接一行地连续存储在内存中。每次从内存中获取双精度值时,都会使用 7 个相邻的双精度值获取,并且这 8 个双精度值保存在缓存中。

So when c.sum(1)运行时需要逐行获取信息:每次从内存中将双精度值提取到缓存中时,都会预取接下来的 7 个数字,以便对它们进行求和。这是最有效的内存访问模式。

对象版本不同。这些信息需要按列进行。所以每次我们获取额外的7将双精度数据放入缓存中,我们还不需要它,当我们可以处理这些双精度数据时,它们已经从缓存中逐出(因为缓存不足以容纳列中的所有数字),并且需要从慢速数据中获取再次记忆。

这意味着使用第二种方法,每个 double 将从内存中读取 8 次,或者从内存/缓存未命中的读取次数将增加 8 倍。

我们可以使用 valgrind 轻松验证缓存研磨测量两个变体的缓存未命中(请参阅 test_cache.py` 末尾的列表):

valgrind --tool=cachegrind python test_cache.py array
...
D1  misses:       144,246,083 
...

valgrind --tool=cachegrind python test_cache.py object
...
D1  misses:     1,285,664,578
...

i.e c_obj-version 的 D1 缓存未命中次数大约是原来的 8 倍。


但还有更多乐趣,对象版本可以更快!

>>> d=np.random.rand(800,6)
>>> d_obj=to_obj_array(d)
>>> %timeit d.sum(1)
20.2 µs ± 217 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
>>> %timeit d_obj.sum()
12.4 µs ± 155 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

怎么可能呢?它们可能很快相似,因为

  1. 整个数组可以保存在缓存中,因此不会出现额外的缓存未命中情况。
  2. python-dispatch 的开销是可以承受的。

但对于其他较慢的版本来说,速度要快两倍?

我不是 100% 确定,但这是我的理论:对于数组版本,我们计算行的总和i:

...
sum_i=sum_i+a[i][j]
sum_i=sum_i+a[i][j+1]
sum_i=sum_i+a[i][j+2]
...

因此硬件无法并行执行指令,因为需要前一个操作的结果来计算下一个操作。

然而对于对象版本,计算如下:

...
sum_i=sum_i+a[i][j]
sum_k=sum_k+a[k][j]
sum_m=sum_m+a[i][j]
...

这些步骤可以由 CPU 并行完成,因为它们之间不存在依赖关系。


那么,为什么 cython 这么慢?因为 cython 对数组中的对象一无所知,所以它并不比通常的 Python 代码快,后者非常慢!

让我们分解一下 cython 函数access_obj:

  1. Cython 一无所知a[i]并假设它是一个通用的 Python 对象。
  2. 这意味着,对于a[i][j]它必须调用__get_item__ of a[i]在Python基础设施的帮助下。顺便提一句。为了那个原因j必须转换为完整的 Python 整数。
  3. __get_item__从存储的 c-double 构造一个成熟的 Python-float 并将其返回给 cython。
  4. 强制转换将 Python-float 返回为 c-float。

在底线下方,我们使用了 Python 调度并创建了两个临时 Python 对象 - 您可以看到所需的成本。


加快求和速度

我们已经看到,求和是一个内存限制任务(至少在我的机器上),所以最重要的是避免缓存未命中(这一点也可能意味着,例如,我们需要不同的 C 算法)阶和 F 阶 numpy 数组)。

然而,如果我们的任务必须与 CPU 密集型任务共享一个 CPU,它可能会再次成为事实上的 CPU 密集型任务,因此它有一些优点,可以不仅仅关注现金缺失。

在稍后将介绍的 C 版本中,我们希望结合这两种方法的良好特性:

  1. 与对象版本类似,我们希望并行计算多个行总和,以便能够使用 CPU 固有的并行性。
  2. 与 numpy-array-sum 类似,我们希望缓存未命中次数最少,因此我们不会同时计算所有行,而只计算一小块(例如 8)。这种按块计算的优点是,一旦缓存的数据在真正需要之前就不会从缓存中逐出。

下面是一个简化版本,它以 8 行为一组处理行:

//fast.c
#define MAX 8
void sum_rows_blocks(double *orig, double *out, int n_rows, int n_cols){
   int n_blocks=n_rows/MAX;
   for (int b=0; b<n_blocks; b++){   //for every block
       double res[MAX]={0.0};        //initialize to 0.0
       for(int j=0;j<n_cols;j++){
           for(int i=0;i<MAX;i++){   //calculate sum for MAX-rows simultaniously
              res[i]+=orig[(b*MAX+i)*n_cols+j];
           }
       }
       for(int i=0;i<MAX;i++){
             out[b*MAX+i]=res[i];
       }
   }  
   //left_overs:
   int left=n_rows-n_blocks*MAX;
   double res[MAX]={0.0};         //initialize to 0.0
   for(int j=0;j<n_cols;j++){
       for(int i=0;i<left;i++){   //calculate sum for left rows simultaniously
              res[i]+=orig[(n_blocks*MAX+i)*n_cols+j];
       }
   }
   for(int i=0;i<left;i++){
         out[n_blocks*MAX+i]=res[i];
   }
}

现在我们可以使用 cython 来包装这个函数:

//c_sum.pyx
cimport numpy as np    
import numpy as np

cdef extern from "fast.c":
    void sum_rows_blocks(double *orig, double *out, int n_rows, int n_cols)

def sum_rows_using_blocks(double[:,:] arr):
    cdef int n_rows
    cdef int n_cols
    cdef np.ndarray[np.double_t] res
    n_rows=arr.shape[0] 
    n_cols=arr.shape[1]
    res=np.empty((n_rows, ),'d')
    sum_rows_blocks(&arr[0,0], &res[0], n_rows, n_cols)
    return res

构建扩展后(请参阅清单setup.py),我们可以进行比较:

import c_sum
%timeit c_sum.sum_rows_using_blocks(d) #shape=(800,6)
4.26 µs ± 95.8 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

它因素3比迄今为止最快的版本(12 µs)还要快。对于大型阵列来说效果如何:

%timeit c_sum.sum_rows_using_blocks(c) #shape=(10**4,10**4)
49.7 ms ± 600 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

不错,比之前稍微快一点c.sum(1)这需要52ms,这意味着我们没有额外的缓存未命中。

可以改进吗?我确实这么认为:例如不使用默认编译标志-fwrapv(通过包括extra_compile_args = ["-fno-wrapv"]在设置中)将改善结果assembly并导致形状求和加速 10%(800, 6).


清单:

def to_obj_array(a):   
     n=a.shape[1]
     obj=np.empty((n,),dtype='O')
     for i in range(n):
          obj[i]=a[:,i]
     return obj

check_cache.py:

import sys
import numpy as np

def to_obj_array(a):   
     n=a.shape[1]
     obj=np.empty((n,),dtype='O')
     for i in range(n):
          obj[i]=a[:,i]
     return obj

c=np.random.rand(10**4,10**4)
c_obj=to_obj_array(c)


for i in range(10):
  if sys.argv[1]=="array":
     c.sum(1)
  elif sys.argv[1]=="object":
     c_obj.sum()
  else:
     raise Exception("unknown parameter")

setup.py:

#setup.py
from distutils.core import setup, Extension
from Cython.Build import cythonize
import numpy as np

setup(ext_modules=cythonize(Extension(
            name='c_sum',
            sources = ["c_sum.pyx"],
            #extra_link_args = ["fast.o"],
            include_dirs=[np.get_include()]

    )))
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Cython 中数组的总和 的相关文章

  • 以类型化内存视图作为成员的结构定义

    目前我正在尝试让一个具有类型化内存视图的结构能够工作 例如 ctypedef struct node unsigned int inds 如果 inds 不是内存视图 据我所知 它可以完美地工作 然而 通过内存视图并使用类似的东西 def
  • numpy:如何连接数组? (获得多个范围的并集)

    我使用Pythonnumpy 我有一个 numpy 索引数组a gt gt gt a array 5 7 12 18 20 29 gt gt gt type a
  • 无法在 PyCharm 版本 9.3.3 中安装 NumPy。 Python版本3.8.2

    在 PyCharm 中安装 NumPy 时出错 尝试安装 Microsoft Visual C 14 0 还是行不通 NumPy 正在通过命令安装pip3 install numpy在 cmd 终端中 但是当尝试将其安装在 PyCharm
  • 高效创建抗锯齿圆形蒙版

    我正在尝试创建抗锯齿 加权而不是布尔 圆形掩模 以制作用于卷积的圆形内核 radius 3 no of pixels to be 1 on either side of the center pixel shall be decimal a
  • Pandas DataFrame 具有 X、Y 坐标到 NumPy 矩阵

    我有一个包含列的 DataFrameX Y and value e g X Y value 1 1 56 2 1 13 3 1 25 1 2 7 2 2 18 1 123 91 50 123 32 我需要将其转换为 DataFrame 到
  • Numpy uint8_t 数组到 vtkImageData

    我正在尝试拍摄一个或三个通道的 2D 图像并使用 VTK 中显示它们vtkImageActor 据我了解 要显示的当前帧可以通过调用来更新SetImageData on vtkImageActor并提供一个实例vtkImageData 我已
  • 如何修复 TypeError: G 必须是 'd' 矩阵?

    目标 尝试通过优化过程运行玩具数据集 我遇到以下错误 TypeError Traceback most recent call last
  • 使用步幅沿轴填充每个切片上的对角线

    考虑 numpy 数组a a np arange 18 reshape 2 3 3 print a 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 我想沿着每个切片的对角线填充axis 0我使用以下方
  • 来自 io.BytesIO 流的 numpy.load

    我将 numpy 数组保存在 Azure Blob 存储中 并将它们加载到如下所示的流中 stream io BytesIO store get blob to stream container cat npy stream 我知道从str
  • 独立滚动矩阵的行

    我有一个矩阵 准确地说 是 2d numpy ndarray A np array 4 0 0 1 2 3 0 0 5 我想滚动每一行A根据另一个数组中的滚动值独立地 r np array 2 0 1 也就是说 我想这样做 print np
  • Scipy Sparse:SciPy/NumPy 更新后出现奇异矩阵警告

    我的问题是由大型电阻器系统的节点分析产生的 我基本上是在设置一个大的稀疏矩阵A 我的解向量b 我正在尝试求解线性方程A x b 为了做到这一点 我正在使用scipy sparse linalg spsolve method 直到最近 一切都
  • 在 python 中使用 pandas 计算行的出现次数

    我有一个包含数千行和 4 列的 pandas 数据框 IE A B C D 1 1 2 0 3 3 2 1 3 1 1 0 有没有办法统计某一行出现了多少次 例如 可以找到多少次 3 1 1 0 并返回这些行的索引 如果你只寻找一行 那么我
  • 通过 python 中的另外两个修改数组[重复]

    这个问题在这里已经有答案了 假设我们有三个一维数组 A 长度为 5 B 长度相同 示例中为5 C 更长 比如长度为 100 C最初用零填充 A给出索引C应更改的元素 它们可能会重复 以及B给出应添加到初始零的值C 例如 如果A 1 3 3
  • 如何在 Python 中使用 PIL\Numpy 获取灰度图像的平均像素值?

    我有很少的灰度图像 我想计算整个图像的平均像素值 这样我就可以使用单个值来表示每个单独的图像 如果你想做这样的事情 你应该考虑使用scikit image而不是原始的 PIL 或枕头 SciKit Image 使用 numpy 数组来存储图
  • 从零开始的 numpy 形状意味着什么

    好的 我发现数组的形状中可以包含 0 对于将 0 作为唯一维度的情况 这对我来说是有意义的 它是一个空数组 np zeros 0 但如果你有这样的情况 np zeros 0 100 让我很困惑 为什么这么定义呢 据我所知 这只是表达空数组的
  • 如何在 OSX 上安装 numpy 和 scipy?

    我是 Mac 新手 请耐心等待 我现在使用的是雪豹 10 6 4 我想安装numpy和scipy 所以我从他们的官方网站下载了python2 6 numpy和scipy dmg文件 但是 我在导入 numpy 时遇到问题 Library F
  • python pandas从0/1数据帧到项目集列表

    从这种形式的 0 1 pandas numpy 数据帧中最有效的方法是什么 gt gt gt dd a 0 1 1 0 2 1 3 0 4 1 5 1 b 0 1 1 1 2 0 3 0 4 1 5 1 c 0 0 1 1 2 1 3 0
  • 从 NumPy 数组到 Mat 的 C++ 转换 (OpenCV)

    我正在围绕 ArUco 增强现实库 基于 OpenCV 编写一个薄包装器 我试图构建的界面非常简单 Python 将图像传递给 C 代码 C 代码检测标记并将其位置和其他信息作为字典元组返回给 Python 但是 我不知道如何在 Pytho
  • 2D Numpy 数组花式索引 + 掩码

    I have import numpy as np a np array 4 99 2 3 4 99 1 8 7 8 6 8 Why is a True True False False 1 2 等于 array 99 99 And not
  • pandas 相当于 np.where

    np where具有向量化 if else 的语义 类似于 Apache Spark 的when otherwise数据帧方法 我知道我可以使用np where on pandas Series but pandas通常定义自己的 API

随机推荐

  • WPF UserControl 不继承父 DataContext

    我正在尝试开发一个可重用的用户控件 但遇到了绑定问题 我创建了一个较小的应用程序来测试它 但无法解决它 或者至少无法理解为什么它没有按照我的预期工作 代码如下 我期望的是我放在 MainWindow xaml 上的 TestUserCont
  • 转换颜色以模仿灰度打印

    读书时这个问题 我开始思考是否可以转换颜色来模仿普通的灰度打印机 假设您的屏幕已校准 找到一个可认可的近似值可以节省纸张 例如 如何转换这些颜色 看看在纸上是否可以区分浅蓝色和深蓝色和红色 temp lt rgb2hsv 239 138 9
  • 使用accepts_nested_attributes_for创建新记录或更新现有记录

    阅读重大更新以获取最新信息 嘿大家 我在 Rails 应用程序中有一个多对多关系 涉及三个表 用户表 兴趣表和连接 user interests 表 该表也有一个评级值 以便用户可以对他们的每个兴趣进行评级1 10 级 我基本上是在寻找一种
  • EF Core 5.0 - 更新 ASP.NET Core Web API 中的多对多实体

    EF Core 5 0 引入了多对多关系 我陷入了如何通过我的 asp net api 更新它们的困境 对于一对一和一对多关系 有一个约定 只需添加属性名称后跟 ID public class Blog public int BlogId
  • 找不到或无法加载程序集 mscorlib.dll

    首先 我见过这个问题 虽然问题看起来相似 但其实并不相同 我正在运行一个精简的单声道 没有使用 4 5 配置文件构建 configure with profile4 yes with profile4 5 no 我有一个针对 NET 4 0
  • Groupby 与 TimeGrouper“向后”

    我有一个DataFrame包含时间序列 rng pd date range 2016 06 01 periods 24 7 freq H ones pd Series 1 24 7 rng rdf pd DataFrame a ones 最
  • 从透明形式的图像中删除轮廓

    我正在尝试将窗口形式转换为透明 并使其仅显示一个对象 但它在我的对象周围仍然有一条线 描边 它并不像我想要的那么完美 如何取出线条 笔画 附上图片对比一下 这是我的代码 private void Form1 Load object send
  • 动态分配 UI 权限 WPF [重复]

    这个问题在这里已经有答案了 可能的重复 在代码隐藏中设置 WPF UI 权限 我开始使用 WPF 并希望创建一个应用程序 该应用程序将根据用户 AD 及其角色 自定义 显示 隐藏控件 我设法通过使用继承 MarkupExpension 和
  • 如何将 Eclipse 和 Eclipse 项目指向 Eclipse 中的新 JRE 版本? (不使用JAVA_HOME)

    设置场景 我的系统上有两个版本的 Java 有 32 位版本和 64 位版本 我拥有的另一个版本是 64 位版本的 Eclipse Java EE MyEclipse 32位版本 指的是32位版本的JDK 我现在尝试将 Eclipse 32
  • 当使用浏览器返回时,如何保留 jquery 删除的一些输入文本?

    我对以下页面有一些错误 http jsbin com agedu 带有一些注释的源代码 http jsbin com agedu edit 问题是 当输入内容并执行查询以显示搜索结果时 如果我返回浏览器中的搜索页面 Firefox 3 5
  • rdtscp、rdtsc 之间的区别:内存和 cpuid/rdtsc?

    假设我们尝试使用 tsc 进行性能监控 并且我们希望防止指令重新排序 这些是我们的选择 1 rdtscp是一个序列化调用 它可以防止对 rdtscp 调用进行重新排序 asm volatile rdtscp serializing read
  • Java:如何在抛出异常后继续读取文件

    所以我的教授给我们分配了一个项目 我们必须从文本文件中获取命令并使用它们来驱动程序的流程 这些命令 例如起飞 着陆 装载货物 卸载货物等 旨在模拟类似飞机的物体 有时这些命令执行起来没有意义 例如在飞机飞行时装载货物 因此 为了防止发生类似
  • 是否可以在不使用 EDT 的情况下在 Java Swing 中执行主动渲染?

    我正在考虑使用缓冲策略以及 Javadoc 中描述的以下技术 Main loop while done Prepare for rendering the next frame Render single frame do The foll
  • 我可以使用 Java API 将图像文件存储在 firebase 中吗?

    有没有什么方法可以使用Java api将图像文件存储在firebase中以用于android应用程序 我已阅读此主题我可以使用 Java API 将图像文件存储在 firebase 中吗但仍然没有答案 我知道有一个官方 api 名为 fir
  • TFrecord 比原始 JPEG 图像占用更多空间

    我正在尝试将 Jpeg 图像集转换为 TFrecords 但 TFrecord 文件占用的空间几乎是图像集的 5 倍 经过大量谷歌搜索后 我了解到当 JPEG 被写入 TFrecords 时 它们就不再是 JPEG 了 但是我还没有遇到这个
  • 如何子类化 matplotlib 的图形类?

    我正在尝试向我的图形添加一些自定义行为和属性 但我无法决定有效的 和Pythonic 方法 我的第一个冲动是简单地子类化matplotlib figure Figure但我不知道如何实现这一点 我通常创建新的图形并用类似的东西开始我的绘图
  • .gitattributes:merge=我们的策略与快进合并

    如果我处于这样的 git 情况 da6a750 A Further in A okay for merging back into master bf27b58 Merge branch master into A 86294d1 HEAD
  • 无法使用沙盒帐户

    我真的需要你们的帮助 两个小时以来 我遇到了一个与 Paypal Sandbox 相关的非常奇怪的问题 我在堆栈溢出上阅读了很多答案 但没有一个对我有帮助 我将尝试解释我的问题 当我创建一个新的沙箱帐户 尊重密码强度 负载平衡等所有规则 时
  • 序列化数组以将它们存储在数据库中的意义是什么?

    我看到人们存储数组的方式如下 a 6 i 0 s 5 11148 i 1 s 5 11149 i 2 s 5 11150 i 3 s 5 11153 i 4 s 5 11152 i 5 s 5 11160 为什么他们不能是 11148 11
  • Cython 中数组的总和

    我正在尝试找到使用 Cython 水平求和 numpy 数组数组的最快方法 首先 假设我有一个随机浮点数 10 x 100 000 的二维数组 我可以创建一个object数组 每一列作为数组中的值 如下所示 n 10 5 a np rand