2016-03-04 28 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

相關問題