为什么字符串的开头慢于?

WKP*_*lus 55 python cpython startswith python-2.7 python-internals

令人惊讶的是,我发现startswith速度比in:

In [10]: s="ABCD"*10

In [11]: %timeit s.startswith("XYZ")
1000000 loops, best of 3: 307 ns per loop

In [12]: %timeit "XYZ" in s
10000000 loops, best of 3: 81.7 ns per loop
Run Code Online (Sandbox Code Playgroud)

众所周知,in操作需要搜索整个字符串,startswith只需要检查前几个字符,所以startswith应该更有效率.

什么时候s足够大,startswith速度更快:

In [13]: s="ABCD"*200

In [14]: %timeit s.startswith("XYZ")
1000000 loops, best of 3: 306 ns per loop

In [15]: %timeit "XYZ" in s
1000000 loops, best of 3: 666 ns per loop
Run Code Online (Sandbox Code Playgroud)

所以看起来调用startswith有一些开销,当字符串很小时它会变慢.

而且我试图弄清楚startswith呼叫的开销是多少.

首先,我使用一个f变量来降低点操作的成本 - 正如本答案中所提到的- 在这里我们可以看到startswith仍然较慢:

In [16]: f=s.startswith

In [17]: %timeit f("XYZ")
1000000 loops, best of 3: 270 ns per loop
Run Code Online (Sandbox Code Playgroud)

此外,我测试了空函数调用的成本:

In [18]: def func(a): pass

In [19]: %timeit func("XYZ")
10000000 loops, best of 3: 106 ns per loop
Run Code Online (Sandbox Code Playgroud)

无论点操作和函数调用的成本如何,时间startswith约为(270-106)= 164ns,但in操作仅需81.7ns.似乎还有一些开销startswith,那是什么?

在poke和lvc 之间startswith和之间添加测试结果__contains__:

In [28]: %timeit s.startswith("XYZ")
1000000 loops, best of 3: 314 ns per loop

In [29]: %timeit s.__contains__("XYZ")
1000000 loops, best of 3: 192 ns per loop
Run Code Online (Sandbox Code Playgroud)

pok*_*oke 38

正如在评论中已经提到的,如果你使用s.__contains__("XYZ")你得到的结果更相似,s.startswith("XYZ")因为它需要采用相同的路径:成员查找字符串对象,然后是函数调用.这通常有些昂贵(当然不足以让你担心).另一方面,当你这样做时"XYZ" in s,解析器会解释运算符,并且可以简化成员访问__contains__(或者更确切地说是它后面的实现,因为__contains__它本身只是访问实现的一种方式).

您可以通过查看字节码来了解这一点:

>>> dis.dis('"XYZ" in s')
  1           0 LOAD_CONST               0 ('XYZ')
              3 LOAD_NAME                0 (s)
              6 COMPARE_OP               6 (in)
              9 RETURN_VALUE
>>> dis.dis('s.__contains__("XYZ")')
  1           0 LOAD_NAME                0 (s)
              3 LOAD_ATTR                1 (__contains__)
              6 LOAD_CONST               0 ('XYZ')
              9 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             12 RETURN_VALUE
Run Code Online (Sandbox Code Playgroud)

所以比较s.__contains__("XYZ")s.startswith("XYZ")会产生更多类似的结果,但是你的例子字符串s,则startswith仍然会比较慢.

为此,您可以检查两者的实现.有意识地看到contains实现是它是静态类型的,只是假设参数本身是一个unicode对象.所以这非常有效.

然而,startswith实现是一个"动态"Python方法,它要求实现实际解​​析参数.startswith还支持一个元组作为参数,这使得方法的整个启动速度有点慢:(由我缩短,我的评论):

static PyObject * unicode_startswith(PyObject *self, PyObject *args)
{
    // argument parsing
    PyObject *subobj;
    PyObject *substring;
    Py_ssize_t start = 0;
    Py_ssize_t end = PY_SSIZE_T_MAX;
    int result;
    if (!stringlib_parse_args_finds("startswith", args, &subobj, &start, &end))
        return NULL;

    // tuple handling
    if (PyTuple_Check(subobj)) {}

    // unicode conversion
    substring = PyUnicode_FromObject(subobj);
    if (substring == NULL) {}

    // actual implementation
    result = tailmatch(self, substring, start, end, -1);
    Py_DECREF(substring);
    if (result == -1)
        return NULL;
    return PyBool_FromLong(result);
}
Run Code Online (Sandbox Code Playgroud)

这可能是一个很大的原因startswith,contains因为它的简单性使得a 快速的字符串变慢.

  • @mounaim显然,Python的Mercurial服务器只停机了几秒钟.它现在再次运作. (2认同)