为什么比较匹配的字符串比比较不匹配的字符串更快?[复制]

Why is it faster to compare strings that match than strings that do not? [duplicate]

提问人:Eric 提问时间:3/28/2022 最后编辑:Peter MortensenEric 更新时间:3/30/2022 访问量:7870

问:

以下是两个测量值:

timeit.timeit('"toto"=="1234"', number=100000000)
1.8320042459999968
timeit.timeit('"toto"=="toto"', number=100000000)
1.4517491540000265

如您所见,比较两个匹配的字符串比比较两个大小相同但不匹配的字符串要快。 这非常令人不安:在字符串比较期间,我认为 Python 正在逐个字符测试字符串,因此测试时间应该比它需要四次测试才能进行不匹配比较的时间更长。也许比较是基于哈希的,但在这种情况下,两种比较的时间应该是相同的。"toto"=="toto""toto"=="1234"

为什么?

Python 性能 比较

评论

18赞 Sayse 3/28/2022
也许是字符串实习?
30赞 khelwood 3/28/2022
检查 的值。很可能同一语句中的两个相同的字符串文本被编译为同一个字符串对象。我想如果你的琴弦是用不同的方式制作的,你会得到不同的结果。"toto" is "toto"
1赞 Masklinn 3/28/2022
@RiccardoBucco“小整数”(从 -5 到 255 IIRC)实际上是预先记住的,它们总是会从缓存中获取。因此,对他们进行身份检查也很有意义。
1赞 Masklinn 3/28/2022
@RiccardoBucco是的,但是您具有相同标识的原因是缓存了小整数(在 cpython 中,作为实现细节)。float 没有这样的缓存,因此同一文本的两个实例是不同的对象。而且由于遇到相同浮点数(相同的对象,而不是相同的值)的可能性很低(因为它们没有被缓存),因此 cpython 不会优化这种比较。
1赞 marcelm 3/29/2022
“在字符串比较中,我相信 python 正在逐个字符地测试字符串”——我真诚地怀疑任何像样的编程语言都会使用朴素的 for 循环进行字符串比较。Python 当然没有,它使用 memcmp,它可以使用 SIMD 指令一次比较多个字节,以及其他优化。

答:

75赞 S3DEV 3/28/2022 #1

结合我的评论和@khelwood的评论:

TL的;DR:
在分析两个比较的字节码时,它揭示了 和 字符串被分配给同一个对象。因此,前期身份检查(在 C 级别)是提高比较速度的原因。
'time''time'

相同对象分配的原因是,作为实现细节,CPython 实习字符串仅包含“名称字符”(即字母和下划线字符)。这将启用对象的身份检查。


字节码:

import dis

In [24]: dis.dis("'time'=='time'")
  1           0 LOAD_CONST               0 ('time')  # <-- same object (0)
              2 LOAD_CONST               0 ('time')  # <-- same object (0)
              4 COMPARE_OP               2 (==)
              6 RETURN_VALUE

In [25]: dis.dis("'time'=='1234'")
  1           0 LOAD_CONST               0 ('time')  # <-- different object (0)
              2 LOAD_CONST               1 ('1234')  # <-- different object (1)
              4 COMPARE_OP               2 (==)
              6 RETURN_VALUE

分配时间:

“加速”也可以在使用时间测试的分配中看到。将两个变量赋值(和比较)到同一字符串比将两个变量赋值(和比较)到不同的字符串要快。为了进一步支持这一假设,底层逻辑正在执行对象比较。这在下一节中得到证实。

In [26]: timeit.timeit("x='time'; y='time'; x==y", number=1000000)
Out[26]: 0.0745926329982467

In [27]: timeit.timeit("x='time'; y='1234'; x==y", number=1000000)
Out[27]: 0.10328884399496019

Python 源码:

正如 @mkrieger1 和 @Masklinn 在他们的评论中提供的,源代码首先执行指针比较,如果 ,则立即返回。unicodeobject.cTrue

int
_PyUnicode_Equal(PyObject *str1, PyObject *str2)
{
    assert(PyUnicode_CheckExact(str1));
    assert(PyUnicode_CheckExact(str2));
    if (str1 == str2) {                  // <-- Here
        return 1;
    }
    if (PyUnicode_READY(str1) || PyUnicode_READY(str2)) {
        return -1;
    }
    return unicode_compare_eq(str1, str2);
}

附录:

  • 参考答案很好地说明了如何读取反汇编的字节码输出。图片由 @Delgan 提供
  • 参考答案很好地描述了 CPython 的字符串实习。@ShadowRanger的Coutresy

评论

0赞 Riccardo Bucco 3/28/2022
如果两个对象代表同一个对象,为什么它们之间的比较速度更快?比较运算符是如何实现的?
8赞 mkrieger1 3/28/2022
对于字符串,它在这里实现:github.com/python/cpython/blob/main/Objects/...正如预期的那样,它首先检查身份并提前返回。
14赞 Masklinn 3/28/2022
@RiccardoBucco因为平等性检查通常从身份检查开始,因为执行起来非常便宜,但如果它允许您绕过“结构性”平等性检查,则非常有效。你可以在_PyUnicode_Equal中看到这一点。第 11139 行到第 11141 行是 C 级相等性检查,这意味着它比较指针,在 CPython 中是恒等比较(因为两个对象不能重叠,因此不能具有相同的指针)。
0赞 S3DEV 3/28/2022
@mkrieger1 - 正是我想要的,谢谢。将包含在答案中。
3赞 David Hammen 3/29/2022
@YanickSalzmann CPython 目前缓存(实习生)仅包含单词字符的字符串。请参见 stackoverflow.com/questions/42684966/are-strings-cached
53赞 Riccardo Bucco 3/28/2022 #2

比较匹配的字符串并不总是更快。相反,比较共享相同 ID 的字符串总是更快。身份确实是这种行为的原因的证据(正如@S3DEV精彩解释的那样):

>>> x = 'toto'
>>> y = 'toto'
>>> z = 'totoo'[:-1]
>>> w = 'abcd'
>>> x == y
True
>>> x == z
True
>>> x == w
False
>>> id(x) == id(y)
True
>>> id(x) == id(z)
False
>>> id(x) == id(w)
False
>>> timeit.timeit('x==y', number=100000000, globals={'x': x, 'y': y})
3.893762200000083
>>> timeit.timeit('x==z', number=100000000, globals={'x': x, 'z': z})
4.205321462000029
>>> timeit.timeit('x==w', number=100000000, globals={'x': x, 'w': w})
4.15288594499998

比较具有相同 id 的对象总是更快(从示例中可以看出,与 和 之间的比较相比,and 之间的比较速度较慢,这是因为 和 不共享相同的 id)。xzxyxz

评论

5赞 ShadowRanger 3/30/2022
仅供参考,“它们是同一个对象吗?”的直接测试是; 确实得到了相同的结果,但它首先做了一些拇指摆动以使对象进行比较,其中只是直接比较内存地址而不包装它。x is yid(x) == id(y)intx is y