- En áreas donde la latencia es una ventaja competitiva, como el trading de alta frecuencia (HFT), se recopila conocimiento de optimización en C++ que escasea públicamente, con foco en experimentos e implementación
- Los resultados se dividen en tres partes: Low-Latency Programming Repository, optimización de una estrategia de pair trading de mercado neutral y una biblioteca del patrón Disruptor en C++
- El benchmarking considera en conjunto velocidad, uso de caché y significancia estadística; Cache Warming y Constexpr muestran grandes beneficios en la reducción de latencia
- La estrategia optimizada de pair trading mejoró en velocidad de ejecución y rentabilidad, y la implementación de Disruptor muestra mejor rendimiento que los enfoques tradicionales basados en colas
- Las tareas futuras incluyen ampliar el repositorio, probar en entornos reales de trading e integrar Disruptor con algoritmos de trading para luego hacer benchmarking del sistema completo
Objetivo de la optimización de baja latencia para HFT
- El objetivo es aumentar la velocidad de ejecución optimizando código sensible a la latencia
- El foco está puesto en estrategias de programación y estructuras de datos usadas en trading de alta frecuencia
- En la industria financiera, especialmente las empresas buy-side que operan en mercados públicos, no se divulga mucho conocimiento relacionado debido a la confidencialidad y a la ventaja competitiva
- Para reducir esa brecha, se crea un Low-Latency Programming Repository personalizado con diversas técnicas y se valida mediante benchmarking estadístico
Tres resultados
-
Low-Latency Programming Repository
- No se limita a una recopilación teórica, sino que funciona como una guía práctica con benchmarking estadístico
- Curaduría de técnicas de programación, patrones de diseño y buenas prácticas para reducir la latencia en sistemas HFT
-
Optimización de una estrategia de pair trading de arbitraje estadístico de mercado neutral
- Integra técnicas de reducción de latencia y optimizaciones a nivel de CPU
- Muestra mejoras en velocidad de ejecución y rentabilidad
-
Biblioteca del patrón Disruptor en C++
- Muestra una mejora de rendimiento frente a los enfoques tradicionales basados en colas
- Demuestra que este tipo de estructura de datos puede aplicarse al Order Management System (OMS) de sistemas HFT
Por qué falta conocimiento público
- El conocimiento sobre optimización de sistemas HFT proviene principalmente de profesionales de la industria, pero la confidencialidad y la ventaja competitiva dificultan la publicación de investigaciones recientes y detalles de implementación
- Áreas como mejoras de latencia, eficiencia del código y optimización de caché tienen material público especialmente limitado
- Existen investigaciones sobre HFT desde una perspectiva económica y financiera, así como estudios de modelos matemáticos para trading algorítmico, pero rara vez cubren técnicas detalladas de optimización de código o reducción de latencia
- Aunque la literatura sobre C++ es relativamente abundante, son limitados los casos que se conectan directamente con el contexto de sistemas HFT de latencia ultrabaja
- Los blogs y publicaciones en línea suelen ofrecer datos superficiales de latencia promedio, pero carecen de análisis detallados sobre el comportamiento del acceso a caché o la latencia de ejecución de instrucciones
Evaluación y mejoras de rendimiento
- Las métricas de evaluación incluyen velocidad, uso de caché y significancia estadística, entre otras
- Entre las técnicas del Low-Latency Programming Repository, Cache Warming y Constexpr muestran los mayores beneficios en reducción de latencia
- La implementación del patrón Disruptor usa un búfer en anillo, números de secuencia y estrategias especiales de espera para ofrecer mejor rendimiento en latencia y velocidad que los enfoques tradicionales basados en colas
- La estrategia de pair trading de mercado neutral mejora su velocidad de ejecución y rentabilidad mediante optimizaciones a nivel de CPU y técnicas de reducción de latencia
Repositorio público y trabajo futuro
- El repositorio, la estrategia de trading y la biblioteca Disruptor están en
https://github.com/0burak/imperial hft - El trabajo futuro incluye ampliar el repositorio
- Queda pendiente probar los algoritmos de trading optimizados en entornos reales de trading
- También se contempla integrar el patrón Disruptor con algoritmos de trading y realizar benchmarking a nivel del sistema completo
1 comentarios
Opiniones de Hacker News
Este artículo parece una introducción bastante básica al tema.
Por mi experiencia enseñando a estudiantes de licenciatura, ellos también suelen saber ya estas cosas. En clases de arquitectura de computadoras aprenden los fundamentos del rendimiento, como la predicción de ramas, la coherencia de caché y la caché de instrucciones.
Me sorprendió que no trate en absoluto un factor clásico de degradación del rendimiento como la compartición falsa (false sharing), y parece centrarse sobre todo en la latencia de un solo hilo. También me sorprendió que falten pistas de optimización “gratis” como fat LTO, PGO,
[[likely]]y[[unlikely]].Para problemas de rendimiento más profundos hay que meterse hasta en APIs específicas de entrada/salida, primitivas de sincronización, comunicación entre procesos y formas de usar intrínsecos oscuros del compilador.
Lo que más le falta a un programador de baja latencia, y lo más difícil de enseñar, es una especie de paranoia. Se necesita miedo y enojo reales ante asignaciones, copias y factores de degradación del rendimiento innecesarios. Es esa sensación de correr benchmarks obsesivamente con callgrind para encontrar, en medio de un hot loop, una llamada al asignador provocada por un fallo en la caché de objetos.
En lo personal, al construir un servidor de baja latencia, fue importante el momento en que me di cuenta de que, en vez de armar operaciones de entrada/salida vectorizada, resultaba más rápido en conjunto copiar objetos pequeños a un búfer contiguo y hacer un único
write. No hay copias gratis, y los fat pointers no son la excepción.El mejor material que se puede conseguir hoy son apenas algunas charlas de conferencias de C++, y dejan bastante que desear.
Dejando de lado la tentación de presumir, este documento es una contribución excelente al área y quizá sea la primera referencia autorizada. Decir vagamente que se puede armar información parecida a partir de otras conferencias no es una contribución y no ayuda a nadie.
Primero, por velocidad bruta, usaría un FPGA en el front-end para dividir la carga en flujos de datos simples por activo. Pero evitaría la tentación de ejecutar ahí mismo, porque la fricción de iteración, personal y cadena de suministro es demasiado alta. La entrada sería algo como un flujo FIX, y la salida se dividiría, a lo largo de un bus de baja latencia, en flujos de eventos binarios por activo que entrarían en segmentos por activo de un clúster escalable compuesto por MCU de bajo costo.
Segundo, en la plataforma de ejecución basada en MCU por activo eliminaría las suposiciones de un sistema operativo de propósito general, para permitir transiciones más rápidas con código de bajo nivel que la gente pueda escribir sobre hardware realmente disponible. Tercero, ¿ganancias? En una estructura así, un supervisor basado en un sistema operativo de propósito general tendría que monitorear el estado global y, cuando fuera necesario, reprogramar elementos individuales para detener o cambiar la estrategia.
La cuestión es qué tan baja es la latencia real. A partir de cierto punto, me parece que quizá conviene más pagar el costo de poner el hardware más cerca del core que seguir haciendo ingeniería. Eso dependerá mucho de las reglas que ofrezca la bolsa o el pool correspondiente, del centro de datos y de la infraestructura de enlaces.
Muchas operaciones rentables probablemente no revelen a qué pool se conectan, y puede que conviertan el front-running en un negocio ignorando regulaciones o términos de servicio. En esos casos, la latencia geográfica relativa de red entre dos puntos de ejecución es más poderosa que la latencia absoluta hasta un punto.
De hecho, la sabiduría común que suele mencionar la gente de compiladores es que, incluso sin PGO, en la mayoría de los casos estas pistas son contraproducentes. Los compiladores modernos confían más en sus propios pases de análisis que en estas pistas y normalmente las ignoran.
Como referencia, en código real solo he visto estas pistas en lugares donde el compilador podría ponerlas fácilmente, por ejemplo en una comprobación de nulo después de una llamada a
malloc.La parte que quiero destacar es esta:
“La salida de esta prueba es el estadístico de prueba (t-statistic) y el p-value asociado. El t-statistic, también llamado puntuación, es el resultado de una prueba de raíz unitaria sobre los residuos. Un t-statistic más negativo sugiere que es más probable que los residuos sean estacionarios. El p-value proporciona una medida de la probabilidad de que la hipótesis nula de la prueba, es decir, la hipótesis de que no hay cointegración, sea verdadera. Los resultados de la prueba arrojaron un p-value de aproximadamente 0.0149 y un t-statistic de -3.7684.”
Esta parte parece escrita con un LLM.
El ejemplo también es realmente raro. Mira la correlación de precios de cierre una vez al día durante 5 años y luego escribe código para calcular el spread con una latencia de 65 microsegundos. No tiene sentido como algo que se haría en la práctica. Tampoco se calcularían estadísticas del spread dentro del bucle interno, y 65 microsegundos es demasiado lento para un bucle interno.
La idea puede ser practicar técnicas de optimización, pero como objetivo de optimización es bastante poco representativo.
Implementé en C++ una bolsa de valores que usa el patrón LMAX Disruptor
https://github.com/sneilan/stock-exchange
También dejé hecha una implementación básica de LMAX Disruptor en unos cuantos archivos C++
https://github.com/sneilan/lmax-disruptor-tutorial
Sin embargo, estoy viendo cómo rehacerlo en Rust. Llegué hasta el punto de implementar un protocolo propio de WebSocket, un sistema de autenticación, SSL, etc., pero me di cuenta de que la gestión de memoria y las dependencias son mucho más fáciles en Rust. Sobre todo si es un proyecto de software de una sola persona
Como los accesos a memoria pueden reordenarse tanto del lado del compilador como del CPU, para obtener las barreras descritas en el artículo original de LMAX Disruptor hay que usar
std::atomicen las posiciones del productor y del consumidorEn el método
get, primero se incrementa la posición del consumidor, es decir, se libera el slot para el productor, y después se devuelve un puntero al elemento interno de la cola. Por eso podría sobrescribirse mientras el usuario lo está accediendoAdemás, es muy probable que las posiciones del productor y del consumidor queden en la misma línea de caché, lo que provoca false sharing
T *item = &this->shared_mem_region->entities[this->shared_mem_region->consumer_position];this->shared_mem_region->consumer_position++;this->shared_mem_region->consumer_position %= this->slots;se puede hacer esto
uint64_t mask = slot_count - 1; // todos 1 en binarioitem = &slots[ pos & mask ];pos ++;Es decir, se puede reducir un poco el cálculo reemplazando la división/módulo por un AND bit a bit. Eso sí, el tamaño del ring buffer debe ser una potencia de 2
Más aún, se puede usar un número de secuencia de rango completo, como
uint64_t. El wrap-around se maneja automáticamente. Incluso al restar dos números de secuencia, funciona sin problemas teniendo en cuenta el wrap-around. También desaparece el problema tonto de tener que dejar un slot vacío para distinguir si el buffer está lleno o vacíoClaro que hay que tener cuidado de que la ventana de números de secuencia “vivos” nunca supere el tamaño de la ventana del ring buffer
Para la gestión de memoria, valdría la pena considerar cambiar a
std::shared_ptr. Elimina por completo esa preocupación sin volverlo más lentoPara sockets, hay bibliotecas libres y open source que rinden mejor que el código escrito a mano y reducen los casos especiales molestos. Por ejemplo, recorrer
FD_ISSETes más lento queepollokqueueLa gestión de dependencias en C++ definitivamente es más áspera que en otros lenguajes. Incluso encontrarlas es más difícil que gestionarlas. Hay código de bibliotecas útil repartido por todas partes, y parte está escondido en rincones olvidados de internet. Encontrarlo ya es una habilidad en sí misma, y si se hace bien puede tener una gran recompensa
En el sistema que estás pensando, creo que será difícil superar un ring buffer SPSC, y si hace falta también se podría implementar work stealing con locks a la vieja usanza
https://martinfowler.com/articles/lmax.html
Me hizo pensar en https://github.com/CppCon/CppCon2017/blob/master/Presentatio...
La diapositiva donde un servidor falso reproduce datos de órdenes, un segundo servidor calcula el tiempo de ejecución, y un servidor bajo prueba junto con un switch de hardware miden el tiempo de los paquetes es deliciosamente hardcore
No tengo ganas de trabajar en finanzas, pero suena divertido lidiar con sistemas críticos de rendimiento donde sea económicamente viable comprar hardware por racks solo para benchmarking
Creé una biblioteca de logging en C++ que tiene muchas similitudes con LMAX Disruptor, y parece que también se usa en cierta medida en la comunidad de HFT
El objetivo original era permitir logs muy detallados en producción para depuración post mortem sin degradar el rendimiento. Algunos colegas se negaban a poner en los logs información importante para resolver problemas por miedo a afectar el rendimiento, pero con esta biblioteca se terminó esa discusión
[1] https://github.com/mattiasflodin/reckless
Otra ventaja del dispatch en tiempo de compilación es que, cuando el compilador puede determinar estáticamente qué función se llama, puede inlinear el código de la función llamada directamente en el punto de llamada.
Eso elimina todo el overhead de la llamada a función y también puede habilitar optimizaciones adicionales como eliminación de código muerto y propagación de constantes.
Un buen JIT soporta inline polimórfico. Mi experiencia con C++ ya es algo vieja, pero la solución a este problema era PGO. Aunque no se usa ampliamente. En cambio, en código sensible al rendimiento se tiende a evitar directamente el dispatch dinámico.
La lección más general es que, en cualquier lenguaje, en las zonas calientes del código conviene evitar ramas dinámicas innecesarias, salvo que tengas una fuerte certeza de que el compilador o el JIT pueden atravesarlas.
https://youtu.be/i5MAXAxp_Tw
¿Hay alguna buena razón para que exista el trading de alta frecuencia? La gente suele criticar a Bitcoin por desperdiciar energía, pero esto también parece claramente una pérdida neta para la sociedad, y sin embargo da la impresión de que se deja pasar sin más.
Es difícil decir que esta industria sea enormemente prosocial, pero sí es cierto que estrechar los spreads reduce el dinero que va a los intermediarios.
Aunque HFT es un ámbito bastante concentrado, su escala en sí es más bien pequeña. En términos de desperdicio de energía, es varios órdenes de magnitud menor que Bitcoin.
El único efecto positivo de HFT es la liquidez y spreads más estrechos, aunque también depende de cómo la gente defina HFT. Por ejemplo, Robinhood y las operaciones gratuitas probablemente no existirían sin esto.
Están capturando una parte que antes iba a brokers y bancos. HFT no es un negocio de desplumar al “inversionista minorista”.
Desde mi punto de vista, tiene poco o ningún impacto negativo sobre la sociedad. Si inviertes en el mercado accionario a largo plazo, casi no hay motivo para preocuparte por HFT.
En cualquier caso, no hay eventos naturales que requieran trading de alta frecuencia. Es raro que el valor fundamental cambie muy rápido y, aun cuando cambia, suele parecerse más a una transición definitiva que a volatilidad.
HFT resuelve inconsistencias —por ejemplo, situaciones en las que tres pares de divisas no cuadran entre sí— o precios “obviamente” incorrectos, haciendo que los mercados financieros sean apenas un poco más precisos.
Cuando intentas operar algo, hay alguien del otro lado. Normalmente es bastante probable que termines operando con un participante de HFT al precio que querías. Si recibes un mejor precio, ese dinero es dinero que conservas.
Tampoco estoy de acuerdo con eso de que “se deja pasar”. HFT también se critica bastante seguido por aquí.
Si eres desarrollador profesional, vale la pena verlo completo:
https://github.com/CppCon/CppCon2017/tree/master/Presentatio...
Y también el directorio superior.
Tengo una duda. En este campo, ¿por qué se usa, o se ha usado, C++ para la lógica en lugar de C? ¿Qué ventajas tiene C++ sobre C en esta área? Soy hábil con C/ensamblador, pero no sé nada de las prácticas de HFT, así que agradecería una explicación sencilla.
Por supuesto, se puede discutir si esa expresividad vale la pena frente a la enorme complejidad del lenguaje, pero en la práctica la gente ha elegido C++ empíricamente.
La estructura y el tono de este artículo tienen un fuerte olor a LLM