Unix / Linux de baixa latência

11

A maioria dos trabalhos de programação de baixa latência / alta frequência (com base nas especificações do trabalho) parece estar implementada em plataformas unix. Em muitas das especificações, eles solicitam pessoas com experiência no tipo "linux de baixa latência".

Supondo que isso não signifique um sistema operacional Linux em tempo real, as pessoas poderiam me ajudar com o que isso poderia estar se referindo? Eu sei que você pode definir a afinidade da CPU como threads, mas suponho que eles estejam pedindo muito mais do que isso.

Ajuste do kernel? (embora eu ouvi fabricantes como o solarflare produzirem placas de rede de desvio de kernel)?

E o DMA ou possivelmente a memória compartilhada entre processos? Se as pessoas pudessem me dar breves idéias, posso fazer a pesquisa no google.

(Essa pergunta provavelmente exigirá alguém familiarizado com negociação de alta frequência)

user997112
fonte
2
O ajuste do kernel é o caminho a seguir para tornar um sistema operacional não em tempo real o mais em tempo real possível. A fixação de threads também é obrigatória. Você pode ler mais sobre isso neste artigo: coralblocks.com/index.php/2014/04/...
rdalmeida
Também relacionado: stackoverflow.com/q/15702601/632951
Pacerier 13/16

Respostas:

26

Fiz uma boa quantidade de trabalho apoiando grupos HFT nas configurações do IB e do Hedge Fund. Vou responder da exibição sysadmin, mas parte disso também é aplicável à programação nesses ambientes.

Há algumas coisas que um empregador geralmente procura quando se refere ao suporte "Baixa latência". Algumas delas são perguntas de "velocidade bruta" (você sabe que tipo de cartão de 10g comprar e em que slot o colocar?), Mas mais deles são sobre as maneiras pelas quais um ambiente de negociação de alta frequência difere do tradicional Ambiente Unix. Alguns exemplos:

  • O Unix é tradicionalmente ajustado para oferecer suporte à execução de um grande número de processos sem precisar de recursos, mas em um ambiente HFT, é provável que você queira executar um aplicativo com um mínimo absoluto de sobrecarga para alternância de contexto e assim por diante. Como um pequeno exemplo clássico, ativar o hyperthreading em uma CPU Intel permite que mais processos sejam executados ao mesmo tempo - mas tem um impacto significativo no desempenho na velocidade em que cada processo individual é executado. Como programador, você também terá que olhar para o custo de abstrações como threading e RPC e descobrir onde uma solução mais monolítica - embora menos limpa - evite sobrecarga.

  • O TCP / IP geralmente é ajustado para evitar quedas de conexão e fazer uso eficiente da largura de banda disponível. Se seu objetivo é obter a menor latência possível de um link muito rápido - em vez de obter a maior largura de banda possível de um link mais restrito -, convém ajustar o ajuste da pilha de rede. Do lado da programação, você também vai querer examinar as opções de soquete disponíveis e descobrir quais padrões têm mais ajustes para largura de banda e confiabilidade do que para reduzir a latência.

  • Assim como na rede, no armazenamento - você vai querer saber como diferenciar um problema de desempenho de armazenamento de um problema de aplicativo e aprender quais padrões de uso de E / S têm menor probabilidade de interferir no desempenho do seu programa (como um Por exemplo, saiba onde a complexidade do uso de E / S assíncrona pode render para você e quais são as desvantagens).

  • Finalmente, e mais dolorosamente: nós, administradores do Unix, queremos o máximo possível de informações sobre o estado dos ambientes que monitoramos, por isso gostamos de executar ferramentas como agentes SNMP, ferramentas de monitoramento ativas como Nagios e ferramentas de coleta de dados como sar (1). Em um ambiente em que as alternâncias de contexto precisam ser absolutamente minimizadas e o uso de E / S de disco e rede seja rigidamente controlado, porém, precisamos encontrar a troca certa entre as despesas de monitoramento e o desempenho bare-metal das caixas monitoradas. Da mesma forma, que técnicas você usa para facilitar a codificação, mas estão custando desempenho?

Finalmente, há outras coisas que vêm com o tempo; truques e detalhes que você aprende com a experiência. Mas eles são mais especializados (quando eu uso o epoll? Por que dois modelos de servidor HP com controladores PCIe teoricamente idênticos têm um desempenho tão diferente?), Mais ligados ao que a sua loja específica está usando e com maior probabilidade de mudar de um ano para outro .

jimwise
fonte
1
Obrigado, embora eu estivesse interessado em uma resposta de programação, isso foi muito útil e informativo.
precisa saber é o seguinte
5
@ user997112 Esta é uma resposta de programação. Se não parece, continue lendo até que pareça :)
Tim Post
15

