c++ proceso - ¿Cómo estimar la sobrecarga de cambio de contexto del hilo?





manejo un (8)


El problema con los cambios de contexto es que tienen un tiempo fijo. La GPU implementó un cambio de contexto de 1 ciclo entre los hilos. Por ejemplo, los siguientes no pueden enhebrarse en la CPU:

double * a; 
...
for (i = 0; i < 1000; i ++)
{
    a[i] = a[i] + a[i]
}

porque su tiempo de ejecución es mucho menor que el costo de cambio de contexto. En Core i7 este código toma alrededor de 1 micro segundo (depende del compilador). Por lo tanto, el tiempo de cambio de contexto importa porque define cómo se pueden enhebrar los trabajos pequeños. Supongo que esto también proporciona un método para la medición efectiva del cambio de contexto. Compruebe cuánto tiempo debe durar la matriz (en el ejemplo superior) para que dos subprocesos del grupo de subprocesos comiencen a mostrar una ventaja real en comparación con uno solo con subprocesos. Esto puede convertirse fácilmente en 100 000 elementos y, por lo tanto, el tiempo de cambio de contexto efectivo estaría en el rango de 20us dentro de la misma aplicación.

Todas las encapsulaciones utilizadas por el grupo de subprocesos deben contabilizarse en el tiempo del conmutador de subprocesos, porque a eso se reduce todo (al final).

Atmapuri

Estoy tratando de mejorar el rendimiento de la aplicación de subprocesos con fechas límite en tiempo real. Se ejecuta en Windows Mobile y está escrito en C / C ++. Tengo la sospecha de que la alta frecuencia de la conmutación de hilos puede estar causando una sobrecarga tangible, pero no puedo probarlo ni desmentirlo. Como todos saben, la falta de pruebas no es una prueba de lo contrario :).

Por lo tanto, mi pregunta es doble:

  • Si existe, ¿dónde puedo encontrar alguna medida real del costo de cambiar el contexto del hilo?

  • Sin perder tiempo escribiendo una aplicación de prueba, ¿cuáles son las formas de estimar la sobrecarga de conmutación de subprocesos en la aplicación existente?

  • ¿Alguien sabe una forma de averiguar el número de interruptores de contexto (encendido / apagado) para un hilo dado?




No lo sé, pero ¿tiene los contadores de rendimiento habituales en Windows Mobile? Podrías mirar cosas como cambios de contexto / seg. No sé si hay alguno que mida específicamente el tiempo de cambio de contexto.




Mis 50 líneas de C ++ muestran para Linux (QuadCore Q6600) el tiempo de cambio de contexto ~ 0.9us (0.75us para 2 hilos, 0.95 para 50 hilos). En este punto de referencia, los hilos invocan el rendimiento inmediatamente cuando obtienen una cantidad de tiempo.




No puedes estimarlo Necesitas medirlo. Y va a variar dependiendo del procesador en el dispositivo.

Hay dos formas bastante simples de medir un cambio de contexto. Uno implica código, el otro no.

Primero, la forma del código (pseudocódigo):

DWORD tick;

main()
{
  HANDLE hThread = CreateThread(..., ThreadProc, CREATE_SUSPENDED, ...);
  tick = QueryPerformanceCounter();
  CeSetThreadPriority(hThread, 10); // real high
  ResumeThread(hThread);
  Sleep(10);
}

ThreadProc()
{
  tick = QueryPerformanceCounter() - tick;
  RETAILMSG(TRUE, (_T("ET: %i\r\n"), tick));
}

Obviamente, hacerlo en un bucle y promediar será mejor. Tenga en cuenta que esto no solo mide el cambio de contexto. También está midiendo la llamada a ResumeThread y no hay garantía de que el planificador cambie inmediatamente a su otro hilo (aunque la prioridad de 10 debería ayudar a aumentar las probabilidades de que así sea).

Puede obtener una medición más precisa con CeLog conectándose a los eventos del programador, pero está lejos de ser simple y no está muy bien documentada. Si realmente quieres seguir esa ruta, Sue Loh tiene varios blogs que un motor de búsqueda puede encontrar.

La ruta sin código sería usar Remote Kernel Tracker. Instale eVC 4.0 o la versión eval de Platform Builder para obtenerlo. Le dará una visualización gráfica de todo lo que está haciendo el núcleo y usted puede medir directamente un cambio de contexto de hilo con las capacidades del cursor proporcionadas. Una vez más, estoy seguro de que Sue tiene una entrada en el blog sobre el uso de Kernel Tracker también.

