Numpy中的向量化字符串操作:为什么它们比较慢?

发布于 2021-01-29 16:15:01

这是那些“出于纯粹的好奇心而提出的(可能徒劳的希望我会学到一些东西)”的问题。

我正在研究在大量字符串上节省内存的方法,在 某些
情况下,看起来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

关注者
0
被浏览
45
1 个回答
  • 面试哥
    面试哥 2021-01-29
    为面试而生,有面试问题,就找面试哥。

    在谈论时,有两种方式使用Vectorized numpy,但并不总是清楚这是什么意思。

    1. 对数组的所有元素进行运算的运算
    2. 在内部调用优化的(在许多情况下为多线程)数字代码的操作

    第二点是使向量化操作比python中的for循环快得多的原因,而多线程部分是使向量化操作比列表理解要快的原因。当这里的评论者指出矢量化代码更快时,他们也指的是第二种情况。但是,在numpy文档中,矢量化仅指第一种情况。这意味着您可以直接在数组上使用函数,而不必遍历所有元素并在每个元素上调用它。从这个意义上讲,它使代码更简洁,但不一定更快。一些矢量化操作确实会调用多线程代码,但据我所知,这仅限于线性代数例程。就个人而言,我更喜欢使用向量化运算,因为即使性能相同,我也认为它比列表理解更具可读性。

    现在,对于有问题的代码,其文档np.char(是的别名np.core.defchararray)指出

    chararray存在是为了与Numarray向后兼容类,所以不推荐在新的发展。从numpy
    1.4开始,如果需要字符串数组,建议使用dtype object_string_或数组
    unicode_,并使用numpy.char模块中的free函数进行快速矢量化字符串操作。

    因此,有四种方法(不建议一种)来处理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)
    

    这将创建具有以下数据类型的数组(或后两个字符数组):

    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字符串会严重影响性能。该文档的状态为这些类型之间的差异以下

    为了与Python
    2向后兼容,Sand和atypestrings保留以零结尾的字节,并且np.string_继续映射到np.bytes_。要在Python
    3中使用实际的字符串,请使用U或np.unicode_。对于不需要零终止的带符号字节,可以使用b或i1。

    在这种情况下,如果字符集不需要unicode,则可以使用更快的string_类型。如果需要unicode,则可以通过使用列表或使用numpy类型的数组(object_如果需要其他numpy功能)来获得更好的性能。列表何时可能更好的另一个很好的例子是附加大量数据

    因此,请注意以下几点:

    1. Python虽然被普遍认为是缓慢的,但在某些常见的事情上却表现出色。Numpy通常相当快,但并未针对所有内容进行优化。
    2. 阅读文档。如果做事的方法不只一种(通常是这样),那么对您尝试做的事情来说,一种可能性更好。
    3. 不要盲目地认为矢量化代码会更快-在关注性能时始终进行概要分析(适用于所有“优化”技巧)。


知识点
面圈网VIP题库

面圈网VIP题库全新上线,海量真题题库资源。 90大类考试,超10万份考试真题开放下载啦

去下载看看