【问题标题】:What could affect Python string comparison performance for strings over 64 characters?什么会影响超过 64 个字符的字符串的 Python 字符串比较性能?
【发布时间】:2012-09-28 22:08:12
【问题描述】:

我正在尝试评估比较两个字符串是否会随着长度的增加而变慢。我的计算表明比较字符串应该花费一个摊销的常数时间,但我的 Python 实验产生了奇怪的结果:

这是字符串长度(1 到 400)与时间(以毫秒为单位)的关系图。自动垃圾回收被禁用,gc.collect 在每次迭代之间运行。

我每次比较 100 万个随机字符串,计数匹配如下。该过程重复 50 次,然后取所有测量时间的最小值。

for index in range(COUNT):
    if v1[index] == v2[index]:
        matches += 1
    else:
        non_matches += 1

长度 64 左右突然增加的原因可能是什么?

注意:假设v1v2 是两个长度为n 的随机字符串列表,并且COUNT 是它们的长度,则可以使用以下sn-p 来尝试重现该问题.

timeit.timeit("for i in range(COUNT): v1[i] == v2[i]",
  "from __main__ import COUNT, v1, v2", number=50)

补充说明:我做了两个额外的测试:用is而不是==比较字符串完全抑制了问题,性能大约是210ms/1M比较。 由于提到了实习,我确保在每个字符串后添加一个空格,这应该可以防止实习;这不会改变任何事情。那除了实习还有别的事吗?

【问题讨论】:

  • 您可能应该包含 Python 的确切版本,以防万一它有所作为。
  • 由于字符串是随机的,比较过程几乎总是在第一个字符处停止。所以你最有可能看到的只是内存管理问题——新建它们,用随机内容填充它们等等。
  • @MikeDunlavey:Python 不会逐个字符地比较字符串 - 它使用字符串的哈希值来进行比较。
  • @Mike,我不是在计时,只是比较。
  • 我很难相信 y 轴是正确的。比较两个长度为 5 的字符串不应该花费 200 毫秒。在 2012 年 也许 微秒。在我的 Intel i7 CPU(64 位)上,它需要不到一纳秒的时间25 个字符(当它们匹配时,不会发生短路)。有些东西闻起来很腥......伙计们快跑吧:%time 'asdfsdsfsadfdsf' == 'asdfsdsfsadfdsf'

标签: python string performance time-complexity


【解决方案1】:

Python 可以 'intern' 短字符串;将它们存储在一个特殊的缓存中,并重新使用该缓存中的字符串对象。

然后比较字符串时,它会首先测试它是否是相同的指针(例如一个实习字符串):

if (a == b) {
    switch (op) {
    case Py_EQ:case Py_LE:case Py_GE:
        result = Py_True;
        goto out;
// ...

只有当指针比较失败时,它才会使用大小检查和memcmp 来比较字符串。

Interning 通常只发生在标识符(函数名、参数、属性等)上,但不适用于在运行时创建的字符串值。

另一个可能的罪魁祸首是字符串常量;代码中使用的字符串文字在编译时存储为常量并在整个过程中重复使用;再次只创建一个对象,并且对这些对象进行身份测试更快。

对于不相同的字符串对象,Python 测试相等的长度,相等的第一个字符,然后在内部 C 字符串上使用 memcmp() 函数。如果您的字符串没有被实习或以其他​​方式重用相同的对象,则所有其他速度特征都归结为memcmp() 函数。

【讨论】:

  • 我以为是这样,但我机器上的实习模式似乎非常不规则:重复 id('ss') 会得到相同的结果,但重复 id('ssssss') 每次都会得到不同的结果。重复id('sssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss') 也会再次返回相同的结果。你知道为什么吗?也许我应该把它作为一个单独的问题来问..
  • 在我的 Python 2.7.3 中,id('ss') 每次都会给出不同的结果,而长字符串上的 id('ssssss')id 始终返回相同的数字。
  • @larsmans 我也在 OS X 上的 python 2.7.3 上。我不确定“短字符串”是否被实习,而长字符串不是。因为id(LONG_STRING) 在某些长度上也会给我同样的结果。
  • 将字符串存储在一个变量中;实习并不总是让弦乐不朽;它将重新使用新字符串的内存地址。有时运行id('onestring') 然后id('anotherstring') 也会给出相同的内存地址。
  • @larsmans: _ 设置为id() 的返回值,而不是您调用它的字符串文字。 :-)
【解决方案2】:

我只是在胡乱猜测,但您问的是“什么可能”而不是什么,所以这里有一些可能性:

  • CPU 缓存行大小为 64 字节,较长的字符串会导致缓存未命中。
  • Python 可能会在一种结构中存储 64 字节的字符串,而在更复杂的结构中存储更长的字符串。
  • 与最后一个相关:它可以将字符串补零到 64 字节数组中,并且能够使用非常快速的 SSE2 向量指令来匹配两个字符串。

【讨论】:

  • 嗨赞,感谢您的回答!我查看了 Python 的实现,但找不到这样的东西……你知道我可以在哪里看吗?另外,你知道什么可以解释长度从 ~64 到 ~100 的稳定增长吗?
  • 我看了源码。 python 如何处理以该长度更改的字符串似乎没有任何进展,但缓存未命中听起来非常合理。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-08-23
  • 1970-01-01
  • 2015-10-11
  • 2016-08-19
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多