threads threading thread subproceso stop example ejemplos definicion multithreading performance d thread-local-storage

multithreading - threading - ¿Por qué el almacenamiento local de subprocesos es tan lento?



threading.timer python (6)

Uno debe ser muy cuidadoso al interpretar los resultados de los puntos de referencia. Por ejemplo, un hilo reciente en los grupos de noticias D llegó a la conclusión de que la generación de código de dmd causaba una desaceleración importante en un bucle que hacía aritmética, pero en realidad el tiempo empleado estaba dominado por la función de ayuda de tiempo de ejecución que hacía una división larga. La generación del código del compilador no tuvo nada que ver con la ralentización.

Para ver qué tipo de código se genera para tls, compilar y obj2asm este código:

__thread int x; int foo() { return x; }

TLS se implementa de forma muy diferente en Windows que en Linux, y será muy diferente de nuevo en OSX. Pero, en todos los casos, habrá muchas más instrucciones que una simple carga de una ubicación de memoria estática. TLS siempre será lento en relación con el acceso simple. El acceso a TLS globales en un ciclo cerrado también será lento. Intente almacenar en caché el valor de TLS de forma temporal.

Hace algunos años, escribí un código de asignación de grupo de subprocesos y guardé en caché el identificador de TLS para el grupo, que funcionó bien.

Estoy trabajando en un asignador de memoria de estilo de marcación personalizada para el lenguaje de programación D que funciona asignando desde regiones locales de subprocesos. Parece que el cuello de botella de almacenamiento local está causando una gran desaceleración (~ 50%) en la asignación de memoria de estas regiones en comparación con una versión del código idéntica, incluso después de diseñar mi código para tener solo una búsqueda TLS por asignación / desasignación. Esto se basa en asignar / liberar memoria una gran cantidad de veces en un bucle, y estoy tratando de averiguar si es un artefacto de mi método de evaluación comparativa. Según tengo entendido, el almacenamiento local de subprocesos básicamente debe incluir el acceso a algo a través de una capa adicional de direccionamiento indirecto, similar al acceso a una variable mediante un puntero. ¿Es esto incorrecto? ¿Cuánta sobrecarga suele tener el almacenamiento local de subprocesos?

Nota: Aunque mencione D, también me interesan las respuestas generales que no son específicas de D, ya que la implementación de D de almacenamiento local de subprocesos probablemente mejorará si es más lenta que las mejores implementaciones.


La velocidad depende de la implementación de TLS.

Sí, tiene razón en que TLS puede ser tan rápido como una búsqueda de puntero. Incluso puede ser más rápido en sistemas con una unidad de administración de memoria.

Sin embargo, para la búsqueda del puntero necesita ayuda del planificador. El planificador debe - en un cambio de tarea - actualizar el puntero a los datos TLS.

Otra forma rápida de implementar TLS es a través de la Unidad de administración de memoria. Aquí, el TLS se trata como cualquier otro dato, con la excepción de que las variables TLS se asignan en un segmento especial. El planificador - en el cambio de tarea - mapeará la porción correcta de memoria en el espacio de direcciones de la tarea.

Si el planificador no admite ninguno de estos métodos, el compilador / biblioteca debe hacer lo siguiente:

  • obtener ThreadId actual
  • Toma un semáforo
  • Busque el puntero al bloque TLS por ThreadId (puede usar un mapa o más)
  • Suelta el semáforo
  • Devuelve ese puntero.

Obviamente, hacer todo esto para cada acceso a datos TLS lleva un tiempo y puede necesitar hasta tres llamadas al sistema operativo: obtener el ThreadId, tomar y liberar el semáforo.

El semáforo es, por cierto, necesario para asegurarse de que no se lee ningún hilo de la lista de punteros TLS, mientras que otro hilo está en el medio de generar un hilo nuevo. (y como tal, asignar un nuevo bloque TLS y modificar la estructura de datos).

Desafortunadamente, no es raro ver en la práctica la implementación lenta de TLS.


Si no puede usar el soporte TLS del compilador, puede administrar TLS usted mismo. Creé una plantilla de contenedor para C ++, por lo que es fácil reemplazar una implementación subyacente. En este ejemplo, lo he implementado para Win32. Nota: Dado que no puede obtener un número ilimitado de índices de TLS por proceso (al menos en Win32), debe apuntar a bloques de bloques lo suficientemente grandes como para contener todos los datos específicos de subprocesos. De esta manera, tiene un número mínimo de índices TLS y consultas relacionadas. En el "mejor de los casos", tendría solo 1 puntero TLS apuntando a un bloque de pila privada por subproceso.

En pocas palabras: no apunte a objetos individuales, en lugar de apuntar a la memoria de montón / contenedores con punteros de objetos específicos para lograr un mejor rendimiento.

No olvides liberar memoria si no se usa de nuevo. Lo hago envolviendo un hilo en una clase (como hace Java) y manejo TLS por constructor y destructor. Además, almaceno los datos usados ​​frecuentemente como identificadores de subprocesos e ID''s como miembros de la clase.

uso:

para tipo *: tl_ptr <tipo>

para const tipo *: tl_ptr <tipo const>

para el tipo * const: const tl_ptr <tipo>

tipo const * const: const tl_ptr <tipo const>

