Cython字符串拼接非常慢;它还有哪些性能不佳的地方?

4
我有一个很大的Python代码库,最近我们开始使用Cython编译。我们计划在分析后通过Cython特定的代码来优化更重的计算,但是没有对代码进行任何更改,我预计性能会保持不变。然而,已编译应用程序的速度暴跌,并且似乎情况普遍存在。方法的执行时间比之前长10%到300%不等。
我一直在尝试测试代码来寻找Cython做得不好的事情,发现字符串操作是其中之一。我的问题是,我是否做错了什么或者Cython确实有些事情做得不好?你能帮我理解为什么它这么差以及Cython可能其他做得非常糟糕的地方吗?
编辑:让我尝试澄清一下。我意识到这种字符串连接方式很糟糕;我只是注意到它有很大的速度差异,所以我发了出来(可能是个坏主意)。虽然代码库没有这种可怕的代码,但仍然大幅减速,我希望能指出Cython处理不佳的构造类型,以便我知道该从哪里入手。我已经尝试过分析,但效果并不明显。
以下是我字符串操作测试代码的参考。我知道下面的代码很糟糕且无用,但我仍然对速度差异感到震惊。
# pyCode.py
def str1():
    val = ""
    for i in xrange(100000):
        val = str(i)

def str2():
    val = ""
    for i in xrange(100000):
        val += 'a'

def str3():
    val = ""
    for i in xrange(100000):
        val += str(i)

计时代码
# compare.py
import timeit

pyTimes = {}
cyTimes = {}

# STR1
number=10

setup = "import pyCode"
stmt = "pyCode.str1()"
pyTimes['str1'] = timeit.timeit(stmt=stmt, setup=setup, number=number)

setup = "import cyCode"
stmt = "cyCode.str1()"
cyTimes['str1'] = timeit.timeit(stmt=stmt, setup=setup, number=number)

# STR2
setup = "import pyCode"
stmt = "pyCode.str2()"
pyTimes['str2'] = timeit.timeit(stmt=stmt, setup=setup, number=number)

setup = "import cyCode"
stmt = "cyCode.str2()"
cyTimes['str2'] = timeit.timeit(stmt=stmt, setup=setup, number=number)

# STR3
setup = "import pyCode"
stmt = "pyCode.str3()"
pyTimes['str3'] = timeit.timeit(stmt=stmt, setup=setup, number=number)

setup = "import cyCode"
stmt = "cyCode.str3()"
cyTimes['str3'] = timeit.timeit(stmt=stmt, setup=setup, number=number)

for funcName in sorted(pyTimes.viewkeys()):
    print "PY {} took {}s".format(funcName, pyTimes[funcName])
    print "CY {} took {}s".format(funcName, cyTimes[funcName])

使用Cython编译模块:

cp pyCode.py cyCode.py
cython cyCode.py
gcc -O2 -fPIC -shared -I$PYTHONHOME/include/python2.7 \
    -fno-strict-aliasing -fno-strict-overflow -o cyCode.so cyCode.c

结果时间

> python compare.py 
PY str1 took 0.1610019207s
CY str1 took 0.104282140732s
PY str2 took 0.0739600658417s
CY str2 took 2.34380102158s
PY str3 took 0.224936962128s
CY str3 took 21.6859738827s

供参考,我已经尝试过使用Cython 0.19.1和0.23.4。我使用gcc 4.8.2和icc 14.0.2编译了C代码,并尝试使用不同的标志。

2个回答

4
值得一读:Pep 0008 > 编程建议:
代码应该以不劣于 Python 的其他实现(PyPy、Jython、IronPython、Cython、Psyco 等)的方式编写。
例如,不要依赖 CPython 在原地字符串连接方面的高效实现,如 a += b 或 a = a + b。这种优化即使在 CPython 中也很脆弱(它仅适用于某些类型),而且在不使用引用计数的实现中根本不存在。在库的性能敏感部分,应该使用 ''.join() 形式。这将确保各种实现中的连接发生在线性时间内。
参考:https://www.python.org/dev/peps/pep-0008/#programming-recommendations

知道CPython专门针对此进行优化确实有所帮助。事实上,将第三个示例更改为“val = str(i) + val”使得CPython所需的时间甚至比Cython(约24秒)还要长。因此,也许真正的问题是,我如何知道CPython进行了什么优化,而其他实现可能没有进行?我确定字符串连接不是我们代码库中的真正问题。 - rpmcnally
2
对于那些好奇优化发生在哪里的人,请查看CPython解释器源代码:https://hg.python.org/cpython/file/7fa3e824a4ee/Python/ceval.c#l1677。请注意,“pyunicode”的特殊情况检查(至少在Python 3中!)。相比之下,Cython只是执行PyNumber_InPlaceAdd - DavidW
如果你想知道CPython在哪些地方进行了Cython没有的优化,那么在该文件中搜索“_CheckExact”可能是一个不错的起点,尽管这可能有点繁琐。我能看到的另一个明显的候选者是百分号字符串格式化。 - DavidW

2
那种形式的重复字符串连接通常不被看好;一些解释器仍然对其进行优化(秘密地过度分配并允许在已知是安全的情况下改变技术上不可变的数据类型),但Cython试图硬编码一些东西,这使得这更加困难。
真正的答案是“不要反复连接不可变类型”(无论在哪里都是错误的,在Cython中更糟)。一个完全合理的方法是将每个str制作成一个list,然后在最后调用''.join(listofstr)一次性制作str
无论如何,您没有给Cython任何类型信息来处理,因此速度提升将不会非常令人印象深刻。尝试帮助它处理简单的事情,那里的速度提升可能会弥补其他地方的损失。例如,使用cdef循环变量和''.join可能会有所帮助:
cpdef str2():
    cdef int i
    val = []
    for i in xrange(100000):  # Maybe range; Cython docs aren't clear if xrange optimized
        val.append('a')
    val = ''.join(val)

2
cython 代码必须调用 Python 函数 - 格式化整数和创建新字符串。它可以将迭代转换为 C,但这只是其中的一小部分。 - hpaulj
感谢您的回答。我意识到这种连接方式是可怕的做法,但我认为,如果没有优化,Cython代码会产生与CPython基本相同的代码,但显然不是这样。但我们的代码库已经非常成熟了,所以我很惊讶会在任何地方发现这种代码;您是否有指向CPython可能进行优化但Cython不会的其他事物的指针? - rpmcnally
@rpmcnally:枚举这些事情将会非常困难。在某些方面,Cython和CPython优化几乎是完全相反的;Cython受益于使用大量低级“基元”(例如循环范围和索引列表),因为它很容易转换为C;相比之下,CPython直接迭代list或者如果需要list,使用enumerate会更快。基本上,当您编写类似C的Python代码并使用类似C的假设(其中之一是字符串连接很慢)时,Cython最快。 - ShadowRanger
我很担心这个问题。问题在于分析显示整体速度变慢,几乎所有东西都变慢了。我想我只能想办法找到更好的方法来分析这段代码。那么,“我问了一个糟糕的问题”的适当礼仪是什么?是删除它还是选择最接近答案的东西? - rpmcnally
1
@rpmcnally 我会保留原问题,并(可选)选择最能回答关于字符串连接的原始问题的答案。相比CPython,Cython存在哪些不足可能是一个更一般性的问题,很难得到明确的答案,因此可能会被关闭为“过于宽泛”。 - DavidW

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接