El intento temprano de eliminar Python GIL resultó en un mal rendimiento: ¿Por qué?

13

Esta publicación del creador de Python, Guido Van Rossum, menciona un intento temprano de eliminar el GIL de Python:

Esto se ha intentado antes, con resultados decepcionantes, por lo que soy reacio a esforzarme mucho. En 1999 Greg Stein (¿con Mark Hammond?) Produjo una bifurcación de Python (1.5 creo) que eliminó el GIL, reemplazándolo con bloqueos de grano fino en todas las estructuras de datos mutables. También presentó parches que eliminaron muchas de las dependencias en las estructuras de datos mutables globales, lo cual acepté. Sin embargo, después de la evaluación comparativa, se demostró que incluso en la plataforma con la primitiva de bloqueo más rápida (Windows en ese momento) ralentizó la ejecución de un solo subproceso casi dos veces, lo que significa que en dos CPU, podría obtener un poco más de trabajo hecho sin el GIL que en una sola CPU con el GIL. Esto no fue suficiente, y el parche de Greg desapareció en el olvido. (Ver la reseña de Greg sobre la actuación).

Apenas puedo discutir con los resultados reales, pero realmente me pregunto por qué sucedió esto. Presumiblemente, la razón principal por la que eliminar el GIL de CPython es tan difícil es por el sistema de gestión de memoria de conteo de referencia. Un programa típico de Python llamará Py_INCREFy Py_DECREFmiles o millones de veces, por lo que es un punto clave de la contención si tuviéramos que envuelva cerraduras alrededor de ella.

Pero, no entiendo por qué agregar primitivas atómicas ralentizaría un solo programa de subprocesos. Supongamos que acabamos de modificar CPython para que la variable de recuento en cada objeto Python fuera una primitiva atómica. Y luego solo hacemos un incremento atómico (instrucciones de buscar y agregar) cuando necesitamos incrementar el recuento de referencia. Esto haría que el recuento de referencias de Python sea seguro para subprocesos, y no debería tener ninguna penalización de rendimiento en una aplicación de un solo subproceso, porque no habría contención de bloqueo.

Pero, por desgracia, muchas personas que son más inteligentes que yo lo han intentado y han fallado, así que obviamente me falta algo aquí. ¿Qué hay de malo en la forma en que estoy viendo este problema?

Siler
fuente
1
Tenga en cuenta que la operación de recuento no sería el único lugar que necesita sincronización. La cita menciona "bloqueos de grano fino en todas las estructuras de datos mutables", que supongo que incluye al menos un mutex para cada lista y objeto de diccionario. Además, no creo que las operaciones de enteros atómicos sean tan eficientes como el equivalente no atómico, independientemente de la contención, ¿tiene una fuente para eso?
simplemente, porque las operaciones atómicas son más lentas que los equivalentes no atómicos. El hecho de que sea una sola instrucción no significa que sea trivial bajo el capó. Vea esto para una discusión
Móż

Respuestas:

9

No estoy familiarizado con la bifurcación de Greg Stein Python, así que descarte esta comparación como analogía histórica especulativa si lo desea. Pero esta fue exactamente la experiencia histórica de muchas bases de código de infraestructura que pasaron de implementaciones de un solo subproceso a múltiples.

Esencialmente, todas las implementaciones de Unix que estudié en la década de 1990 (AIX, DEC OSF / 1, DG / UX, DYNIX, HP-UX, IRIX, Solaris, SVR4 y SVR4 MP) pasaron exactamente por este tipo de " bloqueo de grano más fino - ¡ahora es más lento! " problema. Los DBMS que seguí (DB2, Ingres, Informix, Oracle y Sybase) también lo pasaron.

