【问题标题】:python member str performance too slowpython成员str性能太慢
【发布时间】:2015-08-22 06:32:42
【问题描述】:

我在 python 2.7.3 中添加 str 类成员时遇到了一个奇怪的性能问题。我知道访问局部变量更快,但是,在下面的问题中,两个循环之间的速度差异超过 100 倍。访问a.accum_的那个启动快但慢,好像str iadd是O(n^2),长度为str。

有人知道原因吗?

# Fast ( < 1sec):
accum = str()
for ii in range(1000000):
    if (ii % 10000) == 0:
        print 'fast cnt', ii
    accum += 'zzzzz\n'

# Much slower ( > 5 mins):
class Foo:
    pass
a = Foo()
a.accum_ = str()
for ii in range(1000000):
    if (ii % 10000) == 0:
        print 'slow cnt', ii
    a.accum_ += 'zzzzz\n'

【问题讨论】:

  • 这肯定是因为第一种方法是第一种方法,字符串在其生命周期内只有一个引用,CPython 可以优化这种情况,因此需要 O(N) 时间。在第二种情况下,引用计数肯定会从 1 增加(但究竟在哪里?),因此是二次时间。
  • @AshwiniChaudhary:两种情况下的引用计数相同,但正如答案指出的那样,对第一种情况有特定的优化,但对第二种情况没有,这就是为什么第二种情况需要二次时间。
  • @doublep 我不确定您是如何计算引用的,但请检查+=during += 之前的引用计数。它应该至少增加 1。

标签: python string performance


【解决方案1】:

对于第一个示例,很明显这是单引用优化的情况(实际上有两个引用:一个来自对象本身,一个来自对象本身,一个 LOAD_FAST;unicode_concatenate 将在传递控制之前尝试将其减少到 1到 PyUnicode_Append) 由 CPython 使用 unicode_modifiable 函数完成:

static int
unicode_modifiable(PyObject *unicode)
{
    assert(_PyUnicode_CHECK(unicode));
    if (Py_REFCNT(unicode) != 1)
        return 0;
    if (_PyUnicode_HASH(unicode) != -1)
        return 0;
    if (PyUnicode_CHECK_INTERNED(unicode))
        return 0;
    if (!PyUnicode_CheckExact(unicode))
        return 0;
#ifdef Py_DEBUG
    /* singleton refcount is greater than 1 */
    assert(!unicode_is_singleton(unicode));
#endif
    return 1;
}

但在第二种情况下,因为实例数据存储在 Python dict 而不是一个简单的变量中,所以情况略有不同。

a.accum_ += 'foo'

实际上需要预取a.accum_ 的值并将其存储到堆栈中。所以,现在字符串有至少三个引用:一个来自实例字典,一个来自DUP_TOP,一个来自PyObject_GetAttr,由LOAD_ATTR 使用。因此,Python 无法优化这种情况,因为就地修改其中一个也会影响其他引用。

>>> class A:
    pass
...
>>> a = A()
>>> def func():
    a.str = 'spam'
    print a.str
    return '_from_func'
...
>>> a.str = 'foo'
>>> a.str += func()
spam

您可能希望这里的输出为'spam_from_func',但它会有所不同,因为a.str 的原始值是在调用func() 之前由Python 存储的。

>>> a.str
'foo_from_func'

字节码:

>>> import dis
>>> def func_class():
        a = Foo()
        a.accum = ''
        a.accum += 'zzzzz\n'
...
>>> dis.dis(func_class)
  2           0 LOAD_GLOBAL              0 (Foo)
              3 CALL_FUNCTION            0 (0 positional, 0 keyword pair)
              6 STORE_FAST               0 (a)

  3           9 LOAD_CONST               1 ('')
             12 LOAD_FAST                0 (a)
             15 STORE_ATTR               1 (accum)

  4          18 LOAD_FAST                0 (a)
             21 DUP_TOP
             22 LOAD_ATTR                1 (accum)
             25 LOAD_CONST               2 ('zzzzz\n')
             28 INPLACE_ADD
             29 ROT_TWO
             30 STORE_ATTR               1 (accum)
             33 LOAD_CONST               0 (None)
             36 RETURN_VALUE

请注意,此优化是在 around 2004(CPython 2.4) 中完成的,以防止用户 a += ba = a + b 的缓慢性,因此它主要用于简单变量,并且仅在下一条指令是 STORE_FAST(局部变量)、STORE_DEREF(闭包)和 STORE_NAME 时才有效。这不是一个通用的解决方案,the best way to do this in Python is to create a list and join its items using str.join

CPython 实现细节:如果st 都是字符串,则某些Python 实现(例如CPython)通常可以就地执行 优化s = s + ts += t 形式的赋值。什么时候 适用,这种优化使二次运行时间大大减少 可能。这个优化既是版本也是实现 依赖。对于性能敏感的代码,最好使用 str.join() 确保一致的线性连接的方法 跨版本和实现的性能。

【讨论】:

    【解决方案2】:

    Python 字符串是不可变的,因此不能拥有__iadd__ 方法。您在第一个示例中看到的是 CPython 解释器的微优化。在第一个示例中,解释器注意到它有一个引用计数为 1 的局部变量。因此,解释器可以厚颜无耻地修改字符串。尽管这违反了str 的合同,但在程序执行期间的任何时候都不会明显地违反此合同。

    在后一个例子中,这个微优化没有被实现,这就是它这么慢的原因。看起来可以应用优化,所以我不确定为什么不应用它。

    不过,一般来说,如果构建一个字符串,请整理列表中的子字符串,然后使用 str.join 创建最终产品。

    【讨论】:

    • 相关优化在string_concatenate()文件ceval.c中。显然,这与 CPython 有关。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-12-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多