Gerenciamento de memória para passagem rápida de mensagens entre threads em C ++

9

Suponha que haja dois encadeamentos, que se comunicam enviando mensagens de dados de forma assíncrona. Cada encadeamento possui algum tipo de fila de mensagens.

Minha pergunta é de nível muito baixo: qual pode ser a maneira mais eficiente de gerenciar a memória? Eu posso pensar em várias soluções:

  1. Remetente cria o objeto via new. Chamadas do receptor delete.
  2. Pool de memória (para transferir a memória de volta para o remetente)
  3. Coleta de lixo (por exemplo, Boehm GC)
  4. (se os objetos forem pequenos o suficiente) copie por valor para evitar a alocação de heap completamente

1) é a solução mais óbvia, por isso vou usá-lo como protótipo. As chances são de que já é bom o suficiente. Independentemente do meu problema específico, pergunto-me qual técnica é mais promissora se você está otimizando o desempenho.

Eu esperava que o pool fosse teoricamente o melhor, especialmente porque você pode usar conhecimento extra sobre o fluxo de informações entre os threads. Receio, no entanto, que também seja o mais difícil de acertar. Muita sintonia ... :-(

A coleta de lixo deve ser bem fácil de ser adicionada posteriormente (após a solução 1), e eu esperaria que ele funcionasse muito bem. Então, acho que é a solução mais prática se 1) for muito ineficiente.

Se os objetos forem pequenos e simples, copiar por valor pode ser o mais rápido. No entanto, receio que isso force limitações desnecessárias à implementação das mensagens suportadas, por isso quero evitá-lo.

Philipp Claßen
fonte

Respostas:

9

Se os objetos forem pequenos e simples, copiar por valor pode ser o mais rápido. No entanto, receio que isso force limitações desnecessárias à implementação das mensagens suportadas, por isso quero evitá-lo.

Se você pode antecipar um limite superior char buf[256], por exemplo, Uma alternativa prática, se não puder, que apenas invoca alocações de heap nos casos raros:

struct Message
{
    // Stores the message data.
    char buf[256];

    // Points to 'buf' if it fits, heap otherwise.
    char* data;
};

fonte
3

Vai depender de como você implementa as filas.

Se você optar por uma matriz (estilo round robin), precisará definir um limite superior no tamanho da solução 4. Se você optar por uma fila vinculada, precisará de objetos alocados.

Em seguida, o pool de recursos pode ser feito facilmente quando você substitui o novo e exclui por AllocMessage<T>e freeMessage<T>. Minha sugestão seria limitar a quantidade de tamanhos possíveis Te arredondar ao alocar concreto messages.

A coleta direta de lixo pode funcionar, mas isso pode causar longas pausas quando é necessário coletar uma grande parte e (acho) um desempenho pior do que o novo / excluir.

catraca arrepiante
fonte
3

Se estiver em C ++, basta usar um dos ponteiros inteligentes - unique_ptr funcionaria bem para você, pois não excluirá o objeto subjacente até que ninguém o identifique. Você passa o objeto ptr para o receptor por valor e nunca precisa se preocupar com qual thread deve excluí-lo (nos casos em que o receptor não recebe o objeto).

Você ainda precisaria lidar com o bloqueio entre os threads, mas o desempenho será bom, pois nenhuma memória é copiada (apenas o próprio objeto ptr, que é pequeno).

Alocar memória no heap não é a coisa mais rápida de todos os tempos, então o pool é usado para tornar isso muito mais rápido. Você acabou de pegar o próximo bloco de uma pilha de tamanho pré-definido em um pool, portanto, basta usar uma biblioteca existente para isso.

gbjbaanb
fonte
2
Normalmente, o bloqueio é um problema muito maior do que a cópia em memória. Apenas dizendo.
tdammers
Quando você escreve unique_ptr, eu acho que você quer dizer shared_ptr. Mas, embora não haja dúvida de que o uso de um ponteiro inteligente seja bom para o gerenciamento de recursos, isso não muda o fato de você estar usando alguma forma de alocação e desalocação de memória. Eu acho que essa pergunta é de nível mais baixo.
precisa saber é o seguinte
3

O maior impacto no desempenho ao comunicar um objeto de um encadeamento para outro é a sobrecarga de agarrar um cadeado. Isso é da ordem de vários microssegundos, que é significativamente mais que o tempo médio que um par de new/ deleteleva (da ordem de cem nanossegundos). As newimplementações sãs tentam evitar o travamento a quase todo custo para evitar o impacto no desempenho.

Dito isso, você deseja garantir que não precise bloquear bloqueios ao comunicar os objetos de um encadeamento para outro. Eu conheço dois métodos gerais para conseguir isso. Ambos funcionam apenas unidirecionalmente entre um remetente e um destinatário:

  1. Use um buffer de anel. Ambos os processos controlam um ponteiro nesse buffer, um é o ponteiro de leitura e o outro é o ponteiro de gravação.

    • O remetente primeiro verifica se há espaço para adicionar um elemento comparando os ponteiros, depois adiciona o elemento e depois incrementa o ponteiro de gravação.

    • O receptor verifica se há um elemento para ler comparando os ponteiros, depois lê o elemento e depois incrementa o ponteiro de leitura.

    Os ponteiros precisam ser atômicos, pois são compartilhados entre os threads. No entanto, cada ponteiro é modificado apenas por um thread, o outro precisa apenas de acesso de leitura ao ponteiro. Os elementos no buffer podem ser ponteiros, o que permite dimensionar facilmente seu buffer de anel para um tamanho que não fará o remetente bloquear.

  2. Use uma lista vinculada que sempre contém pelo menos um elemento. O receptor possui um ponteiro para o primeiro elemento, o remetente possui um ponteiro para o último elemento. Esses ponteiros não são compartilhados.

    • O remetente cria um novo nó para a lista vinculada, configurando seu nextponteiro para nullptr. Em seguida, ele atualiza o nextponteiro do último elemento para apontar para o novo elemento. Por fim, ele armazena o novo elemento em seu próprio ponteiro.

    • O receptor observa o nextponteiro do primeiro elemento para ver se há novos dados disponíveis. Nesse caso, ele exclui o primeiro elemento antigo, avança seu próprio ponteiro para apontar para o elemento atual e começa a processá-lo.

    Nessa configuração, os nextponteiros precisam ser atômicos, e o remetente deve certificar-se de não desreferenciar o segundo último elemento depois de definir o nextponteiro. A vantagem é, obviamente, que o remetente nunca precisa bloquear.

Ambas as abordagens são muito mais rápidas do que qualquer abordagem baseada em bloqueio, mas exigem uma implementação cuidadosa para serem acertadas. E, é claro, eles exigem atomicidade de hardware nativo de gravações / cargas de ponteiros; se sua atomic<>implementação usa um bloqueio internamente, você está praticamente condenado.

Da mesma forma, se você tem vários leitores e / ou escritores, está praticamente condenado: você pode tentar criar um esquema sem bloqueio, mas será difícil implementá-lo na melhor das hipóteses. Essas situações são muito mais fáceis de lidar com uma trava. No entanto, depois de abrir um cadeado, você pode parar de se preocupar com new/ deletedesempenho.

cmaster - restabelece monica
fonte
+1 Eu preciso verificar esta solução de buffer de anel como uma alternativa às filas simultâneas usando loops CAS. Parece muito promissor.