He escuchado que "estos cambios no nos retrasarán cuando ejecutamos un solo subproceso" un millón de veces. Nunca funciona de esa manera. El simple acto de verificar condicionalmente "¿estamos ejecutando multiproceso o no?" agrega una sobrecarga real, especialmente en CPU altamente canalizadas. Las operaciones atómicas y los bloqueos de giro ocasionales agregados para garantizar la integridad de las estructuras de datos compartidas deben llamarse con bastante frecuencia, y son muy lentos. Las primitivas de bloqueo / sincronización de primera generación también fueron lentas. La mayoría de los equipos de implementación finalmente agregan varias clases de primitivas, en varias "fortalezas", dependiendo de cuánta protección de enclavamiento se necesita en varios lugares. Luego se dan cuenta de dónde inicialmente abofetearon las primitivas de bloqueo no era realmente el lugar correcto, por lo que tuvieron que perfilar, diseñar alrededor de los cuellos de botella encontrados, y sistemáticamente roto-labrar. Algunos de estos puntos conflictivos eventualmente obtuvieron aceleración del sistema operativo o del hardware, pero toda esa evolución tomó 3-5 años, como mínimo. Mientras tanto, las versiones MP o MT estaban cojeando, en cuanto al rendimiento.

De lo contrario, los equipos de desarrollo sofisticados han argumentado que tales ralentizaciones son básicamente un hecho persistente e intratable de la vida. IBM, por ejemplo, se negó a habilitar AIX para SMP durante al menos 5 años después de la competencia, convencido de que un solo subproceso era simplemente mejor. Sybase utilizó algunos de los mismos argumentos. La única razón por la que algunos de los equipos finalmente llegaron fue porque el rendimiento de un solo hilo ya no podía mejorarse razonablemente a nivel de CPU. Se vieron obligados a usar MP / MT o aceptar tener un producto cada vez menos competitivo.

La concurrencia activa es DURA. Y es engañoso. Todos se apresuran a pensar "esto no será tan malo". Luego golpean las arenas movedizas y tienen que atravesarlo. He visto que esto sucede con al menos una docena de equipos inteligentes de marca reconocida y bien financiados. En general, parecía tomar al menos cinco años después de elegir multihilo para "volver a donde deberían estar, en cuanto al rendimiento" con los productos MP / MT; la mayoría seguía mejorando significativamente la eficiencia / escalabilidad de MP / MT incluso diez años después de realizar el cambio.

Así que mi especulación es que, a falta del respaldo y el apoyo de GvR, nadie ha asumido el largo esfuerzo por Python y su GIL. Incluso si lo hicieran hoy, sería el marco de tiempo de Python 4.x antes de que digas "¡Guau! ¡Realmente hemos superado la joroba MT!"

Quizás haya algo de magia que separe a Python y su tiempo de ejecución de todos los demás softwares de infraestructura con estado: todos los tiempos de ejecución de lenguaje, sistemas operativos, monitores de transacciones y administradores de bases de datos que se han utilizado anteriormente. Pero si es así, es único o casi. Todos los demás que eliminaron un equivalente de GIL han necesitado más de cinco años de esfuerzo e inversión comprometidos para pasar de MT-no a MT-hot.

Jonathan Eunice
fuente
2
+1 Se tardó ese tipo de tiempo en subprocesar Tcl con un equipo bastante pequeño de desarrolladores. El código era seguro para MT antes de eso, pero tenía problemas de rendimiento desagradables, principalmente en la administración de memoria (que sospecho que es un área muy activa para lenguajes dinámicos). Sin embargo, la experiencia realmente no se traslada a Python en nada más que en los términos más generales; Los dos idiomas tienen modelos de subprocesos completamente diferentes. Solo ... espera un trabajo duro y espera errores extraños ...
Donal Fellows
-1

Otra hipótesis descabellada: en 1999, Linux y otros Unices no tenían una sincronización performante como ahora futex(2)( http://en.wikipedia.org/wiki/Futex ). Esos llegaron alrededor de 2002 (y se fusionaron en 2.6 alrededor de 2004).

Como todas las estructuras de datos integradas deben sincronizarse, el bloqueo cuesta mucho. Ӎσᶎ ya señaló, que las operaciones atómicas no son necesariamente baratas.

Sahib
fuente
1
¿Tienes algo para respaldar esto? o esto es casi especulación?
1
La cita de GvR describe el rendimiento "en la plataforma con la primitiva de bloqueo más rápida (Windows en ese momento)", por lo que los bloqueos lentos en Linux no son relevantes.