Além da excelente resposta de ajuste de hardware / configuração do @jimwise, "linux de baixa latência" está implicando:

  • C ++ por razões de determinismo (sem atraso surpresa enquanto o GC entra em ação), acesso a instalações de baixo nível (E / S, sinais), potência do idioma (uso total de TMP e STL, tipo de segurança).
  • prefira velocidade sobre memória:> 512 Gb de RAM é comum; os bancos de dados são produtos NoSQL in-memory, com cache antecipado ou exóticos.
  • escolha do algoritmo: tão rápido quanto possível versus sã / compreensível / extensível, por exemplo, livre de bloqueios, várias matrizes de bits em vez de propriedades de matriz de objetos com propriedades booleanas.
  • uso completo de recursos do SO, como Memória Compartilhada entre processos em diferentes núcleos.
  • seguro. O software HFT geralmente é colocado em uma bolsa de valores, portanto as possibilidades de malware são inaceitáveis.

Muitas dessas técnicas se sobrepõem ao desenvolvimento de jogos, que é uma das razões pelas quais a indústria de software financeiro absorve todos os programadores de jogos recentemente redundantes (pelo menos até que paguem o aluguel atrasado).

A necessidade subjacente é poder ouvir um fluxo muito alto de largura de banda de dados do mercado, como preços de ações (commodities, commodities, câmbio) e, em seguida, tomar uma decisão muito rápida de comprar / vender / não fazer nada com base na segurança, no preço e participações atuais.

Obviamente, tudo isso pode dar errado também.


Então, vou elaborar o ponto das matrizes de bits . Digamos que temos um sistema de negociação de alta frequência que opera em uma longa lista de pedidos (compre 5k IBM, venda 10k DELL etc.). Digamos que precisamos determinar rapidamente se todos os pedidos foram atendidos, para que possamos avançar para a próxima tarefa. Na programação OO tradicional, será semelhante a:

class Order {
  bool _isFilled;
  ...
public:
  inline bool isFilled() const { return _isFilled; }
};

std::vector<Order> orders;
bool needToFillMore = std::any_of(orders.begin(), orders.end(), 
  [](const Order & o) { return !o.isFilled(); } );

a complexidade algorítmica desse código será O (N), pois é uma varredura linear. Vamos dar uma olhada no perfil de desempenho em termos de acessos à memória: cada iteração do loop dentro de std :: any_of () chamará o.isFilled (), que está embutido, tornando-se um acesso à memória de _isFilled, 1 byte (ou 4, dependendo da sua arquitetura, compilador e configurações do compilador) em um objeto digamos 128 bytes no total. Portanto, estamos acessando 1 byte em cada 128 bytes. Quando lemos o byte de 1 byte, presumindo que seja o pior caso, obteremos uma falha no cache de dados da CPU. Isso fará com que uma solicitação de leitura para a RAM leia uma linha inteira da RAM ( veja aqui para mais informações ) apenas para ler 8 bits. Portanto, o perfil de acesso à memória é proporcional a N.

Compare isso com:

const size_t ELEMS = MAX_ORDERS / sizeof (int);
unsigned int ordersFilled[ELEMS];