Dicho todo esto, descubrirá que los conmutadores de contexto de subprocesos de CE son realmente muy rápidos. Son los switches de proceso los que son caros, ya que requiere intercambiar el proceso activo en la RAM y luego realizar la migración.




Aunque dijiste que no querías escribir una aplicación de prueba, hice esto para una prueba previa en una plataforma Linux ARM9 para averiguar cuál es la sobrecarga. Solo se trata de dos subprocesos que impulsarían :: thread :: yield () (o, ya sabes) e incrementarían alguna variable, y después de un minuto aproximadamente (sin otros procesos en ejecución, al menos ninguno que haga algo), la aplicación imprimió cuántos cambios de contexto podría hacer por segundo. Por supuesto, esto no es realmente exacto, pero el punto es que ambos hilos produjeron la CPU entre sí, y fue tan rápido que ya no tenía sentido pensar en la sobrecarga. Por lo tanto, simplemente siga adelante y simplemente escriba una prueba simple en lugar de pensar demasiado acerca de un problema que puede ser inexistente.

Aparte de eso, puede intentar como 1800 sugerido con contadores de rendimiento.

Ah, y recuerdo una aplicación que se ejecuta en Windows CE 4.X, donde también tenemos cuatro hilos con conmutación intensiva a veces, y nunca se encontró con problemas de rendimiento. También tratamos de implementar el núcleo de subprocesamiento sin hilos en absoluto, y no vimos ninguna mejora en el rendimiento (la GUI simplemente respondió mucho más lento, pero todo lo demás era igual). Quizás puedas intentar lo mismo, ya sea reduciendo la cantidad de interruptores de contexto o eliminando hilos por completo (solo para probar).




Dudo que pueda encontrar esta sobrecarga en algún lugar de la web para cualquier plataforma existente. Existen demasiadas plataformas diferentes. La sobrecarga depende de dos factores:

  • La CPU, ya que las operaciones necesarias pueden ser más fáciles o más difíciles en diferentes tipos de CPU
  • El kernel del sistema, ya que diferentes kernels tendrán que realizar diferentes operaciones en cada switch

Otros factores incluyen cómo se produce el cambio. Un cambio puede tener lugar cuando

  1. el hilo ha utilizado todo su tiempo cuántico. Cuando se inicia un hilo, puede ejecutarse durante un tiempo determinado antes de devolver el control al núcleo que decidirá quién será el siguiente.

  2. el hilo fue adelantado. Esto sucede cuando otro hilo necesita tiempo de CPU y tiene una prioridad más alta. Por ejemplo, el hilo que maneja la entrada del mouse / teclado puede ser un hilo. Independientemente del hilo que posea la CPU en este momento, cuando el usuario escribe algo o hace clic en algo, no quiere esperar hasta que el tiempo actual de los subprocesos se haya agotado por completo, quiere que el sistema reaccione de inmediato. Por lo tanto, algunos sistemas harán que el hilo actual se detenga inmediatamente y devuelva el control a otro hilo con mayor prioridad.

  3. el hilo ya no necesita más tiempo de CPU, porque está bloqueando alguna operación o simplemente se ha llamado a sleep () (o similar) para detener la ejecución.

Estos 3 escenarios pueden tener diferentes tiempos de cambio de hilo en teoría. Por ejemplo, esperaría que el último sea el más lento, ya que una llamada a suspensión () significa que la CPU se devuelve al kernel y el kernel necesita configurar una llamada de activación que asegure que el hilo se despierte después de aproximadamente la cantidad de tiempo que solicitó para dormir, luego debe quitar el hilo del proceso de programación, y una vez que el hilo se despierta, debe agregar el hilo nuevamente al proceso de programación. Todas estas pendientes tomarán una cierta cantidad de tiempo. Entonces, la llamada de espera real podría ser más larga que el tiempo que lleva cambiar a otra conversación.

Creo que si quieres saber con certeza, debes comparar. El problema es que, por lo general, tendrá que poner hilos a dormir o debe sincronizarlos utilizando mutexes. Dormir o bloquear / desbloquear mutexes tiene una sobrecarga. Esto significa que su punto de referencia incluirá estos gastos generales también. Sin tener un perfilador potente, es difícil decir cuánto tiempo de CPU se utilizó para el conmutador real y cuánto para la llamada de suspensión / exclusión mutua. Por otro lado, en un escenario de la vida real, tus hilos también dormirán o sincronizarán a través de los bloqueos. Un punto de referencia que mide puramente el tiempo de cambio de contexto es un punto de referencia sintético ya que no modela ningún escenario de la vida real. Los puntos de referencia son mucho más "realistas" si se basan en escenarios de la vida real. ¿De qué sirve una referencia de GPU que me dice que mi GPU puede manejar en teoría 2 mil millones de polígonos por segundo, si este resultado nunca se puede lograr en una aplicación 3D de la vida real? ¿No sería mucho más interesante saber cuántos polígonos puede tener una aplicación 3D real en GPU por segundo?

