【问题标题】:Why is string's startswith slower than in?为什么字符串的startswith比in慢?
【发布时间】:2015-11-02 05:43:25
【问题描述】:

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

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 更快:

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 变量来降低点运算的成本——正如这个answer 中提到的——这里我们可以看到startswith 仍然更慢:

In [16]: f=s.startswith

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

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

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。 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

【问题讨论】:

  • 要获得更相似的结果,您可以使用s.__contains__("XYZ"),因为这将采用与s.startswith("XYZ") 相同的路径(使用in 运算符将缩短成员访问权限)。但是,startswith 对我来说仍然较慢。
  • 我认为性能差异的其余部分是由于__contains__ 被完全键入在 C 中,而 startswith 执行实际参数解析和其他东西(你也可以通过一个元组)。
  • 您使用的是什么版本的 Python?在 3.4.3 上,我得到 s.startswith("XYZ") 报告 153ns,s.__contains__("XYZ") 报告 169ns。正如@poke 所说,使用in 将使用与方法调用完全不同的查找规则——它可以直接从C 级别的函数指针中查找,而方法查找执行两个字典搜索和then i> 必须进行 Python 级别的函数调用。分别计时这些事情可以让您一些了解差异,但不一定准确。在你的数字上,减去这两项开销就可以得到startswith negative!
  • 我更进一步检查了%timeit "XYZ" == s[0:3],它给了我10000000 loops, best of 3: 94 ns per loop,而%timeit "XYZ" in s10000000 loops, best of 3: 59.2 ns per loop。用 python 3.4.3 测试。 (似乎在我的情况下,切片会产生“一些”开销,因为%timeit "XYZ" in s[0:3] 导致10000000 loops, best of 3: 101 ns per loop
  • @LightnessRacesinOrbit 虽然这是真的,但s 是字符串"ABCD" 的重复,因此必须搜索整个字符串才能得出"XYZ" 不包含在其中的结论.

标签: python python-2.7 cpython python-internals startswith


【解决方案1】:

正如 cmets 中已经提到的,如果您使用 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

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

为此,您可以检查两者的实现。 contains implementation 的有趣之处在于它是静态类型的,并且只是假设参数本身是一个 unicode 对象。所以这是非常有效的。

startswith implementation 然而是一种“动态” Python 方法,它需要实现来实际解析参数。 startswith 还支持一个元组作为参数,这使得方法的整个启动有点慢:(由我缩短,用我的 cmets):

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 由于其简单性而很快。

【讨论】:

  • 实现的链接已损坏
  • @mounaim 显然,Python 的 Mercurial 服务器刚刚宕机了几秒钟。它现在又开始工作了。
【解决方案2】:

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

【讨论】:

  • “做更多”是模糊的,可能不是真的。有效地查找子字符串并不是一个简单的问题。 List of algorithms.。相比之下,查找前缀非常容易,而且通常更快。
  • 你是对的@PaulDraper;我写的时候迟到了:P。并感谢 @LightnessRacesinOrbit 的编辑 :)。
猜你喜欢
  • 1970-01-01
  • 2011-12-26
  • 2021-01-15
  • 2014-10-04
  • 1970-01-01
  • 2012-03-11
  • 1970-01-01
  • 2011-03-25
相关资源
最近更新 更多