在Cython与NumPy中总和int和浮点数时的性能差异很大

时间:2018-03-20 16:08:45

标签: python numpy cython

我使用Cython或NumPy对一维数组中的每个元素求和。当总和整数时,Cython的速度提高约20%。求和浮点数时,Cython约为2.5x 。以下是使用的两个简单函数。

#cython: boundscheck=False
#cython: wraparound=False

def sum_int(ndarray[np.int64_t] a):
    cdef:
        Py_ssize_t i, n = len(a)
        np.int64_t total = 0

    for i in range(n):
        total += a[i]
    return total 

def sum_float(ndarray[np.float64_t] a):
    cdef:
        Py_ssize_t i, n = len(a)
        np.float64_t total = 0

    for i in range(n):
        total += a[i]
    return total

计时

创建两个每个包含100万个元素的数组:

a_int = np.random.randint(0, 100, 10**6)
a_float = np.random.rand(10**6)

%timeit sum_int(a_int)
394 µs ± 30 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

%timeit a_int.sum()
490 µs ± 34.2 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

%timeit sum_float(a_float)
982 µs ± 10.8 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

%timeit a_float.sum()
383 µs ± 4.42 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

其他要点

  • NumPy的表现优于(通过相当大的差距)浮动,甚至超过自己的整数。
  • sum_float的效果差异与缺失的boundscheckwraparound指令相同。为什么?
  • sum_int中的整数numpy数组转换为C指针(np.int64_t *arr = <np.int64_t *> a.data)可将性能提高25%。这样做的花车什么都没做

主要问题

如何在Cython中使用浮点运算获得与整数相同的性能?

编辑 - 只是计数很慢?!?

我写了一个更简单的函数,只计算迭代次数。第一个将计数存储为int,后者为double。

def count_int():
    cdef:
        Py_ssize_t i, n = 1000000
        int ct=0

    for i in range(n):
        ct += 1
    return ct

def count_double():
    cdef:
        Py_ssize_t i, n = 1000000
        double ct=0

    for i in range(n):
        ct += 1
    return ct

计算时间

我只运行了一次(害怕缓存)。不知道循环是否实际上正在为整数执行,但count_double具有与上面的sum_float相同的相同性能。这很疯狂......

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

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

1 个答案:

答案 0 :(得分:12)

我不会回答你所有的问题,但只会(在我看来)是最有趣的问题。

让我们从您的计数示例开始:

  1. 编译器能够在整数情况下优化for循环 - 生成的二进制文件不会计算任何东西 - 它只能返回编译阶段预先计算的值。
  2. 对于双重情况​​不是这种情况,因为由于舍入错误,结果将不是1.0*10**6并且因为cython按IEEE 754(非-ffast-math)模式编译默认值。
  3. 在查看你的cython代码时你必须记住这一点:编译器不允许重新排列摘要(IEEE 754),并且因为下一个只需要一个长行,所以需要第一个求和的结果所有操作都在等待。

    但最重要的见解是:numpy与你的cython代码不一样:

    >>> sum_float(a_float)-a_float.sum()
    2.9103830456733704e-08
    

    是的,没有人告诉numpy(与你的cython代码不同),总和必须像这样计算

    ((((a_1+a2)+a3)+a4)+...
    

    numpy以两种方式利用它:

    1. 它会执行pairwise summation(种类),这会导致较小的舍入错误。

    2. 它以块的形式计算总和(python的代码有点难以理解,这里是corresponding template并且在使用的函数pairwise_sum_DOUBLE的列表的下方

      < / LI>

      第二点是您观察加速的原因,计算类似于以下架构(至少我从下面的源代码中理解):

      a1  + a9 + .....  = r1 
      a2  + a10 + ..... = r2
      ..
      a8  + a16 +       = r8
      
      ----> sum=r1+....+r8
      

      这种求和的优点:a2+a10的结果不依赖于a1+a9,这两个值可以在现代CPU上同时计算(例如pipelining),导致你正在观察的加速。

      对于它的价值,在我的机器上,cython-integer-sum比numpy慢。

      需要考虑numpy-array的步幅(仅在运行时知道,另请参阅this question关于向量化)阻止了一些优化。解决方法是使用内存视图,您可以清楚地表明数据是连续的,即:

      def sum_int_cont(np.int64_t[::1] a):
      

      这导致我的机器显着加速(因子2):

      %timeit sum_int(a_int)
      2.64 ms ± 46.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
      
      %timeit sum_int_cont(a_int)
      1.31 ms ± 19 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
      
      %timeit a_int.sum()
      2.1 ms ± 105 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
      

      确实,在这种情况下,使用双精度的内存视图不会带来任何加速(不知道为什么),但一般来说它简化了优化器的使用寿命。例如,将memory-view-variant与-ffast-math编译选项结合起来,这将允许关联性,从而产生与numpy相当的性能:

      %%cython -c=-ffast-math
      cimport numpy as np
      def sum_float_cont(np.float64_t[::1] a):
          cdef:
              Py_ssize_t i, n = len(a)
              np.float64_t total = 0
      
          for i in range(n):
              total += a[i]
          return total
      

      现在:

      >>> %timeit sum_float(a_float)
      3.46 ms ± 226 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
      >>> %timeit sum_float_cont(a_float)
      1.87 ms ± 44 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
      >>> %timeit a_float.sum()
      1.41 ms ± 88.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
      

      pairwise_sum_DOUBLE的列表:

      /*
       * Pairwise summation, rounding error O(lg n) instead of O(n).
       * The recursion depth is O(lg n) as well.
       * when updating also update similar complex floats summation
       */
      static npy_double
      pairwise_sum_DOUBLE(npy_double *a, npy_uintp n, npy_intp stride)
      {
          if (n < 8) {
              npy_intp i;
              npy_double res = 0.;
              for (i = 0; i < n; i++) {
                  res += (a[i * stride]);
              }
              return res;
          }
          else if (n <= PW_BLOCKSIZE) {
              npy_intp i;
              npy_double r[8], res;
      
              /*
               * sum a block with 8 accumulators
               * 8 times unroll reduces blocksize to 16 and allows vectorization with
               * avx without changing summation ordering
               */
              r[0] = (a[0 * stride]);
              r[1] = (a[1 * stride]);
              r[2] = (a[2 * stride]);
              r[3] = (a[3 * stride]);
              r[4] = (a[4 * stride]);
              r[5] = (a[5 * stride]);
              r[6] = (a[6 * stride]);
              r[7] = (a[7 * stride]);
      
              for (i = 8; i < n - (n % 8); i += 8) {
                  r[0] += (a[(i + 0) * stride]);
                  r[1] += (a[(i + 1) * stride]);
                  r[2] += (a[(i + 2) * stride]);
                  r[3] += (a[(i + 3) * stride]);
                  r[4] += (a[(i + 4) * stride]);
                  r[5] += (a[(i + 5) * stride]);
                  r[6] += (a[(i + 6) * stride]);
                  r[7] += (a[(i + 7) * stride]);
              }
      
              /* accumulate now to avoid stack spills for single peel loop */
              res = ((r[0] + r[1]) + (r[2] + r[3])) +
                    ((r[4] + r[5]) + (r[6] + r[7]));
      
              /* do non multiple of 8 rest */
              for (; i < n; i++) {
                  res += (a[i * stride]);
              }
              return res;
          }
          else {
              /* divide by two but avoid non-multiples of unroll factor */
              npy_uintp n2 = n / 2;
              n2 -= n2 % 8;
              return pairwise_sum_DOUBLE(a, n2, stride) +
                     pairwise_sum_DOUBLE(a + n2 * stride, n - n2, stride);
          }
      }