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))
使用的内存消耗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
-
在谈论时,有两种方式使用Vectorized
numpy
,但并不总是清楚这是什么意思。- 对数组的所有元素进行运算的运算
- 在内部调用优化的(在许多情况下为多线程)数字代码的操作
第二点是使向量化操作比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向后兼容,S
and和a
typestrings保留以零结尾的字节,并且np.string_继续映射到np.bytes_。要在Python
3中使用实际的字符串,请使用U或np.unicode_。对于不需要零终止的带符号字节,可以使用b或i1。在这种情况下,如果字符集不需要unicode,则可以使用更快的
string_
类型。如果需要unicode,则可以通过使用列表或使用numpy类型的数组(object_
如果需要其他numpy功能)来获得更好的性能。列表何时可能更好的另一个很好的例子是附加大量数据因此,请注意以下几点:
- Python虽然被普遍认为是缓慢的,但在某些常见的事情上却表现出色。Numpy通常相当快,但并未针对所有内容进行优化。
- 阅读文档。如果做事的方法不只一种(通常是这样),那么对您尝试做的事情来说,一种可能性更好。
- 不要盲目地认为矢量化代码会更快-在关注性能时始终进行概要分析(适用于所有“优化”技巧)。