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

Why is string's startswith slower than in?

令人惊讶的是,我发现startswithin慢:

1
2
3
4
5
6
7
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

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

s足够大时,startswith更快:

1
2
3
4
5
6
7
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

因此,调用startswith似乎有一些开销,这使得字符串很小时速度变慢。

然后我试图弄清楚startswith呼叫的开销是多少。

首先,我使用了一个f变量来降低点操作的成本(如本文所述),这里我们可以看到startswith仍然较慢:

1
2
3
4
In [16]: f=s.startswith

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

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

1
2
3
4
In [18]: def func(a): pass

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

不考虑点操作和函数调用的开销,startswith的时间约为(270-106)=164ns,而in的操作只需要81.7ns。看来江户还是有些日常开支,那是什么?

根据poke和lvc的建议,在startswith__contains__之间添加测试结果:

1
2
3
4
5
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


如注释中所述,如果使用s.__contains__("XYZ"),则会得到与s.startswith("XYZ")更相似的结果,因为它需要采用相同的路径:字符串对象上的成员查找,然后是函数调用。这通常有点贵(当然不足以让你担心)。另一方面,当您执行"XYZ" in s操作时,解析器将解释运算符,并可以缩短成员对__contains__的访问(或者更确切地说,它后面的实现,因为__contains__本身只是访问实现的一种方法)。

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

1
2
3
4
5
6
7
8
9
10
11
>>> 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

因此,将s.__contains__("XYZ")s.startswith("XYZ")进行比较会产生更相似的结果,但是对于您的示例字符串sstartswith仍然会变慢。

为了实现这一点,您可以检查这两者的实现。对于包含实现,有趣的是它是静态类型的,并且只假设参数是Unicode对象本身。所以这是相当有效的。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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);
}

这可能是startswith速度较慢的一个重要原因,因为其简单性,contains速度较快。


这可能是因为str.startswith()str.__contains__()做得更多,而且我相信str.__contains__完全在C中运行,而str.startswith()必须与python类型交互。它的签名是str.startswith(prefix[, start[, end]]),其中前缀可以是一个字符串元组。