- Em áreas como high-frequency trading (HFT), onde a latência é a própria vantagem competitiva, o autor organiza conhecimentos pouco disponíveis publicamente sobre otimização em C++ com foco em experimentos e implementação
- Os resultados são divididos em três partes: o Low-Latency Programming Repository, a otimização de uma estratégia de pair trading neutra ao mercado e uma biblioteca do padrão Disruptor em C++
- O benchmarking avalia velocidade, uso de cache e significância estatística em conjunto, e mostra que Cache Warming e Constexpr trazem grandes ganhos na redução de latência
- A estratégia de pair trading otimizada melhorou a velocidade de execução e a rentabilidade, e a implementação do Disruptor apresentou desempenho superior ao de abordagens tradicionais com filas
- Os próximos passos incluem expandir o repositório, testar em ambiente real de negociação e integrar o Disruptor com o algoritmo de trading para benchmark do sistema completo
Objetivo da otimização de baixa latência em HFT
- O objetivo é otimizar código sensível à latência para aumentar a velocidade de execução
- O foco está em estratégias de programação e estruturas de dados usadas em high-frequency trading
- No setor financeiro, especialmente em empresas buy-side que atuam em mercados públicos, muito desse conhecimento não é divulgado por questões de confidencialidade e vantagem competitiva
- Para reduzir essa lacuna, foi criado um Low-Latency Programming Repository personalizado com várias técnicas, validado por benchmarking estatístico
Três resultados
-
Low-Latency Programming Repository
- Não é apenas uma coletânea teórica, mas um guia prático com benchmarking estatístico
- Faz a curadoria de técnicas de programação, padrões de design e boas práticas para reduzir a latência em sistemas HFT
-
Otimização de uma estratégia de pair trading estatística neutra ao mercado
- Integra técnicas de redução de latência e otimizações em nível de CPU
- Mostra melhorias na velocidade de execução e na rentabilidade
-
Biblioteca do padrão Disruptor em C++
- Mostra ganho de desempenho em relação a abordagens tradicionais com filas
- Demonstra que essa estrutura de dados pode ser aplicada ao Order Management System (OMS) de sistemas HFT
Por que há pouca informação pública
- O conhecimento sobre otimização de sistemas HFT vem principalmente de profissionais do setor, mas detalhes de pesquisas recentes e de implementação raramente são divulgados por confidencialidade e vantagem competitiva
- Áreas como melhoria de latência, eficiência de código e otimização de cache têm material público especialmente limitado
- Existem estudos de HFT sob a perspectiva econômica e financeira, assim como modelos matemáticos de algorithmic trading, mas é raro que entrem nos detalhes técnicos de otimização de código ou redução de latência
- Mesmo havendo relativamente bastante literatura sobre C++, há poucas conexões diretas com o contexto de sistemas HFT de ultrabaixa latência
- Blogs e posts online frequentemente apresentam apenas dados superficiais de latência média, sem análise detalhada de acesso a cache ou atraso na execução de instruções
Avaliação e melhorias de desempenho
- As métricas de avaliação incluem velocidade, uso de cache e significância estatística
- Entre as técnicas do Low-Latency Programming Repository, Cache Warming e Constexpr mostraram os maiores ganhos na redução de latência
- A implementação do padrão Disruptor usa ring buffer, números de sequência e estratégias de espera especializadas para superar abordagens tradicionais com filas em latência e velocidade
- A estratégia de pair trading neutra ao mercado melhorou velocidade de execução e rentabilidade por meio de otimizações em nível de CPU e técnicas de redução de latência
Repositório público e trabalho futuro
- O repositório, a estratégia de trading e a biblioteca Disruptor estão em
https://github.com/0burak/imperial hft - O trabalho futuro inclui a expansão do repositório
- Ainda resta testar o algoritmo de trading otimizado em ambiente real de negociação
- Também inclui integrar o padrão Disruptor ao algoritmo de trading para realizar benchmarking no nível do sistema completo
1 comentários
Opiniões no Hacker News
Este artigo parece uma introdução bastante básica ao tema.
Pela minha experiência ensinando alunos de graduação, eles em geral já sabem esse tipo de coisa. Em aulas de arquitetura de computadores, aprendem os fundamentos de desempenho, como predição de desvios, coerência de cache e cache de instruções.
Achei surpreendente que ele não trate em nada de falso compartilhamento (false sharing), um fator clássico de queda de desempenho, e parece se concentrar principalmente na latência em single thread. Também me surpreendeu a ausência de dicas de otimização “gratuitas” como fat LTO, PGO,
[[likely]]e[[unlikely]].Problemas de desempenho mais profundos exigem entrar em APIs específicas de entrada/saída, primitivas de sincronização, comunicação entre processos e formas de uso de recursos internos obscuros do compilador.
O que mais falta a programadores de baixa latência, e o mais difícil de ensinar, é uma espécie de paranoia. É preciso ter medo e raiva reais de alocações desnecessárias, cópias e fatores que degradam o desempenho. É aquela sensação de rodar benchmarks compulsivamente no callgrind para encontrar, no meio de um hot loop, uma chamada que vai parar no alocador porque o cache de objetos falhou.
Para mim, ao criar um servidor de baixa latência, foi importante o momento em que percebi que, em vez de montar operações de E/S vetorizadas, copiar pequenos objetos para um buffer contíguo e fazer um único
writeera mais rápido no geral. Não existe cópia grátis, e fat pointer não é exceção.O melhor material disponível hoje se resume a algumas palestras de conferências de C++, e elas deixam bastante a desejar.
Deixando de lado a tentação de se exibir, este documento é uma contribuição excelente para a área e talvez seja a primeira referência de autoridade. Dizer vagamente que dá para juntar informações parecidas em outras aulas não é uma contribuição e não ajuda ninguém.
Primeiro, para obter velocidade bruta, eu usaria um FPGA no front-end para dividir a carga em fluxos de dados simples por ativo. Mas evitaria a tentação de executar a lógica ali de fato, porque o atrito de iteração de desenvolvimento, equipe, cadeia de suprimentos etc. é grande demais. A entrada seria algo como um fluxo FIX, e a saída seria dividida, por um barramento de baixa latência, em fluxos de eventos binários por ativo, entrando em segmentos por ativo de um cluster escalável composto por MCUs de baixo custo.
Em segundo lugar, na plataforma de execução baseada em MCU por ativo, eu removeria as suposições de um sistema operacional de propósito geral, permitindo uma transição mais rápida com código de baixo nível que as pessoas conseguem escrever sobre hardware realmente disponível. Terceiro, lucro? Nessa arquitetura, um supervisor baseado em sistema operacional de propósito geral teria de monitorar o estado global e, quando necessário, reprogramar elementos individuais para interromper ou alterar a estratégia.
A questão é quão baixa é a latência real. A partir de certo ponto, acho que pode valer mais a pena pagar o custo de colocar o hardware mais perto do núcleo do que continuar investindo em engenharia. Isso dependeria muito das regras, do data center e da infraestrutura de links oferecidos pela bolsa ou pool em questão.
Muitas operações lucrativas provavelmente não revelam a quais pools se conectam e talvez façam de front-running um negócio, ignorando regulações ou termos de uso. Nesses casos, a latência geográfica relativa de rede entre dois pontos de execução é mais poderosa do que a latência absoluta até um único ponto.
De fato, o senso comum que o pessoal de compiladores costuma mencionar é que, mesmo sem PGO, na maioria dos casos essas dicas são contraproducentes. Compiladores modernos confiam mais em seus próprios passes de análise do que nessas dicas e normalmente as ignoram.
A propósito, os únicos lugares em que vi essas dicas em código real foram aqueles em que o compilador poderia inseri-las facilmente. Por exemplo, uma verificação de nulo depois de uma chamada a
malloc.A parte que eu quero destacar é esta:
“A saída deste teste é a estatística do teste (t-statistic) e o p-value associado. A t-statistic, também chamada de pontuação, é o resultado do teste de raiz unitária sobre os resíduos. Uma t-statistic mais negativa sugere maior probabilidade de que os resíduos sejam estacionários. O p-value fornece uma medida da probabilidade de que a hipótese nula do teste, isto é, a hipótese de ausência de cointegração, seja verdadeira. O resultado do teste produziu um p-value de aproximadamente 0,0149 e uma t-statistic de -3,7684.”
Essa parte parece ter sido escrita por um LLM.
O exemplo também é realmente estranho. Ele observa a correlação dos preços de fechamento uma vez por dia durante 5 anos e depois escreve código para calcular o spread com latência de 65 microssegundos. Isso não faz sentido como algo que você faria de verdade. Você também não calcularia estatísticas de spread no loop interno, e 65 microssegundos é lento demais para um loop interno.
A ideia pode ser praticar técnicas de otimização, mas, como alvo de otimização, é bem pouco representativo.
Implementei em C++ uma bolsa de valores usando o padrão LMAX Disruptor
https://github.com/sneilan/stock-exchange
Também deixei uma implementação básica do LMAX Disruptor em alguns arquivos C++
https://github.com/sneilan/lmax-disruptor-tutorial
Só que estou pensando em refazer isso em Rust. Cheguei ao ponto de implementar um protocolo WebSocket próprio, sistema de autenticação, SSL etc., mas percebi que gerenciamento de memória e dependências são muito mais fáceis em Rust. Especialmente em um projeto de software de uma pessoa só
O acesso à memória pode ser reordenado tanto pelo compilador quanto pela CPU, então é preciso usar
std::atomicnas posições do produtor e do consumidor para obter as barreiras descritas no artigo original do LMAX DisruptorNo método
get, a posição do consumidor é incrementada e, portanto, o slot é liberado para o produtor, antes de retornar um ponteiro para o elemento interno da fila. Assim, ele pode ser sobrescrito enquanto o usuário está acessandoAlém disso, é provável que as posições do produtor e do consumidor fiquem na mesma linha de cache, causando 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;dá para fazer assim
uint64_t mask = slot_count - 1; // todos 1 em binárioitem = &slots[ pos & mask ];pos ++;Ou seja, dá para reduzir um pouco o cálculo trocando divisão/módulo por AND bit a bit. Mas o tamanho do ring buffer precisa ser uma potência de 2
Indo além, dá para usar um número de sequência com intervalo completo, como
uint64_t. O wraparound é tratado automaticamente. Subtrair dois números de sequência também funciona sem problemas levando o wraparound em conta. Também desaparece aquele problema idiota de ter que deixar um slot vazio para distinguir se o buffer está cheio ou vazioClaro, é preciso tomar cuidado para que a janela de números de sequência “vivos” nunca ultrapasse o tamanho da janela do ring buffer
Para gerenciamento de memória, vale considerar trocar para
std::shared_ptr. Isso elimina completamente essa preocupação sem reduzir a velocidadePara sockets, existem bibliotecas livres e open source que têm desempenho melhor e reduzem os casos excepcionais chatos em comparação com código escrito do zero. Por exemplo, iterar com
FD_ISSETé mais lento do queepolloukqueueO gerenciamento de dependências em C++ é definitivamente mais bruto do que em outras linguagens. Encontrar dependências também pode ser mais difícil do que gerenciá-las. Código de biblioteca útil está espalhado por aí, e parte dele fica escondida em cantos esquecidos da internet. Encontrar isso já é uma habilidade por si só, e quando você é bom nisso, a recompensa é grande
No sistema que você está considerando, acho que vai ser difícil superar um ring buffer SPSC e, se necessário, também dá para implementar work stealing com locks à moda antiga
https://martinfowler.com/articles/lmax.html
Isso me lembrou https://github.com/CppCon/CppCon2017/blob/master/Presentatio...
O slide em que um servidor falso reproduz dados de ordens, um segundo servidor calcula o tempo de execução e um servidor em teste, junto com um switch de hardware, mede o tempo dos pacotes é deliciosamente hardcore
Não tenho vontade de trabalhar no setor financeiro, mas deve ser divertido lidar com sistemas críticos de desempenho em um nível em que é economicamente viável comprar hardware em racks só para benchmarking
Criei uma biblioteca de logging em C++ que tem muitos pontos em comum com o LMAX Disruptor e que parece ser usada em certa medida pela comunidade de HFT
O objetivo original era permitir logs extremamente detalhados em produção para depuração post-mortem, sem degradação de desempenho. Eu tinha colegas que relutavam em colocar nos logs informações importantes para solucionar problemas por medo de impactar a performance, mas esta biblioteca encerrou essa discussão
[1] https://github.com/mattiasflodin/reckless
Outra vantagem do despacho em tempo de compilação é que, quando o compilador consegue determinar estaticamente qual função será chamada, ele pode inlinear o código da função chamada diretamente no ponto de chamada
Isso elimina todo o overhead da chamada de função e também pode permitir otimizações adicionais, como eliminação de código morto e propagação de constantes
Um bom JIT oferece suporte a inline polimórfico. Minha experiência com C++ já é um pouco antiga, mas a solução para esse problema era PGO. Só que não é algo amplamente usado. Em vez disso, em código sensível a desempenho, a tendência é evitar o despacho dinâmico em si
A lição mais geral é: em qualquer linguagem, nos trechos quentes do código, evite ramificações dinâmicas desnecessárias, a menos que você tenha forte convicção de que o compilador ou o JIT consegue atravessá-las
https://youtu.be/i5MAXAxp_Tw
Há algum bom motivo para a existência de negociação de alta frequência? As pessoas criticam bastante o Bitcoin por desperdiçar energia, mas isso também parece claramente uma perda líquida para a sociedade e, curiosamente, parece passar batido
É difícil dizer que esse setor seja extremamente pró-social, mas é verdade que estreitar os spreads reduz o dinheiro que vai para intermediários
HFT é uma área bastante concentrada, mas seu tamanho em si é relativamente pequeno. Em termos de desperdício de energia, é várias ordens de grandeza menor que o Bitcoin
O único efeito positivo do HFT é a liquidez e spreads mais estreitos, mas isso também depende de como as pessoas definem HFT. Por exemplo, a Robinhood e as negociações gratuitas provavelmente não existiriam sem isso
Eles estão tomando uma fatia que antes ia para corretoras e bancos. HFT não é um negócio de passar a perna no “investidor pessoa física”
Do meu ponto de vista, o impacto negativo sobre a sociedade é quase inexistente, ou inexistente. Para quem investe no mercado de ações no longo prazo, há pouquíssimos motivos para se preocupar com HFT
De todo modo, não há nenhum evento natural que exija negociação de alta frequência. É raro que o valor fundamental mude muito rapidamente e, mesmo quando muda, está mais para uma transição definida do que para volatilidade
HFT torna os mercados financeiros um pouquinho mais precisos ao resolver inconsistências, por exemplo quando três pares de moedas ficam desalinhados entre si, ou corrigindo uma precificação “óbvia” errada
Quando você tenta negociar algo, há alguém do outro lado. Em geral, é bem provável que você acabe negociando com um participante de HFT no preço que deseja. Se você recebe um preço melhor, esse é dinheiro que fica com você
Também é difícil concordar com a ideia de que isso “passa batido”. HFT também é criticado com bastante frequência por aqui
Se você é um desenvolvedor profissional, vale a pena ver tudo
https://github.com/CppCon/CppCon2017/tree/master/Presentatio...
E também o diretório acima dele
Tenho uma dúvida. Por que, nessa área, usariam ou teriam usado C++ para a lógica, em vez de C? Quais vantagens o C++ tem sobre C nesse domínio? Sou fluente em C/assembly, mas não conheço nada das práticas de HFT, então uma explicação simples seria bem-vinda
Claro, dá para discutir se essa expressividade vale a enorme complexidade da linguagem, mas, na prática, as pessoas têm escolhido C++ empiricamente
A estrutura e o tom deste texto deixam um forte cheiro de LLM