template<typename T> class tl_ptr { protected: DWORD index; public: tl_ptr(void) : index(TlsAlloc()){ assert(index != TLS_OUT_OF_INDEXES); set(NULL); } void set(T* ptr){ TlsSetValue(index,(LPVOID) ptr); } T* get(void)const { return (T*) TlsGetValue(index); } tl_ptr& operator=(T* ptr){ set(ptr); return *this; } tl_ptr& operator=(const tl_ptr& other){ set(other.get()); return *this; } T& operator*(void)const{ return *get(); } T* operator->(void)const{ return get(); } ~tl_ptr(){ TlsFree(index); } };


Los usuarios locales de subprocesos en D son realmente rápidos. Aquí están mis pruebas.

64 bit Ubuntu, core i5, dmd v2.052 Opciones del compilador: dmd -O -release -inline -m64

// this loop takes 0m0.630s void main(){ int a; // register allocated for( int i=1000*1000*1000; i>0; i-- ){ a+=9; } } // this loop takes 0m1.875s int a; // thread local in D, not static void main(){ for( int i=1000*1000*1000; i>0; i-- ){ a+=9; } }

Por lo tanto, solo perdemos 1.2 segundos de uno de los núcleos de la CPU por cada 1000 * 1000 * 1000 accesos locales de subprocesos. Se accede a los usuarios locales de subprocesos utilizando% fs register, por lo que solo hay un par de comandos de procesador involucrados:

Desmontando con objdump -d:

- this is local variable in %ecx register (loop counter in %eax): 8: 31 c9 xor %ecx,%ecx a: b8 00 ca 9a 3b mov $0x3b9aca00,%eax f: 83 c1 09 add $0x9,%ecx 12: ff c8 dec %eax 14: 85 c0 test %eax,%eax 16: 75 f7 jne f <_Dmain+0xf> - this is thread local, %fs register is used for indirection, %edx is loop counter: 6: ba 00 ca 9a 3b mov $0x3b9aca00,%edx b: 64 48 8b 04 25 00 00 mov %fs:0x0,%rax 12: 00 00 14: 48 8b 0d 00 00 00 00 mov 0x0(%rip),%rcx # 1b <_Dmain+0x1b> 1b: 83 04 08 09 addl $0x9,(%rax,%rcx,1) 1f: ff ca dec %edx 21: 85 d2 test %edx,%edx 23: 75 e6 jne b <_Dmain+0xb>

Tal vez el compilador podría ser aún más inteligente y almacenar en caché el subproceso local antes de un registro y devolverlo al hilo local al final (es interesante compararlo con el compilador gdc), pero incluso ahora las cosas son muy buenas en mi humilde opinión.


Hemos visto problemas de rendimiento similares de TLS (en Windows). Confiamos en él para ciertas operaciones críticas dentro del "kernel" de nuestro producto. Después de un esfuerzo, decidí intentar mejorarlo.

Me complace decir que ahora tenemos una pequeña API que ofrece> 50% de reducción en el tiempo de CPU para una operación equivalente cuando el hilo de callin no "conoce" su id de hilo y> 65% de reducción si el hilo de llamada ya tiene obtuvo su ID de hilo (tal vez para algún otro paso de procesamiento anterior).

La nueva función (get_thread_private_ptr ()) siempre devuelve un puntero a una estructura que usamos internamente para contener todos los géneros, por lo que solo necesitamos uno por hilo.

Con todo, creo que la compatibilidad con Win32 TLS está mal elaborada realmente.


Diseñé multitareas para sistemas integrados y, conceptualmente, el requisito clave para el almacenamiento local de subprocesos es que el método de cambio de contexto guarde / restaure un puntero al almacenamiento local de subprocesos junto con los registros de la CPU y cualquier otra cosa que esté guardando / restaurando. Para los sistemas integrados que siempre estarán ejecutando el mismo conjunto de códigos una vez que se hayan iniciado, es más fácil simplemente guardar / restaurar un puntero, que apunta a un bloque de formato fijo para cada subproceso. Agradable, limpio, fácil y eficiente.

Tal enfoque funciona bien si a uno no le importa tener espacio para cada variable local de subproceso asignada dentro de cada subproceso, incluso aquellas que nunca lo usan realmente, y si todo lo que va a estar dentro del bloque de almacenamiento local de subprocesos puede ser definido como una estructura única. En ese escenario, los accesos a las variables locales de subprocesos pueden ser casi tan rápidos como el acceso a otras variables, la única diferencia es una desreferencia de puntero adicional. Desafortunadamente, muchas aplicaciones de PC requieren algo más complicado.

En algunos frameworks para PC, un subproceso solo tendrá espacio asignado para variables estáticas de subproceso si un módulo que usa esas variables se ha ejecutado en ese subproceso. Si bien esto a veces puede ser ventajoso, significa que diferentes hilos a menudo tendrán su almacenamiento local distribuido de manera diferente. En consecuencia, puede ser necesario que los hilos tengan algún tipo de índice de búsqueda de dónde se encuentran sus variables, y para dirigir todos los accesos a esas variables a través de ese índice.

Me gustaría esperar que si el marco asigna una pequeña cantidad de almacenamiento de formato fijo, puede ser útil mantener un caché de las últimas 1-3 variables locales de subprocesos a los que se accede, ya que en muchos escenarios incluso un caché de un solo elemento podría ofrecer un tasa de éxito bastante alta.