bool needToFillMore = std::any_of(ordersFilled, &ordersFilled[ELEMS+1],
   [](int packedFilledOrders) { return !(packedOrders == 0xFFFFFFFF); }

o perfil de acesso à memória disso, assumindo o pior caso novamente, é ELEMS dividido pela largura de uma linha de RAM (varia - pode ser de canal duplo ou triplo, etc.).

Com efeito, estamos otimizando algoritmos para padrões de acesso à memória. Nenhuma quantidade de RAM ajudará - é o tamanho do cache de dados da CPU que causa essa necessidade.

Isso ajuda?


Há uma excelente conversa sobre CPPCon sobre programação de baixa latência (para HFT) no YouTube: https://www.youtube.com/watch?v=NH1Tta7purM

JBRWilkinson
fonte
"matrizes de bits múltiplos em vez de propriedades de matriz de objetos com propriedades booleanas", o que você quer dizer com isso?
user997112
1
Eu elaborei com exemplos e links.
JBRWilkinson
Indo um passo adiante - em vez de usar um byte inteiro para indicar se um pedido é preenchido ou não - você pode usar apenas um bit. Portanto, em um único cache (64 bytes) - você pode representar o estado de 256 pedidos. Então - menos erros.
quixver
Além disso - se você estiver fazendo verificações lineares de memória - o pré-buscador de hardware faz um ótimo trabalho ao carregar seus dados. Desde que você acesse a memória seqüencialmente ou a passos largos ou algo simples. Mas se você estiver acessando a memória de qualquer forma não seqüencial - o pré-buscador da CPU fica confuso. Por exemplo, uma pesquisa binária. Nesse ponto, o programador pode ajudar a CPU com dicas - _mm_prefetch.
quixver
-2

Como eu não havia colocado um ou dois softwares de alta frequência em produção, diria as coisas mais importantes:

  1. A configuração de hardware e os administradores de sistema, juntamente com os engenheiros de rede, NÃO definem um bom resultado do número de pedidos processados ​​pelo sistema de negociação, mas podem fazer o rebaixamento muito se não souberem o básico descrito acima.
  2. A única pessoa que realmente faz o sistema realizar transações de alta frequência é um cientista da computação que reúne o código em c ++

    Entre os conhecimentos utilizados está

    A. Operações de comparação e troca.

    • como o CAS é usado no processador e como o computador o suporta para ser usado no chamado processamento de estrutura sem bloqueio. Ou processamento sem bloqueio. Não vou escrever um livro inteiro aqui. Em resumo, o compilador GNU e o compilador Microsoft suportam o uso direto de instruções CAS. Ele permite que seu código tenha "No.Wair" ao extrair elemento da fila ou colocar um novo na fila.
  3. O cientista talentoso usará mais. Ele deve encontrar nos novos "padrões" recentes um que apareceu primeiro em Java. Chamado padrão DISRUPTOR. A troca de dobras LMAX na Europa explicou à comunidade de alta frequência que a utilização baseada em encadeamento em processadores modernos perderia o tempo de processamento na liberação do cache de memória pela CPU se a fila daya não estiver alinhada com o tamanho do cache da CPU moderna = 64

    Portanto, para essa leitura, eles tornaram público um código java que permite que o processo de multiencadeamento use o cache da CPU do hardware corretamente, sem resolução de conflitos. E um bom cientista da computação PRECISA descobrir que esse padrão já foi portado para c ++ ou se portar.

    Essa é uma maneira de proficiência além de qualquer configuração de administrador. Isso está no coração real de alta frequência hoje.

  4. O cara da ciência da computação TEM QUE escrever muito código C ++, não apenas para ajudar o pessoal de controle de qualidade. Mas também
    • validar nos comerciantes enfrentam comprovada velocidade alcançada
    • condenar usou diferentes tecnologias antigas e expô-las com seu próprio código para mostrar que elas não produzem bons resultados
    • escreva o próprio código c ++ de comunicação multiencadeada com base na velocidade comprovada do pupe / selecione o kernel em vez de usar novamente as tecnologias antigas. Vou dar um exemplo - a biblioteca tcp moderna é o ICE. E as pessoas que fizeram isso são brilhantes. Mas suas prioridades estavam no campo da compatividade com muitas línguas. Então. Você pode fazer melhor em c ++. Portanto, procure exemplos de alto desempenho com base na chamada seletiva ASSÍNCRONA. E não vá para vários consumidores, múltiplos produtores - não para HF.
      E você ficará surpreso ao descobrir que o pipe é usado SOMENTE PARA a notificação do kernel da mensagem que chegou. Você pode colocar o número da mensagem de 64 bits lá - mas para o conteúdo, você acessa a fila CAS sem bloqueio. Disparado por select()chamada assíncrona do kernel .
    • além disso. Aprenda sobre a atribuição com afinidade de encadeamento c ++ ao seu encadeamento que faz o piping / enfileiramento de suas mensagens. Esse segmento deve ter afinidade central. Ninguém mais deve usar o mesmo número de núcleo da CPU.
    • e assim por diante.

Como você pode ver - a alta frequência é um CAMPO DE DESENVOLVIMENTO. Você não pode ser apenas um programador de C ++ para ter sucesso.

E quando digo para ter sucesso, quero dizer que o fundo de hedge para o qual você trabalhará reconhecerá os esforços de turismo em remuneração anual além do número de pessoas e recrutadores que estão falando.

Os tempos das perguntas simples sobre construtores / destruidores desapareceram para sempre. E o c ++ ... migrou com novos compiladores para aliviá-lo do gerenciamento de memória e impor não-herança de grande profundidade nas classes. Perda de tempo. O paradigma de reutilização de código foi alterado. Não se trata apenas de quantas classes você criou em polimorfo. Trata-se de um desempenho em tempo confirmado direto do código que você pode reutilizar.

Portanto, é sua escolha entrar na curva de aprendizado lá, ou não. Ele nunca atingirá um sinal de parada.

alex p
fonte
6
Você pode se esforçar para ortografia e formatação. Na sua forma atual, este post é pouco compreensível.
CodesInChaos
1
Você descreve a situação de 10 anos atrás. Atualmente, as soluções baseadas em hardware superam facilmente o C ++ puro, não importa o quão otimizado seja o seu C ++.
Sjoerd
Para quem quer saber o que é uma solução baseada em hardware - são principalmente as soluções FPGA, onde o código é realmente gravado na memória rápida e não é alterado sem o retorno da chamada memória ROM. Somente leitura
alex p
@alexp Você claramente não sabe do que está falando. FPGA é algo diferente de "código gravado na memória rápida".
Sjoerd