La concatenación de cuerdas de Cython es super lenta; ¿Qué más hace mal?

Tengo una gran base de código Python que recientemente comenzamos a comstackr con Cython. Sin realizar ningún cambio en el código, esperaba que el rendimiento se mantuviera casi igual, pero planeamos optimizar los cálculos más pesados ​​con el código específico de Cython después del perfilado. Sin embargo, la velocidad de la aplicación comstackda se desplomó y parece ser general. Los métodos llevan de 10% a 300% más tiempo que antes.

He estado jugando con el código de prueba para intentar encontrar cosas que Cython hace mal y parece que la manipulación de cuerdas es una de ellas. Mi pregunta es, ¿estoy haciendo algo mal o es realmente malo Cython en algunas cosas? ¿Puedes ayudarme a entender por qué esto es tan malo y qué otra cosa podría hacer Cython muy mal?

EDIT: Déjame tratar de aclarar. Me doy cuenta de que este tipo de concatenación de cuerdas es muy malo; Acabo de notar que tiene una gran diferencia de velocidad, así que la publiqué (probablemente una mala idea). La base de código no tiene este tipo de código terrible, pero aún se ha reducido drásticamente y espero que haya sugerencias sobre qué tipo de construcciones maneja mal Cython para poder averiguar dónde buscar. He intentado perfilar, pero no fue particularmente útil.

Para referencia, aquí está mi código de prueba de manipulación de cadenas. Me doy cuenta de que el código de abajo es terrible e inútil, pero todavía estoy sorprendido por la diferencia de velocidad.

# 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) 

Código de tiempo

 # 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]) 

Comstackndo un modulo Cython con

 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 

Tiempos resultantes

 > 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 

Para referencia, he intentado esto con Cython 0.19.1 y 0.23.4. He comstackdo el código C con gcc 4.8.2 e icc 14.0.2, probando varios indicadores con ambos.

Vale la pena leer: Pep 0008> Recomendaciones de progtwigción:

El código debe escribirse de manera que no perjudique a otras implementaciones de Python (PyPy, Jython, IronPython, Cython, Psyco, etc.).

Por ejemplo, no confíe en la implementación eficiente de la concatenación de cadenas en el lugar de CPython para las declaraciones en la forma a + = b o a = a + b. Esta optimización es frágil incluso en CPython (solo funciona para algunos tipos) y no está presente en absoluto en implementaciones que no usan refcounting. En las partes de la biblioteca sensibles al rendimiento, se debe utilizar el formulario ” .join () en su lugar. Esto asegurará que la concatenación ocurra en tiempo lineal en varias implementaciones.

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

La concatenación de cuerdas repetida de esa forma generalmente es mal vista; de todos modos, algunos intérpretes lo optimizan (en general, se está generalizando y permitiendo la mutación de tipos de datos técnicamente inmutables en los casos en que se sabe que son seguros), pero Cython está tratando de codificar algunas cosas, lo que lo hace más difícil.

La respuesta real es: “No concatene tipos inmutables una y otra vez”. (Está mal en todas partes, solo que peor en Cython). Un enfoque perfectamente razonable que Cython probablemente manejaría bien es hacer una list de la str individual, y luego llamar ''.join(listofstr) al final para hacer la str de una vez.

En cualquier caso, no le está dando a Cython ninguna información de mecanografía con la que trabajar, por lo que los incrementos de velocidad no serán muy impresionantes. Trate de ayudarlo con las cosas fáciles, y los aumentos de velocidad allí pueden más que compensar las pérdidas en otros lugares. Por ejemplo, cdef su variable de bucle y usar ''.join podría ayudar aquí:

 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)