【问题标题】:Vectorized string operations in Numpy: why are they rather slow?Numpy 中的向量化字符串操作:为什么它们相当慢?
【发布时间】:2026-02-03 22:10:01
【问题描述】:

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

我正在研究在大量字符串的操作上节省内存的方法,对于一些场景,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 文档页面所述),并且 numpy 字符串数组显然使用更少的内存,因此对它们的操作至少应该是更多的 CPU 缓存-友好,字符串数组的性能至少与纯 Python 中的性能相似?

环境:

Python 3.6.3 x64,Linux

numpy==1.14.1

【问题讨论】:

  • 评论:我通常怀疑在测试和证明之前操作被“矢量化”的说法。 2 个示例:np.vectorize(比map 替换稍多)和pd.Series.str(源代码只是一个循环的lambda)。
  • 看看source code(函数_vec_string_vec_string_no_args,...)似乎没有矢量化魔法,只是循环(不确定你可以矢量化字符串操作反正)。我不认为字符串连续存储在数组中,因为它们可以有不同的大小,我猜它们的引用是。我想列表推导的开销比创建 NumPy 数组的开销要小,而且字符串操作本身也需要大约相同的时间。
  • 时间表现似乎取决于 Python 的版本。这可能是针对特定版本的优化。
  • 字符串存储在连续的数组数据缓冲区中,如果需要,可以使用填充。但是 numpy 使用 Python 字符串方法来处理它们。它没有实现自己编译的字符代码,也就是我们通常所说的真向量化。 defchararray 对于已经是字符串 dtype 的数组来说是一种便利,而不是处理字符串列表的替代品。从文档中,“所有这些都基于 Python 标准库中的字符串方法”
  • @hpaulj - 好像是答案 ;)

标签: python numpy benchmarking


【解决方案1】:

在谈到 numpy 时,Vectorized 有两种用法,但并不总是很清楚是什么意思。

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

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

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

chararray 类的存在是为了向后兼容 Numarray,不建议用于新开发。从 numpy 开始 1.4、如果需要字符串数组,建议使用数组 dtypeobject_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)

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

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,您可以使用列表或 object_ 类型的 numpy 数组(如果需要其他 numpy 功能)来获得更好的性能。 另一个更好的列表示例是appending lots of data

所以,从这里的要点:

  1. Python 虽然通常被认为很慢,但对于一些常见的事情来说却非常高效。 Numpy 通常很快,但并未针对所有内容进行优化。
  2. 阅读文档。如果有不止一种做事的方式(通常有),很可能有一种方式更适合您尝试做的事情。
  3. 不要盲目假设矢量化代码会更快 - 当您关心性能时,请始终进行分析(这适用于任何“优化”技巧)。

【讨论】: