Concurrencia, GIL y multi-núcleo

Francesc Alted faltet en pytables.org
Mie Jun 3 18:38:39 CEST 2009


A Wednesday 03 June 2009 16:17:28 Pau Freixes escrigué:
> Buenas Lista, siguiendo las objetividades de Francesc hago algunos apuntes
> que creo que a mi punto de vista podrían desmitificar
> el uso de procesos como sistema primario de multiconcurrencia vs threads en
> la actual arquitectura de Python.
>
> > Bueno, pues la pregunta es bastante general, pero intentaré responder.
> > Personalmente pienso que existen todavia muchas maneras de sacar
> > rendimiento a
> > las CPUs multi-núcleo sin necesidad de acudir a temas de concurrencia. 
> > Por ejemplo, hacer un uso óptimo del ancho de banda de memoria es una
> > cosa crítica
> > y muy poca gente es consciente de ello.  Si nuestros cálculos están
> > limitados
> > por el acceso a memoria (y hoy en día la mayoría lo están), un sistema
> > multi-
> > núcleo poco podrá hacer por acelerarlos.
> >
> > Otro tema importante es hacer uso de las capacidades de cálculo vectorial
> > que
> > vienen en las CPUs modernas (instrucciones SSE[2-4] o VMX en el futuro
> > próximo) y que estan bastante desaprovechadas en general.  Creo que si
> > los desarrolladores hicieran un mejor aprovechamiento de las
> > instrucciones vectoriales, se podrian conseguir velocidades bastante
> > superiores en muchas situaciones.
>
> Totalmente decuerdo, pero no sera un problema especifico de las
> aplicaciones en Python, es un problema orientado a la optimización de
> código dependiente de arquitectura. Se han hecho pasos para "oficializar"
> sistemas de calculo vectoriales entre
> distintos fabricantes de marcas para poder dar al programador un mínimo
> entorno neutro - AMD, INTEL - pero seguimos teniendo
> diferentes instrucciones para distintos procesadores. Además - como es
> lógico - este conjunto de instrucciones se amplian a medida que mejoramos
> las prestaciones de los procesadores - lease diferentes releasees de
> procesadores para un mismo
> fabricante.

Según mi punto de vista, *sí* que es un problema de la aplicaciones en Python, 
ya que muchas de ellas (especialmente las que están diseñadas para aprovechar 
al máximo los recursos de cálculo disponibles) se programan como extensiones C 
(directamente o con alguna interface como Pyrex/Cython) o incluso en Fortran 
(a través de F2PY, por ejemplo). Y en C y Fortran sí que es relativamente 
fácil usar las capacidades de vectorización.  Y además, en C y Fortran uno 
*sí* que es capaz de librarse del bloqueo del GIL sin problemas.

Respecto a la estandarización, estoy de acuerdo contigo en que es un problema.  
Sin embargo, hay que recordar que el conjunto SSE2 lo implementan 
*completamente* tanto Intel como AMD en todos sus procesadores desde los 
tiempos del Pemtium 4 y el Athlon.  Que haya muy poco software que lo use 
tiene muy poca justificación, desde mi punto de vista.

> Por esta razón el programador de apie, ha hecho poco o nada para intentar
> optimizar sus programas. De hecho este es un proceso
> que puede dejarse a razón del compilador - no en su totalidad, pero si con
> el tipico inlinning, loop unrolling, especializacion - pero la mayoria de
> los programas de hoy en dia los compilamos sin tener en cuenta los flags de
> optimización.

Te aseguro que los compiladores están todavía muy lejos de poder aprovechar 
completamente las capacidades vectoriales de los procesadores modernos, 
excepto en casos muy triviales, donde si pueden hacer una muy buena labor 
(especialmente las últimas versiones del compilador GCC, y lo digo por 
experiencia).  Curiosamente, cuando todo el mundo creia (y yo me incluyo entre 
ellos) que el ensamblador era una cosa obsoleta, el aprovechamiento de la 
maquinaria de vectorización por parte del programador actual pasa por conocer 
cosas como los registros disponibles, los ciclos de reloj de cada instrucción, 
los tamaños de cache, etc.  En fin, no exactamente ensamblador, pero si que se 
requieren unos conocimientos del hardware bastante importantes, como antaño.

> Para el tema del ancho de banda de memoria, totalmente de acuerdo. Tener
> consciencia de la cache de nivel 1 i nivel 2 y de sus características
> pueden modificar sobradamente el rendimiento de una aplicación. Por bien o
> por mal actualmente tiene que ser el
> programador que tendrá que tener en cuenta estas opciones, y siempre
> acabará dependiendo de la arquitectura donde se esta ejecutando.
>
> > Así es que me da la impresión que, debido a que la industria ha derivado
> > hacia
> > la construcción de procesadores con núcleos múltiples (básicamente por
> > razones
> > de imposibilidad técnica de seguir por los caminos tradicionales de subir
> > la
> > frecuencia de los procesadores), existe una fiebre un poco desmesurada
> > por parte de los usuarios en poder usar todos los procesadores de forma
> > paralela,
> > cuando la realidad es que conseguir esto no es posible en general.  Por
> > esta
> > razón coincido con GvR en que no veo demasiado crítico la limitación del
> > GIL.
>
> Umm a parte de ser una salida hacia delante contra el problema actual de
> seguir escalando la frequencia, por suerte sigue siendo una solución
> "valida" para augmentar el rendimiento de nuestras maquinas. El programador
> tiene que ser consciente de la nueva arquitectura y sacar el maximo jugo a
> ella. Pero para poder hacerlo tendrá que tener las herramientas. El quid de
> la questión es como y sobre que raizes se contruyen estas herramientas. En
> el caso de Python esta claro que hay una apuesta a corto plazo para
> utilizar el proceso como entidad de procesamiento en un entorno de
> múltiples cores. Distinto por ejemplo a openMP sobre linux que lo hace
> sobre pthreads.

Como he dicho, el disponer de varios procesadores puede ser una solución 
válida para ciertos casos, pero no para la mayoría.  Por poner un ejemplo 
concreto, supongamos que queremos calcular la suma de dos arrays, digamos 'a' 
y 'b', y depositar el resultado en 'c'.  Pues bien, los procesadores modernos 
pueden efectuar hasta 2 sumas (y hasta 4 si se usa vectorización) en un mismo 
ciclo de reloj.  Sin embargo, los sistemas de memoria actuales sólo pueden 
suministrar datos a la CPU a un ritmo de 1 elemento cada entre 4 y 8 ciclos de 
reloj (dependiendo de la placa base).  Encima, para hacer la suma hacen falta 
2 elementos, así que la CPU no tiene más remedio que quedarse parada entre 7 y 
15 ciclos de reloj por cada ciclo digamos 'productivo' (estamos hablando de un 
factor 10 en la caida de rendimiento!).  Y en este escenario usar múltiple 
núcleos no sirve para nada, aunque si me apuras,  puede incluso llegar a ser 
contraproducente (por los tiempos de arbitración del bus, y por el tiempo 
empleado en sincronizar las acciones entre los distintos hilos de ejecución).

> > Dicho esto, es cierto que hay problemas que pueden sacar rendimiento a
> > varios
> > procesadores simultáneamente (codificación de vídeo, por ejemplo), aunque
> > en
> > mi opinión, son áreas bastante restringidas y no afectan a la mayoría de
> > desarrollos.
>
> Creo sinceramente que hay otros paradigmas donde puede ser interesante, en
> los modelos tradicionales de cliente-servidor. Los actuales desarrollos de
> servidores - apache, squid, postfix, ... - han tenido que modificar a lo
> largo del tiempo sus paradigmas para dar soporte cada vez a mas i mas
> usuarios simultaneos. Algunos eliguieron cambiar a sistemas event-driven,
> otros a multihilos/procesos, y otros a sistemas mixtos.

Éste es un caso un poco distinto del que estamos hablando.  Los paradigmas de 
los servidores que nombras (apache, squid, postfix) son efectivos incluso en 
sistemas de un único núcleo.  Aquí el problema básicamente es poder dar 
respuesta rápida a una serie de peticiones (los clientes) si tener que esperar 
a que acaben otras que pueden ser muy pesadas (pero que hay que atender 
también).  Así que el desarrolador ha decidido usar threads (o multiproceso) 
para poder desentenderse de tener que programar la multitarea para atender 
cada petición.  Al dejar al sistema operativo esta responsabilidad, 1) los 
cambios de contexto se hacen más rápido y 2) es mucho más fácil de programar.  
Aún así, cuando hablamos de despachar los más rápido posible un millón de 
peticiones, o pones un sistema con un bus de acceso a memoria ancho y 
poderoso, o no te va servir de nada cambiar tu procesador mono-núcleo por uno 
de 8 vias.