Desafortunadamente no sé nada de la programación de Windows. Podría escribir una aplicación para Windows en Java o quizás en C #, pero C / C ++ en Windows me hace llorar. Solo puedo ofrecerte un código fuente para POSIX.

#include <stdlib.h>
#include <stdint.h>
#include <stdio.h>
#include <pthread.h>
#include <sys/time.h>
#include <unistd.h>

uint32_t COUNTER;
pthread_mutex_t LOCK;
pthread_mutex_t START;
pthread_cond_t CONDITION;

void * threads (
    void * unused
) {
    // Wait till we may fire away
    pthread_mutex_lock(&START);
    pthread_mutex_unlock(&START);

    pthread_mutex_lock(&LOCK);
    // If I'm not the first thread, the other thread is already waiting on
    // the condition, thus Ihave to wake it up first, otherwise we'll deadlock
    if (COUNTER > 0) {
        pthread_cond_signal(&CONDITION);
    }
    for (;;) {
        COUNTER++;
        pthread_cond_wait(&CONDITION, &LOCK);
        // Always wake up the other thread before processing. The other
        // thread will not be able to do anything as long as I don't go
        // back to sleep first.
        pthread_cond_signal(&CONDITION);
    }
    pthread_mutex_unlock(&LOCK); //To unlock
}

int64_t timeInMS ()
{
    struct timeval t;

    gettimeofday(&t, NULL);
    return (
        (int64_t)t.tv_sec * 1000 +
        (int64_t)t.tv_usec / 1000
    );
}


int main (
    int argc,
    char ** argv
) {
    int64_t start;
    pthread_t t1;
    pthread_t t2;
    int64_t myTime;

    pthread_mutex_init(&LOCK, NULL);
    pthread_mutex_init(&START, NULL);   
    pthread_cond_init(&CONDITION, NULL);

    pthread_mutex_lock(&START);
    COUNTER = 0;
    pthread_create(&t1, NULL, threads, NULL);
    pthread_create(&t2, NULL, threads, NULL);
    pthread_detach(t1);
    pthread_detach(t2);
    // Get start time and fire away
    myTime = timeInMS();
    pthread_mutex_unlock(&START);
    // Wait for about a second
    sleep(1);
    // Stop both threads
    pthread_mutex_lock(&LOCK);
    // Find out how much time has really passed. sleep won't guarantee me that
    // I sleep exactly one second, I might sleep longer since even after being
    // woken up, it can take some time before I gain back CPU time. Further
    // some more time might have passed before I obtained the lock!
    myTime = timeInMS() - myTime;
    // Correct the number of thread switches accordingly
    COUNTER = (uint32_t)(((uint64_t)COUNTER * 1000) / myTime);
    printf("Number of thread switches in about one second was %u\n", COUNTER);
    return 0;
}

Salida

Number of thread switches in about one second was 108406

Más de 100,000 no es tan malo y eso a pesar de que tenemos esperas bloqueadas y condicionales. Supongo que sin todas estas cosas al menos el doble de conmutadores de subprocesos serían posibles por segundo.




¡Solo he intentado estimar esto una vez y eso fue en un 486! El resultado fue que el cambio de contexto del procesador requería alrededor de 70 instrucciones para completar (obsérvese que esto estaba sucediendo para muchas llamadas a la API del sistema operativo así como para el cambio de subprocesos). Calculamos que tomaba aproximadamente 30us por cambio de hilo (incluida la sobrecarga del sistema operativo) en un DX3. Los pocos miles de conmutadores de contexto que estábamos haciendo por segundo absorbían entre el 5 y el 10% del tiempo del procesador.

¿Cómo se traduciría esto en un procesador moderno multi-core, multi-ghz? No lo sé, pero supongo que a menos que estuvieras yendo por encima con el cambio de hilo es una carga insignificante.

Tenga en cuenta que la creación / eliminación de subprocesos es un enrutador más costoso de CPU / OS que la activación / desactivación de subprocesos. Una buena política para aplicaciones con muchos subprocesos es usar grupos de subprocesos y activar / desactivar según sea necesario.




Si usa Runnable, puede guardar el espacio para extenderlo a cualquiera de sus otras clases.





c++ c multithreading windows-mobile