2016-03-04 44 views
4

我有一个很大的Python代码库,我们最近开始用Cython编译。在不对代码做任何修改的情况下,我期望性能保持不变,但我们计划在性能分析后使用Cython特定的代码优化较重的计算。然而,编译的应用程序的速度急剧下降,似乎是全面的。方法比以前多花费10%到300%。Cython字符串串联超慢;它还有什么不好呢?

我一直在玩弄测试代码来试图找到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]) 

编译与

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模块,我已经与用Cython 0.19.1尝试这样做和0.23.4。我用gcc 4.8.2和icc 14.0.2编译了C代码,尝试使用两种标志。

回答

4

值得一读:佩普0008>编程建议:

代码应写在不劣势的其他实现方式Python(PyPy,Jython,IronPython,Cython,Psyco等)。

例如,不要依赖CPython对a + = b或a = a + b形式的语句进行就地字符串串联的高效实现。即使在CPython中,这种优化也很脆弱(它只适用于某些类型),并且在不使用refcounting的实现中完全不存在。在库的性能敏感部分,应该使用''.join()表单来代替。这将确保串联在各种实现中以线性时间发生。

参考:https://www.python.org/dev/peps/pep-0008/#programming-recommendations

+0

了解CPython专门针对此进行优化确实有所帮助。事实上,将第三个例子改为'val = str(i)+ val'使得CPython比Cython花费的时间更长(约24s)。所以也许真正的问题是,我怎么知道CPython优化的其他实现可能不会?我确信字符串连接在我们的代码库中不是真正的问题。 – rpmcnally

+1

要了解优化发生的方式,请参阅CPython解释器源代码:https://hg.python.org/cpython/file/7fa3e824a4ee/Python/ceval.c#l1677。注意特殊情况检查“pyunicode”(至少在Python 3中!)。相比之下,Cython只是'PyNumber_InPlaceAdd' – DavidW

+0

如果你想知道CPython在哪里进行优化,Cython不会那么搜索'_CheckExact'文件可能是一个很好的开始,尽管它可能有点乏味。主要的其他明显的候选人,我可以看到如果'%'字符串格式 – DavidW

2

该表单的重复字符串连接通常会被忽略;有些解释器无论如何都会对它进行优化(暗中超额定位,并允许在已知安全的情况下对技术上不可变的数据类型进行变形),但是Cython正在尝试对某些事情进行硬编码,这使得难度更大。

真正的答案是“不要连续重复不可变的类型”。 (这到处都是错误的,在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) 
+1

的'cython'代码必须使Python函数调用 - 格式化整数,并创建一个新的字符串。它可能会将迭代转换为C,但这只是该操作的一小部分。 – hpaulj

+0

感谢您的回答。我意识到这种串联是非常糟糕的做法,但是在没有优化的情况下,Cython代码会抛出与CPython基本相同的代码,但显然不是。但是我们的代码库相当成熟,所以我会震惊地在任何地方找到这种类型的代码;你有没有指出CPython可能会优化的其他东西,但是Cython不会? – rpmcnally

+0

@rpmcnally:列举这些事情真的很难。 Cython和CPython优化在某些方面接近极性,因为很容易将其转换为C,所以Cython从使用大量低级“基元”(遍历范围和索引列表)中受益匪浅。相比之下,CPython直接迭代'list'要快得多,如果需要'list',则使用'enumerate'。基本上,使用类似C的假设(其中之一是字符串串联),编写类似Python的Python代码时,Cython速度最快。 – ShadowRanger