Las aplicaciones en tiempo real requieren un rendimiento determinista, que excluye Java y otros lenguajes recolectados de basura. C y C ++ son los lenguajes elegidos para sistemas en tiempo real. Creo que cuando se refiere al comercio algorítmico, en realidad se refiere al comercio de alta frecuencia (HFT).
Los sistemas en tiempo real y de rendimiento combinan:
- Arquitectura básica sólida
- Consideración HW
- Perfiles y optimización basada en datos
Arquitectura básica
La arquitectura básica incluye la aplicación del conocimiento del rendimiento big-O con la comprensión de la escala de datos en diferentes puntos del programa. Cuantos núcleos ¿Cuál es la situación de coherencia de caché? ¿Está involucrado NUMA? ¿Qué tan importante es la afinidad del procesador?
- Cómo superar y comprender el algoritmo / código de otras personas
- ¿Crees que KNN tiene privilegios en comparación con otros algoritmos de aprendizaje automático porque aprende con el tiempo?
- En Python, dada la siguiente permutación de a, b, c, d, e, f, g, h, i, j, ¿cuál es la próxima permutación en el orden lexicográfico (diccionario)?
- ¿Cuál es el mejor curso para algoritmos y estructuras de datos?
- ¿Existe tal cosa como un 'Algoritmo Maestro'?
Muéstrame tus diagramas de flujo y oculta tus tablas, y seguiré desconcertado. Muéstrame tus tablas, y generalmente no necesitaré tus diagramas de flujo; Serán obvios. ~ Fred Brooks, El Mes del Hombre Mítico
Tiene que obtener sus estructuras de datos y sus interfaces correctas. Sin las estructuras de datos correctas, su programa está condenado al infierno del rendimiento. Debe tomar el tiempo y el cuidado adecuados con sus interfaces, de modo que pueda variar la implementación sin afectar a los clientes. “[Código] a toda prisa, arrepiéntete a gusto”.
Los algoritmos importan
Por ejemplo, durante la revisión del código de una interfaz de mensajería del cliente, encontramos que hubo dos bucles for-over involuntariamente sobre los mensajes, uno anidado dentro del otro. La revisión del código se activó porque en condiciones adversas el tiempo de procesamiento se deterioró de 1us a 10ms. Los desarrolladores insistieron en que, dado que el procesamiento de un solo mensaje tomó 1us, los 10ms tuvieron que ser un problema en otra parte. “¿Cuál es el mayor número de mensajes que ve en la cola de mensajes?” “100, lo limitamos a 100 mensajes”. Tiene un procesamiento O (N ^ 2), lo que significa que con 100 mensajes es 100 * 100 = 10,000X ralentiza ese solo mensaje. ¿Qué es 1us * 10,000? ”“ 10ms … ”Cambiar el algoritmo de manera que cada mensaje se considerara solo una vez mejoró el rendimiento del peor de los casos a significativamente menos de 1ms.
Los casos de “esquina” importan
En otro caso, usamos weak_ptr en los métodos de devolución de llamada para E / S asíncrona. Mientras los sistemas estuvieran en estado estable, podríamos salir con la pereza y usar shared_ptr en todas partes. Sin embargo, este uso de shared_ptr evitó el apagado limpio con E / S asíncrona. El uso de weak_ptr, que observa el shared_ptr y solo puede promocionarse a shared_ptr utilizable mientras exista el recurso referenciado, fue fundamental para limpiar el apagado.
Algunos miembros del equipo argumentaron que nunca cerramos; solo apagado. Sin embargo, al solucionar los problemas que impedían el apagado limpio, también abordamos los errores difíciles de reproducir que ocurrieron cuando se retiró una solicitud de E / S asíncrona.
Patrones de diseño comunes
Productor-Consumidor con una cola segura para subprocesos como notificación de conexión y condición variable. Esto libera al Productor para hacer más trabajo tan pronto como indique la variable de condición. El consumidor es eficiente porque bloquea la variable de condición proporcionada por el sistema operativo (y eficiente).
El patrón de mensajería entrega paquetes de datos a un destinatario con un nombre abstracto. La responsabilidad de traducir el nombre abstracto en un mecanismo de entrega concreto y un punto final se maneja dentro de la biblioteca de mensajes.
Los grupos de subprocesos crean previamente una serie de subprocesos y estos subprocesos se asignan según sea necesario para trabajar una tarea específica y luego se devuelven al grupo para su reutilización.
La E / S asincrónica es una técnica de codificación que complementa el Patrón de mensajería y los Grupos de subprocesos. El programa realiza una solicitud de E / S y proporciona una función de devolución de llamada invocada por el sistema operativo cuando se completa la E / S. Esto es eficiente porque las CPU son órdenes de magnitud más rápidas que las E / S. La biblioteca Asio de Boost es de gran ayuda aquí para E / S asíncronas portátiles.
Sondeo : sí, su profesor de sistemas operativos le dijo que el sondeo es ineficiente. Lo es, pero proporciona una respuesta de latencia más baja que interrumpe a los manejadores. Particularmente cuando tiene docenas de núcleos disponibles, asignar un núcleo a cada puerto de E / S de 40 Gb tiene mucho sentido.
shared_ptr : es difícil justificar no usar shared_ptr (o similar) a menos que y hasta que el perfil muestre una sobrecarga inaceptable. Una vez que hice ese perfil, una vez rechacé una oferta de trabajo atractiva porque el programador principal me dijo: “Cualquiera que no recuerde cuándo liberar sus recursos no debería estar programando para mí”. Sí, y dejaré de usar cinturones de seguridad cuando Conduzco porque si no puedo evitar un accidente, no debería operar un automóvil.
Event Drive Architecture, donde cada programa registra manejadores de mensajes que activan ciertas piezas de código a través de una variable de condición. Idealmente, los controladores de mensajes se registran al inicio y hay una sola llamada de bloqueo en la aplicación pendiente de mensajes. El bloqueo en un solo punto hace que sea mucho más fácil rastrear puntos muertos.
Perfilado
Una de las mayores verdades desconocidas de la programación moderna es que los humanos predicen espectacularmente mal dónde estará el cuello de botella en el rendimiento. Haríamos bien en cumplir la ley de carpintería: “Mide dos veces, corta una vez”.
Trabajando en mi tesis de maestría, me encontré con problemas de rendimiento. Sabía que mi código dependía en gran medida de la integración, por lo que pasé una semana optimizando ese código con la biblioteca Intel Performance Primitives, ajustes manuales, etc. Al final de la semana, aceleré la integración en un impresionante 60%. Desafortunadamente, cuando ejecuté la aplicación general con anticipación esperanzadora, me decepcionó lamentablemente. En cuanto a la creación de perfiles, descubrí que la mejora del 60% en el tiempo de integración se tradujo en una mejora del 3% en el rendimiento general de la aplicación. La elaboración de perfiles mostró que más del 80% del tiempo del programa se gastó en las instrucciones impares _I386EXCHANGEINTERLOCK. Bueno, compré la PC más rápida que pude para el proyecto, que era un Athlon de doble procesador (no de doble núcleo). Esta instrucción se usa para sincronizar cachés. Al aplicar un comando de una sola línea , SetProcessorAffinity, obtuve una mejora de velocidad de 5X. Lección aprendida.