Guía práctica para lograr el máximo de soft real-time posible en un Windows convencional

· Actualizado el: · · Desarrollo en Windows, Soft real-time, Diseño, Medición

Cuando se desarrollan en Windows procesos como el procesamiento periódico, el audio, el video, la medición o el control de equipos —es decir, procesos donde «un retraso es un problema»—, suele darse la impresión de que «en Windows va a ser complicado». Esa impresión es en parte acertada y en parte no: Windows no es un sistema operativo hard real-time, pero si se cuidan bien el diseño, la implementación, la medición y la operación, se puede llegar a un estado bastante práctico como soft real-time.

En este artículo se trata un Windows 10/11 convencional, sin dar por supuesto ninguna extensión RTOS especial, ningún controlador de kernel propio ni ningún controlador dedicado. El tema es más bien práctico: hasta dónde se pueden reducir la latencia y el jitter en una aplicación en modo usuario sobre el PC de escritorio o portátil habitual. Aunque los detalles difieren entre audio, video, control periódico y adquisición de datos, los puntos que suelen causar problemas son bastante comunes, así que aquí se reúne esa parte común en forma de lista de verificación.

Público objetivo y el idioma de los ejemplos de código

Este artículo está escrito para quienes desarrollan en Windows procesos donde «un retraso es un problema» (control periódico, audio y video, medición, control de equipos). Se asume el desarrollo de aplicaciones en modo usuario; la implementación de controladores en modo kernel queda fuera de alcance.

El idioma de los ejemplos de código se divide de la siguiente manera.

