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

Let*_*t4U 4 python benchmarking numpy

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

我正在研究在大量字符串上节省内存的方法,在某些情况下,在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))
Run Code Online (Sandbox Code Playgroud)

使用的内存消耗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
Run Code Online (Sandbox Code Playgroud)

到目前为止,一切都很好; 但是,计时结果令我惊讶:

%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)
Run Code Online (Sandbox Code Playgroud)

这是为什么?我预计,由于Numpy对其数组使用连续的内存块,并且对其向量进行矢量化处理(如上述numpy doc页面所述),并且numpy字符串数组显然使用的内存更少,因此对其进行的操作至少应该在CPU上至少具有更多的高速缓存,友好,字符串数组的性能至少与纯Python中的相似?

环境:

Python 3.6.3 x64,Linux

numpy == 1.14.1

use*_*699 6

在谈论时,有两种方式使用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)
Run Code Online (Sandbox Code Playgroud)

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

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')
Run Code Online (Sandbox Code Playgroud)

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

%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)
Run Code Online (Sandbox Code Playgroud)

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

使用unicode字符串会严重影响性能。该文档的状态为这些类型之间的差异以下

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

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

因此,要点是:

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