> > Para la gente interesada en estos temas, hay un par de presentaciones
> > bastante
> > interesantes que dió Jesse Noller en el último PyCon de Chicago.  En [1],
> > describe el paquete ``multiprocessing`` incluido en las últimas versiones
> > de
> > Python, y cómo se compara con los threads clásicos.  En [2], hace una
> > introducción bastante básica y asequible sobre los sistemas concurrentes
> > hoy
> > en día, haciendo especial énfasis en aclarar unas cuantas falsedades
> > sobre la
> > percepción que la gente tiene de ellos.  La encuentro bastante
> > esclarecedora,
> > pues desmitifica un poco las expectativas puestas en el paralelismo.
>
> Vi haze tiempo la prensentacion [1], que està realmente bien. Ahora bien
> tengo un pero de la presentación, el justifica en cierta medida el uso de
> multiprocessing mediante un ejemplo de los tiempos utilizados por el modelo
> antiguo de threads y el que el presenta para calcular N numeros primos.
> Pero claro justamente en aprlicaciones de CPU intensiva el diseño de Python
> sobre threads es bastante deficiente, los números cantan por si solo y no
> es justamente por el uso explicito de threads sino por la implemetnación
> actual de GIL. Habría estado muy bien una comparativa contra una versión
> del programa con python stackless.

Bueno, en descargo de Jesse, él ya advierte que el ejemplo está 'trucado' 
(contrived).  Y añade que la implementación con threads es un ejemplo 
paradigmático de contención (y aquí se refiere implícitamente al GIL, según 
entiendo).

Por otro lado, el ejemplo (recuerdo, 'trucado') que ha puesto le funciona muy 
bien para procesos concurrentes ya que en el interior de la rutina para 
cálculo de primos ha usado (muy astutamente) una función `sqrt`, cuyo cálculo 
requiere de *mucha* CPU.  Además, en su programa no hay ninguna dependencia 
entre los procesos: cada uno puede testear si su `N` particular es primo o no.

Sin embargo, cualquiera que haya intentado elaborar un algoritmo de cálculo 
rápido de números primos sabe que hay maneras mucho más eficientes de hacerlo, 
sin necesidad de recurrir a funciones artificialmente 'costosas'.  Por 
ejemplo, en [1] se puede ver un caso de algoritmo que puede calcular los 
números primos entre el 1 y el *100 millones* en 13 segundos, usando un viejo 
Athlon 2400XP con un solo núcleo, mientras que al ejemplo de Jesse le cuesta 6 
minutos encontrar los primos entre el 1 millón y el 5 millones, y esto usando 
8 núcleos Core2 recientes!.  Y crees que usando multiproceso con varios 
núcleos en [1] se podría mejorar la cifra? lo dudo mucho.

Moraleja: si tu objetivo es hacer cálculos rápidamente, concéntrate en buscar 
un buen algoritmo primero; muchas veces se hace un algoritmo muy ineficiente y 
se quiere paralelizar para lograr mejores prestaciones de forma rápida.  Pero 
esto no vale más que para engañarse a uno mismo en la mayoría de las 
ocasiones.

[1] http://www.troubleshooters.com/codecorn/primenumbers/primenumbers.htm

> Ahora bien, sigo creyendo que Python podría haber hecho una apuesta de
> futuro, y no a corto plazo. Solucionando el "problema"
> de GIL vs Threads. Tal como comentan la actual implementación libera GIL de
> forma implicita cada n instrucciones o bien mediante el orden explicito por
> la macro Py_BEGIN_ALLOW_THREADS.

En el futuro el escenario que describo --núcleos cada vez más rápidos y buses 
que a duras penas les pueden proveer de algun dato de vez en cuando-- será 
todavía más evidente.  Seamos claros, el GIL no es un problema 'duro' ahora y 
lo será menos aún en el futuro.  Para lograr altas prestaciones, o bien se han 
de buscar paquetes optimizados en C (NumPy, SciPy, Numexpr), o si se necesita, 
uno se construye su propia extensión (al fin y al cabo hacer esto en 
Pyrex/Cython tampoco es tan complicado) donde se pueda soltar el GIL sin 
problemas.

> De hecho la construcción de un sistema multi concurrente con uso de threads
> tiene la gran problematica del uso de la compartición de memoria de facto,
> pero tambien tiene sus cosas positivas : proceso ligero, context switch mas
> rapido, uso de memomria más eficiente - en procesos tenemos COW - , etc ..
>
> Bueno estas son mis reflexiones, espero no ser muy pesado :P

Bueno, definitivamente yo sí lo he sido.  Mis disculpas.

-- 
Francesc Alted
_______________________________________________
Lista de correo Python-es 
http://listas.aditel.org/listinfo/python-es
FAQ: http://listas.aditel.org/faqpyes





Más información sobre la lista de distribución Python-es