Contenido Lenguaje Ubicación
Partes que llaman directamente a la API Win32, como el bucle periódico, MMCSS o el QoS de energía C++ (Win32) 4.1, 4.3, 4.5
La misma API Win32 invocada desde C# C# (P/Invoke) 4.5
Medición de tiempo, GC y precauciones sobre la asignación de memoria .NET (C#) «Comprobaciones del lado de .NET» en 4.4, 5.2

La explicación de las causas hasta el capítulo 3 y el cuerpo de la lista de verificación del capítulo 4 no dependen del lenguaje. Quienes solo usen C# pueden leer el código en C++ simplemente como una explicación de «qué API se llama y en qué orden».

Índice

  1. Primero, la conclusión (en una frase)
    • 1.1. Tabla rápida por rango de periodo (por dónde empezar a leer)
  2. Qué es el «soft real-time» en un Windows convencional
    • 2.1. Qué entendemos aquí por «Windows convencional»
    • 2.2. Qué se puede hacer y a partir de dónde se complica
    • 2.3. Antes que nada, un vistazo a los términos
  3. Principales causas de latencia y jitter
    • 3.1. El planificador y las prioridades
    • 3.2. DPC/ISR y los controladores
    • 3.3. Fallos de página y memoria
    • 3.4. Resolución del temporizador y gestión de energía
    • 3.5. Migración entre núcleos y calor
  4. Lista de verificación práctica para reducir retrasos en un Windows convencional
    • 4.1. El bucle periódico y el método de espera
    • 4.2. Fast path, slow path y la cola de longitud fija
    • 4.3. Prioridad, MMCSS y background mode
    • 4.4. Memoria, GC y el coste de la primera ejecución
    • 4.5. Configuración de energía, EcoQoS y la resolución del temporizador
    • 4.6. Ubicación de CPU, migración entre núcleos y calor
    • 4.7. Aislar controladores, DPC/ISR y otras perturbaciones
  5. Medición y evaluación
    • 5.1. Qué se debe registrar
    • 5.2. Cómo leer p99, p99.9 y max
    • 5.3. Con qué herramientas mirar
    • 5.4. Buenas prácticas para las pruebas
  6. Criterios generales para decidir
  7. Resumen
  8. Referencias

1. Primero, la conclusión (en una frase)

  • En un Windows convencional, el objetivo no es garantizar hard real-time, sino lograr, como soft real-time, una configuración que «se retrase poco y no se rompa aunque se retrase».
  • Lo que más impacto tiene es hacer el hot path corto, de longitud fija y no bloqueante.
  • Se separan el fast path (adquisición / control) y el slow path (guardado / comunicación / interfaz), conectados por una cola de longitud fija.
  • El bucle periódico no se deja en manos de Sleep(1), sino que gira con un plazo absoluto.
  • En flujos continuos como el audio o el video, lo primero que hay que considerar es MMCSS.
  • Para medir el tiempo se usa QueryPerformanceCounter (QPC), o Stopwatch en .NET.
  • Para esperar, se da preferencia a un evento de dispositivo o a un waitable timer de alta precisión (temporizador esperable).
  • timeBeginPeriod se usa solo mientras haga falta. No se diseña asumiendo que estará activo todo el tiempo.
  • En el uso real ayudan la alimentación de CA, el modo de energía, el tratamiento de EcoQoS y ordenar la carga en segundo plano.
  • La evaluación no se basa solo en el promedio, sino en p99 (el límite a partir del cual, en 100 mediciones, empieza a verse 1 caso lento), p99.9, max, el número de miss, DPC/ISR, page fault y la profundidad de la cola.

En resumen, en un Windows convencional es más eficaz reducir mediante el diseño las causas del retraso que subir la prioridad. La prioridad y la configuración de energía son importantes, pero por sí solas no bastan para lograr estabilidad.

1.1. Tabla rápida por rango de periodo (por dónde empezar a leer)

Como es un artículo largo, se coloca primero esta tabla rápida para que pueda leer solo la fila que corresponde a su caso. Este contenido, que antes estaba al final (capítulo 6), se ha adelantado aquí.

Periodo / requisito Configuración inicial Secciones clave
Del orden de 10-20 ms, con capacidad de absorber alguna variación ocasional Separar fast path y slow path, cola de longitud fija, prioridad normal a algo alta, orientado a eventos. Con esto suele bastar 4.1, 4.2
Del orden de 1-5 ms, con necesidad de cumplir el plazo de forma continua Además de lo anterior, hot path sin asignaciones, hilo dedicado, MMCSS o un ajuste cuidadoso de prioridad, waitable timer de alta precisión, y revisar la alimentación de CA y la configuración de energía De 4.1 a 4.5
Cerca de menos de 1 ms, sin margen para fallar ni en funcionamiento prolongado ni con carga alta Muy difícil de lograr solo con modo usuario en un Windows convencional. Conviene estudiar primero sacar la parte crítica a otro lugar (firmware del propio dispositivo, controlador dedicado, FPGA, RTOS) 2.2, 6
Quiere hacerlo convivir todo con GUI / registro / comunicación / BD No cargarlo todo en «un proceso, un bucle»: separar responsabilidades. Las necesidades de las etapas posteriores tienden a romper el plazo de las anteriores 4.2, 4.3, 6

El aislamiento de causas y las buenas prácticas de medición son comunes a cualquier rango (capítulos 3 y 5).

2. Qué es el «soft real-time» en un Windows convencional

2.1. Qué entendemos aquí por «Windows convencional»

Lo que aquí llamamos Windows convencional parte, a grandes rasgos, de los siguientes supuestos.

  • Un PC de escritorio o portátil habitual con Windows 10/11
  • Sin extensiones RTOS propias
  • Sin desarrollo de controladores en modo kernel propios
  • Una aplicación normal en modo usuario
  • Ajustes realizados con la API y la configuración habituales de Windows

En otras palabras, no se trata de «construir un equipo dedicado entero para control en tiempo real», sino de «hasta dónde se puede llegar de forma realista sobre un PC Windows convencional».

Objetivo del soft real-time en un Windows convencionalDiagrama que muestra cómo una app en modo usuario sobre un PC Windows 10/11 convencional persigue el soft real-time reduciendo la latencia, el jitter y observando los deadline miss sin romperse, frente a la vía alternativa de garantizar cero incumplimientos de plazo mediante RTOS, controladores dedicados, FPGA o procesamiento en el propio dispositivo.PC Windows 10 / 11 convencionalAplicación en modo usuarioPersigue el soft real-timeReduce la latenciaReduce el jitterObserva los deadline miss para no romperseQuiere garantizar cero incumplimientos de plazoRTOS / controlador dedicado / FPGA / procesamiento en el dispositivo

2.2. Qué se puede hacer y a partir de dónde se complica

Incluso en un Windows convencional, procesos como los siguientes permiten lograr, de forma bastante realista, un estado en el que «cuesta que se retrasen».

  • Procesamiento periódico de pocos a varias decenas de milisegundos
  • Manejo de audio/video por búfer
  • Adquisición de sensores y bucles de control
  • Procesamiento a periodo constante al estilo de un PLC por software
  • Canalizaciones de baja latencia que corren en un hilo separado de la interfaz de usuario

Sin embargo, ese «se puede» no significa que se puedan eliminar por completo los picos de retraso ocasionales; lo que en realidad se persigue es este otro estado.

  • Mantener baja la latencia en condiciones normales
  • Reducir el jitter
  • No romperse aunque ocasionalmente se incumpla el plazo
  • Poder observar el hecho de que se incumplió

Por el contrario, cuando el requisito se convierte en lo siguiente, cumplirlo únicamente con modo usuario en un Windows convencional resulta bastante difícil.

  • Garantizar cero incumplimientos de plazo
  • Mantener de forma estable y prolongada un margen por debajo de cientos de microsegundos
  • Convivir con una GUI pesada, red y almacenamiento
  • Hacerlo funcionando con batería o priorizando el ahorro de energía
  • No admitir tampoco picos originados por controladores o dispositivos

En estos casos es más seguro considerar trasladar únicamente la parte realmente crítica en tiempo al firmware del propio dispositivo, a un controlador dedicado, a un FPGA o a un RTOS.

2.3. Antes que nada, un vistazo a los términos

Antes de continuar, conviene tener claro el significado de los términos que aparecen en este artículo.

Término En una frase Cómo se usa en la práctica
soft real-time Puede haber algún retraso ocasional, pero la idea es reducirlo y no romperse aunque ocurra Es lo primero que se busca en un Windows convencional
hard real-time El mundo donde se garantiza cero incumplimientos de plazo No es un objetivo alcanzable solo con modo usuario en un Windows convencional
jitter Variación del periodo o del tiempo de respuesta Aunque el promedio sea bueno, un jitter grande vuelve inestable el funcionamiento real
deadline miss Que el procesamiento no termine antes de la hora prevista No ocultarlo: contarlo y registrarlo en el log
p99 / p99.9 Indicadores que muestran la cola de los valores más lentos p99 es «el límite a partir del cual empieza a verse 1 de cada 100 casos lentos»
DPC / ISR Procesamiento del lado del kernel relacionado con controladores e interrupciones Si duran mucho, los hilos en modo usuario quedan a la espera
MMCSS Mecanismo de Windows que asigna CPU a procesos sensibles al tiempo, como audio o video Muy eficaz en procesos que no deben quedarse sin datos en el búfer
QPC Se refiere a QueryPerformanceCounter Base para medir tiempo transcurrido: un contador de alta precisión, no un reloj de pared
waitable timer (temporizador esperable) Objeto del kernel que pasa a estado signaled en el momento indicado. Al añadir CREATE_WAITABLE_TIMER_HIGH_RESOLUTION a CreateWaitableTimerExW se obtiene la versión de alta precisión Más adecuado que Sleep como base de la espera periódica (4.1)
EcoQoS Estado clasificado como «puede priorizarse el ahorro de energía». Reduce la frecuencia de la CPU o desplaza el trabajo hacia núcleos más eficientes Evitarlo en procesos sensibles al tiempo. Si no se indica lo contrario, el sistema operativo lo infiere automáticamente (4.5)
IGNORE_TIMER_RESOLUTION Indicación de que puede ignorarse la solicitud de resolución del temporizador del proceso (PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION) Si está activo, el efecto de timeBeginPeriod desaparece. En Windows 11 puede aplicarse automáticamente al ocultarse de la pantalla (4.5)
CPU Sets Mecanismo para indicar de forma flexible que un hilo o proceso «se ejecute preferentemente en este grupo de núcleos» Probarlo antes de fijar núcleos específicos (4.6)
ETW / WPR / WPA La infraestructura estándar de trazas de Windows (ETW) junto con su herramienta de registro (WPR) y su GUI de análisis (WPA) Se usa para investigar context switch, DPC/ISR y page fault (5.3)
LatencyMon Herramienta de terceros para observar retrasos causados por controladores Permite ver el tiempo de ejecución de DPC/ISR por controlador y detectar sospechosos (5.3)

3. Principales causas de latencia y jitter

En un Windows convencional, los motivos por los que se retrasa el procesamiento periódico suelen reducirse a alguno de los que aparecen en este diagrama.

Causas principales de retraso y jitterDiagrama que muestra que un retraso en el procesamiento periódico puede originarse en el planificador y las prioridades, en DPC/ISR y los controladores, en fallos de página y memoria, en la resolución del temporizador y la gestión de energía, o en la migración entre núcleos y el calor.El procesamiento periódico se retrasaPlanificador / prioridadesDPC / ISR / controladoresFallos de página / memoriaResolución del temporizador / gestión de energíaMigración entre núcleos / calor

3.1. El planificador y las prioridades

En Windows, el orden de ejecución de los hilos se determina por la prioridad. Entre hilos de la misma prioridad se rota en round-robin, y en cuanto un hilo de mayor prioridad queda listo para ejecutarse, desplaza a los de menor prioridad.

Es decir, aunque el hilo periódico esté bien escrito,

  • Otro hilo
  • Otro proceso
  • Procesamiento interno del sistema operativo
  • Productos de seguridad
  • Procesamiento auxiliar de dispositivos
  • Sincronización en segundo plano

pueden perfectamente ejecutarse antes.

3.2. DPC/ISR y los controladores

Este punto es bastante importante. Aunque se ajuste la prioridad del lado de la aplicación, si el DPC (Deferred Procedure Call) o el ISR (Interrupt Service Routine) duran mucho, durante ese tiempo los hilos en modo usuario no pueden ejecutarse.

Los dispositivos y controladores que suelen originar esto son, entre otros, los siguientes.

  • USB
  • Wi-Fi / Bluetooth
  • Almacenamiento
  • Audio
  • GPU
  • ACPI y lo relacionado con la energía

Aunque el código de la aplicación no tenga ningún defecto, puede verse detenido por causas del controlador o del hardware. En este punto, pensar que «si subo aún más la prioridad de la aplicación, ganaré» suele terminar mal.

3.3. Fallos de página y memoria

Si en el hot path ocurre un page fault (la página necesaria no está en memoria y hay que ir a buscarla), la latencia se dispara de golpe.

A continuación se listan los patrones que conviene evitar en especial.

  • Confirmación (commit) de página en el primer acceso
  • Carga diferida (lazy loading)
  • Paginación de entrada de archivos mapeados en memoria
  • Asignación dinámica más allá de lo necesario
  • Objetos grandes o un heap fragmentado

En el cuerpo del procesamiento periódico, lo adecuado es, más o menos, reservar de antemano la memoria necesaria y tocarla una vez al iniciar.

3.4. Resolución del temporizador y gestión de energía

«Quiero que se ejecute cada 1 ms, así que uso Sleep(1)» casi nunca funciona bien. La precisión de la espera en Windows depende de la resolución del temporizador, la planificación y el estado de energía.

Además, no hay que pasar por alto que la configuración que aumenta la resolución del temporizador mejora algo la precisión de la espera, pero tiene efectos secundarios sobre el consumo de energía y el comportamiento general del sistema.

3.5. Migración entre núcleos y calor

Cuando un hilo migra entre núcleos, la caché tiene que volver a «calentarse». Esto en sí mismo suele estar bien gestionado por el sistema operativo, pero en entornos con carga alta se convierte en una causa de variación.

Además, cuando se funciona durante mucho tiempo, el calor tampoco se puede ignorar. Si entra en juego el thermal throttling, el periodo que hasta entonces era estable puede desmoronarse.

4. Lista de verificación práctica para reducir retrasos en un Windows convencional

A partir de aquí empieza la parte práctica. Frente a las causas vistas en la sección anterior, se resume en forma de lista de verificación qué comprobar, qué evitar y qué decidir de antemano en un Windows convencional.

4.1. El bucle periódico y el método de espera

El antipatrón típico es este.

while (running)
{
    Sleep(1);
    Step();
}

Esto no es «un periodo de 1 ms», sino un bucle que espera al menos 1 ms, aproximadamente, y le suma encima el tiempo de ejecución de Step(). Y además, el exceso de espera se va acumulando tal cual.

Bucle periódico basado en tiempo relativo frente a plazo absolutoComparación entre un bucle basado en tiempo relativo, donde la espera fija y el paso de ejecución acumulan poco a poco el error de espera y el tiempo de cómputo, y un bucle basado en plazo absoluto, donde next += period, WaitUntil, un spin corto y FastStep evitan que se acumule la deriva.Basado en plazo absolutoBasado en tiempo relativoWaitUntil(next - margin)next += periodSpin corto si hace faltaFastStep()Step()Sleep(1)El error de espera y el tiempo de ejecución se van acumulandoDifícilmente acumula deriva

Lista de verificación

  • No se usa Sleep(1) como base del bucle periódico
  • El periodo gira con el plazo absoluto de next += period
  • La espera da preferencia a un evento de dispositivo o a un waitable timer (temporizador esperable)
  • Solo el ajuste final se limita a un busy-spin (espera activa) muy corto
  • timeBeginPeriod se usa solo mientras haga falta, revirtiéndolo después
  • Se comprueba el comportamiento también minimizado, oculto o cuando no es visible

El bucle periódico es más estable si gira con un plazo absoluto en lugar de con tiempo relativo.

int64_t next = QpcNow() + periodTicks;

while (running)
{
    WaitUntil(next - wakeMarginTicks);

    while (QpcNow() < next)
    {
        CpuRelax(); // Solo al final, spin corto
    }

    int64_t started = QpcNow();
    FastStep();
    int64_t finished = QpcNow();

    RecordTiming(next, started, finished);

    next += periodTicks;

    while (finished > next)
    {
        ++missedDeadlines;
        next += periodTicks;
    }
}

4.2. Fast path, slow path y la cola de longitud fija

La base de la arquitectura es colocar en el fast path solo el «trabajo sensible al plazo» y desplazar todo lo demás al slow path.

Separación entre fast path y slow path con cola de longitud fijaDiagrama en el que el evento del dispositivo o de captura entra al fast path de adquisición, control y copia mínima, pasa por una cola de longitud fija hacia el slow path de guardado, envío, interfaz y agregación, mientras el fast path registra lateness, miss y profundidad de cola hacia el slow path.Dispositivo / evento de capturafast path: adquisición, control, copia mínimaCola de longitud fijaslow path: guardado, envío, interfaz, agregaciónRegistra lateness / miss / profundidad de cola

Lo que se hace en el fast path se limita, más o menos, a lo siguiente.

  • Adquisición de datos
  • Cálculo del valor de control
  • Copia mínima necesaria
  • Marca de tiempo
  • Inserción en la cola
  • Registro de miss / overrun

Todo lo demás se deja caer al slow path.

Lista de verificación

  • No se escribe a archivo, se envía por red ni se escribe en BD dentro del hot path
  • No se hace registro de log pesado, Flush ni RPC síncrono dentro del hot path
  • El fast path y el slow path están claramente separados por hilo o por responsabilidad
  • La cola tiene longitud fija
  • Se ha decidido de antemano la política para cuando la cola se desborde
  • Se observan el número de miss, el número de drop y la profundidad de la cola
  • La actualización de la interfaz y la agregación de logs están separadas hacia un periodo más bajo

Cuando la cola se llena, es más seguro no dejar la política en la ambigüedad.

Política ante el desbordamiento de la colaDiagrama de decisión que, ante una cola llena, pregunta qué se debe proteger, y muestra tres respuestas: descartar los elementos antiguos y conservar el más reciente, emitir una alerta o detener el flujo aguas arriba, o descartar los elementos antiguos y solo registrar el número de descartes.El valor más reciente importaTodos los elementos importanUso para registroLa cola está llena¿Qué se debe proteger?Descarta los elementos antiguos y conserva el más recienteAlerta / detención / control aguas arribaDescarta los elementos antiguos y solo registra el número de descartes

4.3. Prioridad, MMCSS y background mode

La base de la prioridad es no subirlo todo. En un Windows convencional funciona mejor «subir solo los hilos importantes y bajar bien el trabajo secundario». El background mode es un mecanismo para tratar con prioridad más baja no solo la CPU, sino también recursos como la E/S.

Reparto de prioridades por tipo de trabajoDiagrama que separa el trabajo en hilos sensibles al plazo, trabajo de guardado, envío, compresión y agregación, e interfaz de usuario, asignando a los primeros una prioridad más alta o MMCSS si hace falta, a los segundos background mode o una prioridad más baja, a la interfaz la prioridad normal, y advirtiendo no usar REALTIME_PRIORITY_CLASS desde el principio.Reparte el trabajoHilos sensibles al plazoGuardado / envío / compresión / agregaciónInterfaz de usuarioPrioridad más alta o MMCSS si hace faltabackground mode / prioridad más bajaPrioridad normalNo empezar directamente con REALTIME_PRIORITY_CLASS

Lista de verificación

  • No se pone a todos los hilos en prioridad alta
  • Solo se sube la prioridad de los hilos realmente exigentes en tiempo
  • El trabajo secundario (guardado, envío, compresión, sincronización) se deja en background mode
  • Se considera MMCSS en el procesamiento continuo por búfer, como audio, video, captura o reproducción
  • Se piensa primero por hilo, más que por el proceso completo
  • No se usa REALTIME_PRIORITY_CLASS hasta que la necesidad quede clara

MMCSS (Multimedia Class Scheduler Service) es especialmente eficaz en procesos como el audio o el video, donde se quiere «llenar el búfer dentro de un tiempo determinado». Se ajusta mejor al diseño de Windows que simplemente mantener siempre un hilo de alta prioridad en ejecución.

El aspecto del código es más o menos así.

DWORD taskIndex = 0;
HANDLE avrt = AvSetMmThreadCharacteristicsW(L"Pro Audio", &taskIndex);
if (!avrt)
{
    throw std::runtime_error("AvSetMmThreadCharacteristicsW failed");
}

// Ejecutar aquí el bucle sensible al tiempo

if (!AvRevertMmThreadCharacteristics(avrt))
{
    throw std::runtime_error("AvRevertMmThreadCharacteristics failed");
}

4.4. Memoria, GC y el coste de la primera ejecución

Si en el hot path se usan cada vez new, malloc, List<T>.Add, concatenación de cadenas o LINQ, tarde o temprano saldrán a la superficie los efectos de la recolección o la reubicación. El GC (recolector de basura) en sí no es malo, pero si se escribe código con muchas asignaciones, su efecto se manifiesta como jitter.

Secuencia de calentamiento antes de medirDiagrama que muestra la secuencia desde el arranque hasta reservar los búferes necesarios, tocarlos una vez para calentar las páginas, completar el JIT, la carga de DLL y la E/S inicial, y solo después realizar la medición o el funcionamiento real.ArranqueReserva los búferes necesariosLos toca una vez para calentar las páginasCompleta el JIT, la carga de DLL y la E/S inicialDespués realiza la medición o el funcionamiento real

Lista de verificación

  • No se hace asignación/liberación de memoria en cada iteración del hot path
  • Los búferes necesarios se reservan de antemano al iniciar
  • Al iniciar, se tocan una vez para calentar las páginas
  • El JIT inicial, la primera carga de DLL y la E/S inicial no se mezclan con la medición real
  • No se dejan crecer estructuras enormes ni logs de longitud variable dentro del bucle
  • Si se usa VirtualLock, se limita a una región crítica muy pequeña

Comprobaciones del lado de .NET

  • Se usa Stopwatch / Stopwatch.GetTimestamp() para medir el tiempo
  • No se usan LINQ, concatenación de cadenas, ToString() ni generación de logs enormes en el hot path
  • No se introduce async/await en el hot path
  • Se evalúa por separado antes y después del calentamiento

4.5. Configuración de energía, EcoQoS y la resolución del temporizador

Este punto es poco vistoso, pero es eficaz. Por mucho que se afine el código, si el control de energía de nivel superior tiene un efecto fuerte, el resultado no será estable.

Gestión de energía en un Windows convencionalDiagrama que agrupa bajo la gestión de energía de un Windows convencional la ejecución con alimentación de CA, un modo de energía orientado al máximo rendimiento, un plan de energía dedicado para producción si hace falta, evitar EcoQoS en procesos sensibles al tiempo, y comprobar el tratamiento de las solicitudes de resolución del temporizador.Gestión de energía en un Windows convencionalEjecutar con alimentación de CAModo de energía: orientado al máximo rendimientoPlan de energía dedicado para producción si hace faltaEvitar EcoQoS en procesos sensibles al tiempoComprobar el tratamiento de las solicitudes de resolución del temporizador

Lista de verificación

  • La evaluación de producción se realiza primero con alimentación de CA
  • [Configuración] > [Sistema] > [Energía y batería] > [Modo de energía] está orientado hacia máximo rendimiento
  • No se usa el modo de ahorro de batería ni un modo que priorice el ahorro de energía durante la ejecución
  • Se comprueban los modos silencioso, eco o de prioridad de batería de utilidades propias del fabricante
  • No se pone descuidadamente en EcoQoS (QoS orientado al ahorro de energía) a un proceso sensible al tiempo
  • IGNORE_TIMER_RESOLUTION no está activo en el lado del proceso sensible al tiempo
  • Se comprueba si el efecto de la solicitud de resolución del temporizador cambia al minimizar u ocultar la ventana
  • Se separa la configuración de energía de uso habitual de la de producción/medición/demostración

timeBeginPeriod es útil si se usa de forma ordenada, pero no es una solución milagrosa.

  • Se llama justo antes de necesitarlo
  • Al terminar, se revierte con timeEndPeriod
  • Desde Windows 10 versión 2004, ya no tiene el comportamiento global completo de antes
  • En Windows 11, si un proceso con ventana queda completamente oculto, minimizado, invisible o inaudible, es posible que no se garantice una resolución alta
  • Aumentar la resolución no mejora la precisión de QPC

Si se sospecha del efecto de la energía o del QoS, se puede comprobar el estado del power throttling con SetProcessInformation.

PROCESS_POWER_THROTTLING_STATE state{};
state.Version = PROCESS_POWER_THROTTLING_CURRENT_VERSION;
state.ControlMask =
    PROCESS_POWER_THROTTLING_EXECUTION_SPEED |
    PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION;
state.StateMask = 0; // HighQoS (orientado al rendimiento) + respeta la solicitud de resolución del temporizador

if (!SetProcessInformation(
        GetCurrentProcess(),
        ProcessPowerThrottling,
        &state,
        sizeof(state)))
{
    throw std::runtime_error("SetProcessInformation failed");
}

ControlMask indica «qué mecanismos se controlan uno mismo», y StateMask indica «si ese mecanismo se activa o se desactiva». En el ejemplo anterior se eligen dos mecanismos como objeto de control y ambos se desactivan (off). Es decir, se declara no degradar a EcoQoS (orientado a HighQoS) y no dejar que se ignore la solicitud de resolución del temporizador. Por el contrario, si se pone ControlMask en 0, ambos vuelven a quedar en manos del sistema operativo (comportamiento predeterminado).

Cómo llamarlo desde C#

Si se quiere hacer lo mismo desde C#, la declaración P/Invoke queda así.

// C# / .NET 8
using System.Runtime.InteropServices;

internal static class PowerQos
{
    [StructLayout(LayoutKind.Sequential)]
    private struct PROCESS_POWER_THROTTLING_STATE
    {
        public uint Version;
        public uint ControlMask;
        public uint StateMask;
    }

    private const uint PROCESS_POWER_THROTTLING_CURRENT_VERSION = 1;
    private const uint PROCESS_POWER_THROTTLING_EXECUTION_SPEED = 0x1;
    private const uint PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION = 0x4;

    // El quinto valor de PROCESS_INFORMATION_CLASS (4 empezando desde 0) es ProcessPowerThrottling
    private const int ProcessPowerThrottling = 4;

    [DllImport("kernel32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool SetProcessInformation(
        IntPtr hProcess,
        int processInformationClass,
        ref PROCESS_POWER_THROTTLING_STATE processInformation,
        uint processInformationSize);

    [DllImport("kernel32.dll")]
    private static extern IntPtr GetCurrentProcess();

    /// <summary>Anula tanto el tratamiento orientado al ahorro de energía como la ignorancia de la solicitud de resolución del temporizador.</summary>
    public static void OptOutOfPowerThrottling()
    {
        var state = new PROCESS_POWER_THROTTLING_STATE
        {
            Version = PROCESS_POWER_THROTTLING_CURRENT_VERSION,
            ControlMask =
                PROCESS_POWER_THROTTLING_EXECUTION_SPEED |
                PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION,
            StateMask = 0,
        };

        if (!SetProcessInformation(
                GetCurrentProcess(),
                ProcessPowerThrottling,
                ref state,
                (uint)Marshal.SizeOf<PROCESS_POWER_THROTTLING_STATE>()))
        {
            throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
        }
    }
}

La llamada consiste en ejecutar una sola vez PowerQos.OptOutOfPowerThrottling(); antes de iniciar el bucle sensible al tiempo. El identificador de proceso necesita el permiso de acceso PROCESS_SET_INFORMATION, pero no hay problema si se usa el pseudo-identificador del propio proceso que devuelve GetCurrentProcess().

4.6. Ubicación de CPU, migración entre núcleos y calor

En la ubicación de CPU, suele funcionar mejor empezar por una indicación cercana a la soft affinity («preferentemente que se ejecute en este grupo de núcleos») en lugar de pasar directamente a fijar un núcleo específico (hard affinity / CPU pinning).

Orden recomendado para ajustar la ubicación de CPUDiagrama que empieza por medir, prueba SetThreadIdealProcessor o CPU Sets, comprueba si la mejora es suficiente para detenerse ahí o, en caso contrario, considerar SetThreadAffinityMask como último recurso, y a la vez recomienda comprobar la temperatura, el reloj y el funcionamiento prolongado.NoMedir primeroSetThreadIdealProcessor / CPU Sets¿Mejoró lo suficiente?Detenerse ahíConsiderar SetThreadAffinityMask como último recursoComprobar también temperatura / reloj / funcionamiento prolongado

Lista de verificación

  • Se mide primero y solo después se ajusta la ubicación de CPU
  • No se fija directamente un núcleo específico
  • Se prueba primero SetThreadIdealProcessor o CPU Sets
  • SetThreadAffinityMask se trata como último recurso
  • En funcionamiento prolongado se comprueban la temperatura, el reloj y el thermal throttling
  • Se comprueban el modo silencioso o de bajo ruido del portátil

En cuanto al orden, este flujo es el más seguro.

  1. Medir primero
  2. Si hace falta, ideal processor / CPU Sets
  3. Si aún así se necesita mejorar, fijar un núcleo específico

Fijar un núcleo específico parece eficaz, pero reduce el margen de maniobra del sistema operativo, así que usarlo a la ligera puede volver el sistema, al contrario, menos flexible.

4.7. Aislar controladores, DPC/ISR y otras perturbaciones

Cuando ocurre que «de vez en cuando solo el max se dispara» o «el promedio es bueno pero el p99.9 es malo», conviene sospechar también de perturbaciones ajenas al código de la aplicación.

Árbol de decisión para diagnosticar picos de retrasoDiagrama de decisión que, ante un pico de late, miss o max, pregunta primero si el propio tiempo de procesamiento también es largo, y en ese caso indica acortar el hot path, reducir asignaciones y eliminar E/S, y en caso contrario pregunta por picos de DPC/ISR, que llevan a revisar controladores de USB, Wi-Fi, Bluetooth, GPU, audio, almacenamiento y ACPI, y si no hay picos pregunta por page fault, GC o coste de primera ejecución, que lleva a reservar memoria por adelantado, calentar y reducir la carga del heap, y si tampoco es eso pregunta por el efecto de la batería, el ahorro de energía o el calor, que lleva a usar alimentación de CA, ajustar la energía, enfriar y hacer pruebas largas, y en última instancia recomienda profundizar con ETW, WPA o LatencyMon.NoNoNoNoAparece un pico de late / miss / max¿El propio tiempo de procesamiento también es largo?Acortar hot path / reducir asignaciones / eliminar E/S¿Hay picos de DPC / ISR?Revisar USB / Wi-Fi / Bluetooth / GPU / audio / almacenamiento / ACPI / actualización de controladores¿Hay page fault / GC / coste de primera ejecución?Reserva previa / calentamiento / reducir carga del heap¿Hay efecto de batería / ahorro de energía / calor?Alimentación de CA / configuración de energía / enfriamiento / pruebas largasProfundizar con ETW / WPA / LatencyMon

Lista de verificación

  • Se revisan los controladores de Wi-Fi, Bluetooth, USB, almacenamiento, GPU y audio
  • Se comparan los resultados deteniendo la sincronización en la nube, la indexación y las actualizaciones automáticas innecesarias
  • Se prueba también si se desestabiliza al minimizar o al apagar la pantalla
  • Se observa la tendencia de DPC/ISR con LatencyMon o ETW
  • Se distingue entre si «el propio procesamiento es pesado» o si «algo externo lo está deteniendo»

5. Medición y evaluación

5.1. Qué se debe registrar

Como mínimo, conviene registrar al menos lo siguiente.

  • Hora prevista del periodo
  • Hora real de inicio
  • Hora real de finalización
  • lateness (cuánto se retrasó el inicio real respecto al inicio previsto)
  • Tiempo de ejecución
  • Número de missed deadline
  • Número de missed deadline consecutivos
  • Profundidad de la cola (queue depth)
  • Número de drop
  • Uso de CPU
  • Desequilibrio entre núcleos
  • Picos de DPC/ISR
  • page fault
  • Variaciones de temperatura y reloj

Mirar solo el promedio dificulta captar lo esencial. Lo que causa problemas en producción son los picos de retraso grandes que aparecen de vez en cuando.

5.2. Cómo leer p99, p99.9 y max

Indicadores como p99 sirven para observar la cola de los valores más lentos. Con solo el promedio, los grandes retrasos ocasionales quedan ocultos.

Indicador Significado Imagen con 10 000 mediciones
Promedio Valor promediado del conjunto Los picos suelen quedar ocultos
p50 Valor central Cercano a la sensación habitual
p95 Límite donde empieza a verse el 5 % más lento Límite tras excluir los 500 casos más lentos
p99 Límite donde empieza a verse el 1 % más lento Límite tras excluir los 100 casos más lentos
p99.9 Límite donde empieza a verse el 0,1 % más lento Límite tras excluir los 10 casos más lentos
max Peor valor El caso más lento de todos

Supongamos, por ejemplo, que se obtiene la siguiente serie (es un ejemplo para explicar cómo leer los indicadores, no una medición real en un equipo concreto).

  • Promedio: 0,8 ms
  • p99: 1,2 ms
  • p99.9: 3,5 ms
  • max: 28 ms

Esto corresponde a un estado en el que normalmente es rápido, pero de vez en cuando presenta picos grandes. En un Windows convencional, el problema real suele aparecer justo en esa cola que va de p99 a max.

Cabe señalar que este artículo no presenta cifras del efecto de la configuración recomendada. La latencia y el jitter cambian fácilmente según la CPU, los controladores, el software residente, la configuración de energía y la forma de aplicar la carga, así que las cifras del entorno de otra persona no sirven tal cual como fundamento para el propio entorno. En su lugar, se deja aquí el procedimiento para elaborar una tabla con la misma forma en su propio entorno.

Procedimiento mínimo para obtener el p99 en su propio entorno

  1. En el hot path, solo se registra. Se toman el lateness y el tiempo de ejecución con Stopwatch.GetTimestamp() (o QueryPerformanceCounter en C++) y se escriben en un arreglo reservado de antemano. Aquí no se debe calcular el promedio ni ordenar nada
  2. La agregación se realiza después de detener la medición. Se ordena y se extrae el valor en la posición del percentil correspondiente
  3. Se repite el mismo procedimiento cambiando las condiciones: antes/después del calentamiento, CA/batería, interfaz en primer plano/minimizada, con o sin carga de otros procesos (5.4)
  4. Cada vez que se introduce un cambio, se vuelve a medir en las mismas condiciones y se compara
// C# / .NET 8. La agregación se realiza después de detener la medición
using System.Diagnostics;

// En el hot path solo se escribe en el arreglo (cero asignaciones)
long[] latenessTicks = new long[100_000];
int count = 0;

// Ejemplo: dentro del bucle periódico
// latenessTicks[count++] = Stopwatch.GetTimestamp() - scheduledTimestamp;

static double PercentileMs(long[] ticks, int count, double percentile)
{
    long[] sorted = ticks.AsSpan(0, count).ToArray();
    Array.Sort(sorted);

    int index = (int)Math.Ceiling(percentile / 100.0 * count) - 1;
    index = Math.Clamp(index, 0, count - 1);

    return sorted[index] * 1000.0 / Stopwatch.Frequency;
}

// Uso
// Console.WriteLine($"p50={PercentileMs(latenessTicks, count, 50):F3}ms");
// Console.WriteLine($"p99={PercentileMs(latenessTicks, count, 99):F3}ms");
// Console.WriteLine($"p99.9={PercentileMs(latenessTicks, count, 99.9):F3}ms");
// Console.WriteLine($"max={PercentileMs(latenessTicks, count, 100):F3}ms");

Stopwatch.Frequency es el número de cuentas por segundo, así que al dividir y multiplicar por 1000 se obtienen milisegundos. Si el número de muestras es escaso, el p99.9 pierde sentido. Si va a hablar de p99.9, reúna como mínimo 10 000 muestras (idealmente 100 000).

5.3. Con qué herramientas mirar

El conjunto de herramientas está, en general, bastante definido.

  • Medición dentro de la app Primero se toman por cuenta propia period / lateness / execution time / queue depth / drop
  • ETW / WPR / WPA Permite profundizar en CPU, context switch, DPC/ISR y page fault
  • LatencyMon Ayuda a detectar sospechosos de variación originada por controladores
  • Monitoreo de temperatura / reloj Permite ver el efecto del calor
Herramientas de medición y su aporte a la decisión de mejoraDiagrama en el que la medición dentro de la app aporta las distribuciones p50, p95, p99, p99.9 y max, además de miss, drop y profundidad de cola, mientras que ETW/WPR/WPA y el monitoreo de temperatura y reloj aportan al mismo origen de context switch, DPC/ISR y page fault, y todos esos resultados confluyen en decidir la prioridad de las mejoras.Medición dentro de la appp50 / p95 / p99 / p99.9 / maxmiss / drop / profundidad de colaETW / WPR / WPAcontext switch / DPC / ISR / page faultMonitoreo de temperatura / relojDecide la prioridad de las mejoras

Llegar hasta WPA cuesta un poco de trabajo, pero resulta bastante eficaz para distinguir si la causa es el DPC/ISR o si simplemente el propio procesamiento es pesado.

Cómo obtenerlas y el uso mínimo

Herramienta Cómo obtenerla Procedimiento mínimo
WPR / WPA (Windows Performance Toolkit) Se instala al elegir «Windows Performance Toolkit» durante la instalación del Windows ADK (Windows Assessment and Deployment Kit). La ubicación predeterminada es C:\Program Files (x86)\Windows Kits\10\Windows Performance Toolkit Abra un símbolo del sistema como administrador y (1) inicie el registro con wpr -start CPU, (2) ejecute el proceso problemático durante decenas de segundos, (3) guarde con wpr -stop trace.etl "investigación de retraso periódico". Después abra trace.etl con WPA. Los nombres de perfil disponibles se consultan con wpr -profiles
LatencyMon Se descarga del sitio de Resplendence Software. La Home Edition para uso personal es gratuita Inícielo, comience la medición y deje el proceso objetivo funcionando durante varios minutos. Se recopilan el retraso máximo del temporizador del kernel y, por controlador, el tiempo de ejecución de ISR/DPC y los hard pagefault, de modo que anote los controladores con un tiempo de ejecución destacado

En WPA, lo primero que se mira, dentro de los gráficos relacionados con la CPU, es el tiempo de ejecución de DPC/ISR y el context switch. Ahí aparece «qué controlador retenía la CPU durante el tramo en que el propio hilo no pudo ejecutarse», así que se contrasta con la hora de aparición de los retrasos registrados en 5.1.

Cabe señalar que este artículo no incluye capturas de pantalla. Como la apariencia cambia fácilmente según la versión, siga el procedimiento anterior y compruébelo en la pantalla real.

5.4. Buenas prácticas para las pruebas

Las pruebas no bastan con un entorno de banco tranquilo. Como mínimo, conviene observar por separado estas condiciones.

  • Justo después de iniciar, antes del calentamiento
  • Después del calentamiento
  • Funcionamiento continuo prolongado
  • Interfaz en primer plano
  • Interfaz minimizada o en un estado cercano a oculto
  • Alimentación de CA
  • Funcionamiento con batería
  • Estado con carga en la red o el disco

Evaluar solo con el entorno de banco facilita pasar por alto los problemas que aparecen en el uso real. Como el comportamiento de un Windows convencional tiende a verse arrastrado por «cómo se usa», es importante verificarlo en condiciones cercanas al uso real.

6. Criterios generales para decidir

La tabla rápida por rango de periodo se adelantó a 1.1 para que pueda volver a ella mientras lee. Aquí se complementan dos decisiones que esa tabla por sí sola no resuelve.

Cuándo decidir que «en un Windows convencional no es posible»

Cuando surge la necesidad de mantener menos de 1 ms durante mucho tiempo y con carga alta, en muchos casos es más rápido decidir sacar fuera solo la parte crítica en tiempo que seguir ajustando. Los candidatos para trasladarla son el firmware del propio dispositivo, un controlador dedicado, un FPGA o un RTOS. El criterio de decisión no debe ser subjetivo, sino basarse en las cifras del capítulo 5.

  • El hot path ya no se puede acortar más, y sin embargo el p99.9 y el max siguen superando el requisito
  • La causa son perturbaciones externas (DPC/ISR, controladores, otros procesos) que las medidas del lado de la aplicación no alcanzan a resolver (verificado con el aislamiento de 4.7)
  • El requisito ha pasado de «no romperse aunque falle alguna vez» a «no puede fallar ni una sola vez»

Cuando se quiere hacer convivir todo en un solo proceso

Si se cargan la GUI, el log, la comunicación y la BD en el mismo bucle del mismo proceso, las necesidades de las etapas posteriores rompen el plazo de las anteriores, porque la espera de flush de un archivo, la reconexión a la BD o el redibujado de la interfaz pueden alargarse, cada uno de ellos, decenas de milisegundos. También es una opción extender la separación entre fast path y slow path (4.2) hasta el nivel de proceso, en lugar de solo el de hilo. Al separar los procesos, aunque la etapa posterior se quede bloqueada, el periodo de la etapa anterior sigue funcionando.

7. Resumen

Hay dos premisas que conviene tener claras.

  • En un Windows convencional, el objetivo no es garantizar hard real-time, sino lograr, como soft real-time, una configuración que reduzca la latencia y el jitter y no se rompa aunque ocurra un incumplimiento de plazo
  • Lo más eficaz no es ajustar la prioridad, sino ordenar el hot path

En la implementación, lo que resulta eficaz es, más o menos, lo siguiente.

  • Separar el fast path y el slow path
  • Decidir de antemano la cola de longitud fija y la política para cuando se desborde
  • Medir con QPC y esperar con un event o un waitable timer (temporizador esperable)
  • Evitar asignaciones, E/S bloqueante y bloqueos pesados en el hot path

En el plano operativo, lo que resulta eficaz es, más o menos, lo siguiente.

  • Funcionar con alimentación de CA
  • Separar la configuración de energía de producción
  • Reducir la carga innecesaria en segundo plano
  • Evaluar con p99, p99.9, max y el número de miss

El soft real-time en un Windows convencional no se decide únicamente con la configuración de prioridad; si se afinan por separado el diseño, la implementación, la configuración de energía, la medición y la operación, se puede lograr un sistema bastante estable.

8. Referencias

Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.

Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.

Preguntas frecuentes

Preguntas habituales en las consultas sobre el tema del artículo.

¿Se puede hacer procesamiento en tiempo real en Windows?
No es posible garantizar hard real-time (cero incumplimientos de plazo), pero si se cuidan bien el diseño, la implementación, la medición y la operación, se puede llegar a un estado bastante práctico como soft real-time. El procesamiento periódico de pocos a varias decenas de milisegundos, el manejo de audio/video por búfer, la adquisición de sensores y los bucles de control son realistas incluso en un Windows 10/11 convencional. Por el contrario, si se necesita garantizar cero incumplimientos de plazo o una estabilidad prolongada por debajo de cientos de microsegundos, conviene estudiar trasladar esa parte a un RTOS, un controlador dedicado, un FPGA o el procesamiento en el propio dispositivo.
¿Por qué no se debe usar Sleep(1) en un bucle periódico?
Porque Sleep(1) no produce «un periodo de 1 ms», sino un comportamiento de «esperar al menos 1 ms y sumarle el tiempo de procesamiento», de modo que el exceso de espera se va acumulando tal cual. Es más estable hacer girar el bucle periódico con el plazo absoluto de next += period, dar preferencia a un evento de dispositivo o a un waitable timer de alta precisión para la espera, y limitar el ajuste final a un busy-spin (espera activa) muy corto. timeBeginPeriod debe usarse solo mientras haga falta, revirtiéndolo después.
¿Qué es lo más eficaz para reducir la latencia y el jitter?
Más que subir la prioridad, lo que más ayuda es hacer el hot path corto, de longitud fija y no bloqueante. Se separan el fast path (adquisición y control) y el slow path (guardado, comunicación, interfaz) y se conectan mediante una cola de longitud fija, evitando en el hot path la escritura de archivos, el envío por red, el registro de log pesado y la asignación de memoria en cada iteración. En el plano operativo ayudan la alimentación de CA, revisar el modo de energía, comprobar EcoQoS y ordenar la carga en segundo plano.
¿Con qué indicadores debo evaluar la estabilidad del procesamiento periódico?
No basta con el promedio: hay que mirar p99, p99.9, max y el número de missed deadline. Por ejemplo, un promedio de 0,8 ms con un max de 28 ms indica un estado que suele ser rápido pero que de vez en cuando presenta picos grandes, y en un Windows convencional el problema real suele aparecer justo en esa cola que va de p99 a max. Conviene registrar también los picos de DPC/ISR, los page fault, la profundidad de la cola y las variaciones de temperatura y reloj, y evaluar por separado condiciones como antes/después del calentamiento, funcionamiento prolongado, estado minimizado o alimentación por batería.

Perfil del autor

Página de presentación del autor del artículo.

Go Komura

Representante de KomuraSoft LLC

Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.

Volver al blog