为什么比较匹配的字符串比比较不匹配的字符串更快?

Eri*_*ric 77 python comparison performance

这里有两个测量值:

timeit.timeit('"toto"=="1234"', number=100000000)
1.8320042459999968
timeit.timeit('"toto"=="toto"', number=100000000)
1.4517491540000265
Run Code Online (Sandbox Code Playgroud)

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

为什么?

S3D*_*DEV 75

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

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

相同对象分配的原因是,作为实现细节,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
Run Code Online (Sandbox Code Playgroud)

作业时间:

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

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

Python源代码:

正如 @mkrieger1 和 @Masklinn 在他们的评论中所提供的,源代码首先执行unicodeobject.c指针比较True,然后 if 立即返回。

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

附录:

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

  • @RiccardoBucco,因为相等性检查通常会从“身份”检查开始,因为执行起来非常便宜,但如果它允许您绕过“结构性”相等性检查,则非常有效。您可以在 [`_PyUnicode_Equal`](https://github.com/python/cpython/blob/850687df47b03e98c1433e6e70e71a8921eb4454/Objects/unicodeobject.c#L11134-L11146) 中看到这一点。第 11139 行到第 11141 行是 C 级相等性检查,这意味着它比较指针,这在 CPython 中是身份比较(因为两个对象不能重叠,因此不能具有相同的指针)。 (14认同)
  • 对于字符串,它在这里实现:https://github.com/python/cpython/blob/main/Objects/unicodeobject.c#L11134 正如预期的那样,它首先检查身份并提前返回。 (8认同)
  • @YanickSalzmann CPython 当前缓存(实习生)仅包含单词字符的字符串。请参阅 /sf/ask/2987947651/ 。 (3认同)

Ric*_*cco 53

比较匹配的字符串并不总是更快。相反,比较共享相同 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
Run Code Online (Sandbox Code Playgroud)

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

  • 仅供参考,直接测试“它们是同一个对象吗?” 是“x 是 y”;`id(x) == id(y)` 确实得到了相同的结果,但它首先做了一些操作来使 `int` 对象进行比较,其中 `x is y` 只是直接比较内存地址而不包装它。 (5认同)