Numpy中的矢量化字符串操作:为什么它们相当慢?

时间:2018-03-05 14:21:20

标签: python numpy benchmarking

这是那些"大多数是出于纯粹的好奇心(可能徒劳地希望我会学到一些东西)"的问题。

我正在调查在大量字符串操作上节省内存的方法,而对于某些场景,似乎string operations in numpy可能很有用。但是,我得到了一些令人惊讶的结果:

import random
import string

milstr = [''.join(random.choices(string.ascii_letters, k=10)) for _ in range(1000000)]

npmstr = np.array(milstr, dtype=np.dtype(np.unicode_, 1000000))

使用memory_profiler消耗内存:

%memit [x.upper() for x in milstr]
peak memory: 420.96 MiB, increment: 61.02 MiB

%memit np.core.defchararray.upper(npmstr)
peak memory: 391.48 MiB, increment: 31.52 MiB

到目前为止,这么好;然而,时间结果让我感到惊讶:

%timeit [x.upper() for x in milstr]
129 ms ± 926 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

%timeit np.core.defchararray.upper(npmstr)
373 ms ± 2.36 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

为什么?我预计,因为Numpy为其数组使用了连续的内存块,并且它的操作是矢量化的(正如上面的numpy doc页面所说)和numpy字符串数组显然使用较少的内存,因此对它们进行操作至少可能是更多的CPU缓存 - 友好,字符串数组的性能至少与纯Python相似?

环境:

Python 3.6.3 x64,Linux

numpy的== 1.14.1

1 个答案:

答案 0 :(得分:4)

在讨论numpy时,Vectorized以两种方式使用,而且并不总是清楚它是什么意思。

  1. 对阵列的所有元素进行操作的操作
  2. 内部调用优化(在许多情况下是多线程)数字代码的操作
  3. 第二点是使矢量化操作比python中的for循环快得多,而多线程部分使它们比列表理解更快。 当这里的评论者说明矢量化代码更快时,他们也指的是第二种情况。 但是,在numpy文档中,vectorized仅指第一种情况。 这意味着您可以直接在数组上使用函数,而无需遍历所有元素并在每个元素上调用它。 从这个意义上说,它使代码更简洁,但不一定更快。 一些矢量化操作会调用多线程代码,但据我所知,这仅限于线性代数例程。 就个人而言,我更喜欢使用矢量化操作系统,因为我认为它比列表推导更具可读性,即使性能相同。

    现在,对于有问题的代码,np.char的文档(np.core.defchararray的别名),状态

      

    chararray类的存在是为了向后兼容    Numarray,不建议用于新开发。从numpy开始    1.4,如果需要字符串数组,建议使用数组    dtype object_string_unicode_,并使用免费功能    在numpy.char模块中进行快速矢量化字符串操作。

    因此有四种方法(一种不推荐)来处理numpy中的字符串。 有些测试是有序的,因为每种方式肯定会有不同的优点和缺点。 使用如下定义的数组:

    npob = np.array(milstr, dtype=np.object_)
    npuni = np.array(milstr, dtype=np.unicode_)
    npstr = np.array(milstr, dtype=np.string_)
    npchar = npstr.view(np.chararray)
    npcharU = npuni.view(np.chararray)
    

    这将使用以下数据类型创建数组(或最后两个的chararrays):

    In [68]: npob.dtype
    Out[68]: dtype('O')
    
    In [69]: npuni.dtype
    Out[69]: dtype('<U10')
    
    In [70]: npstr.dtype
    Out[70]: dtype('S10')
    
    In [71]: npchar.dtype
    Out[71]: dtype('S10')
    
    In [72]: npcharU.dtype
    Out[72]: dtype('<U10')
    

    基准测试在这些数据类型中提供了相当大的性能范围:

    %timeit [x.upper() for x in test]
    %timeit np.char.upper(test)
    
    # test = milstr
    103 ms ± 1.42 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
    377 ms ± 3.67 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
    
    # test = npob
    110 ms ± 659 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
    <error on second test, vectorized operations don't work with object arrays>
    
    # test = npuni
    295 ms ± 1.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
    323 ms ± 1.05 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
    
    # test = npstr
    125 ms ± 2.52 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
    125 ms ± 483 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
    
    # test = npchar
    663 ms ± 4.94 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
    127 ms ± 1.27 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
    
    # test = npcharU
    887 ms ± 8.13 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
    325 ms ± 3.23 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
    

    令人惊讶的是,使用普通的旧字符串列表仍然是最快的。 当数据类型为string_object_时,Numpy具有竞争力,但一旦包含unicode,性能就会变得更糟。 chararray是迄今为止最慢,最慢的处理unicode。 应该清楚为什么不建议使用它。

    使用unicode字符串会显着降低性能。 docs表示以下这些类型之间的差异

      

    为了向后兼容Python 2,Sa类型字符串保持零终止字节,np.string_继续映射到np.bytes_。要在Python 3中使用实际字符串,请使用U或np.unicode_。对于不需要零终止的有符号字节,可以使用b或i1。

    在这种情况下,如果字符集不需要unicode,那么使用更快的string_类型是有意义的。 如果需要unicode,如果需要其他numpy功能,则可以通过使用列表或类型为object_的numpy数组来获得更好的性能。 列表可能更好的另一个好例子是appending lots of data

    所以,请注意:

    1. Python虽然通常被认为很慢,但对于一些常见的东西来说非常有效。 Numpy通常很快,但并未针对一切进行优化。
    2. 阅读文档。如果有不止一种做事方式(并且通常存在),那么对于你正在尝试做的事情,赔率是一种更好的方式。
    3. 不要盲目地假设矢量化代码会更快 - 在关注性能时总是进行分析(这适用于任何“优化”提示)。