ByteBuffer.allocate () vs. ByteBuffer.allocateDirect ()

144

Para allocate()ou para allocateDirect(), eis a questão.

Há alguns anos, apenas fico com o pensamento de que, como DirectByteBuffers é um mapeamento direto da memória no nível do sistema operacional, ele seria mais rápido com chamadas de recebimento / colocação do que HeapByteBuffers. Eu nunca estava realmente interessado em descobrir os detalhes exatos sobre a situação até agora. Quero saber qual dos dois tipos de ByteBuffers é mais rápido e em que condições.

ROMANIA_engineer
fonte
Para dar uma resposta específica, você precisa dizer especificamente o que está fazendo com eles. Se um era sempre mais rápido que o outro, por que haveria duas variantes. Talvez você possa expandir por que agora está "realmente interessado em descobrir os detalhes exatos". BTW: Você leu o código, especialmente para DirectByteBuffer?
precisa saber é o seguinte
Eles serão usados ​​para ler e gravar em SocketChannels configurados para não-bloqueio. Então, com relação ao que @bmargulies disse, DirectByteBuffers terá um desempenho mais rápido nos canais.
@Gnarly Pelo menos a versão atual da minha resposta diz que os canais devem se beneficiar.
21811 bmargulies #

Respostas:

150

Ron Hitches, em seu excelente livro Java NIO, parece oferecer o que eu pensei que poderia ser uma boa resposta para sua pergunta:

Os sistemas operacionais realizam operações de E / S em áreas de memória. Essas áreas de memória, no que diz respeito ao sistema operacional, são seqüências contíguas de bytes. Não é surpresa, portanto, que apenas os buffers de byte sejam elegíveis para participar de operações de E / S. Lembre-se também de que o sistema operacional acessará diretamente o espaço de endereço do processo, neste caso o processo da JVM, para transferir os dados. Isso significa que as áreas de memória que são alvos de perações de E / S devem ser sequências contíguas de bytes. Na JVM, uma matriz de bytes não pode ser armazenada contiguamente na memória, ou o Garbage Collector pode movê-lo a qualquer momento. Matrizes são objetos em Java, e a maneira como os dados são armazenados dentro desse objeto pode variar de uma implementação da JVM para outra.

Por esse motivo, a noção de um buffer direto foi introduzida. Buffers diretos destinam-se à interação com canais e rotinas de E / S nativas. Eles se esforçam ao máximo para armazenar os elementos de bytes em uma área de memória que um canal pode usar para acesso direto ou bruto usando código nativo para dizer ao sistema operacional para drenar ou preencher a área de memória diretamente.

Buffers de byte direto geralmente são a melhor opção para operações de E / S. Por design, eles suportam o mecanismo de E / S mais eficiente disponível para a JVM. Buffers de bytes não indiretos podem ser transmitidos aos canais, mas isso pode resultar em uma penalidade de desempenho. Geralmente, não é possível que um buffer não direto seja o destino de uma operação de E / S nativa. Se você passar um objeto ByteBuffer não indireto para um canal para gravação, o canal poderá implicitamente fazer o seguinte em cada chamada:

  1. Crie um objeto ByteBuffer direto temporário.
  2. Copie o conteúdo do buffer não direto para o buffer temporário.
  3. Execute a operação de E / S de baixo nível usando o buffer temporário.
  4. O objeto de buffer temporário fica fora do escopo e, eventualmente, é coletado como lixo.

Isso pode resultar em cópia de buffer e rotatividade de objetos em todas as E / S, que são exatamente os tipos de coisas que gostaríamos de evitar. No entanto, dependendo da implementação, as coisas podem não ser tão ruins. O tempo de execução provavelmente armazenará em cache e reutilizará os buffers diretos ou executará outros truques inteligentes para aumentar a taxa de transferência. Se você está simplesmente criando um buffer para uso único, a diferença não é significativa. Por outro lado, se você estiver usando o buffer repetidamente em um cenário de alto desempenho, é melhor alocar buffers diretos e reutilizá-los.

Os buffers diretos são ideais para E / S, mas podem ser mais caros de criar do que os buffers de bytes não indiretos. A memória usada pelos buffers diretos é alocada através da chamada para código específico do sistema operacional nativo, ignorando o heap da JVM padrão. Configurar e remover buffers diretos pode ser significativamente mais caro que os buffers residentes em heap, dependendo do sistema operacional do host e da implementação da JVM. As áreas de armazenamento de memória dos buffers diretos não estão sujeitas à coleta de lixo porque estão fora do heap da JVM padrão.

As compensações de desempenho do uso de buffers diretos versus não-diretos podem variar amplamente de acordo com a JVM, o sistema operacional e o design do código. Ao alocar memória fora do heap, você pode sujeitar seu aplicativo a forças adicionais que a JVM desconhece. Ao colocar peças móveis adicionais em jogo, verifique se está obtendo o efeito desejado. Eu recomendo a antiga máxima do software: primeiro faça o trabalho e depois rápido. Não se preocupe muito com otimização antecipadamente; concentre-se primeiro na correção. A implementação da JVM pode ser capaz de executar o armazenamento em cache do buffer ou outras otimizações que fornecerão o desempenho necessário sem muito esforço desnecessário de sua parte.

Edwin Dalorzo
fonte
9
Não gosto dessa citação porque ela contém muita adivinhação. Além disso, a JVM certamente não precisa alocar um ByteBuffer direto ao executar E / S para um ByteBuffer não direto: é suficiente para alocar uma sequência de bytes no heap, fazer o IO, copiar dos bytes para o ByteBuffer e liberar os bytes. Essas áreas podem até ser armazenadas em cache. Mas é totalmente desnecessário alocar um objeto Java para isso. Respostas reais somente serão obtidas através da medição. Na última vez que fiz medições, não houve diferença significativa. Eu precisaria refazer os testes para obter todos os detalhes específicos.
Robert Klemme
4
É questionável se um livro que descreve NIO (e operações nativas) pode ter certezas. Afinal, JVMs e sistemas operacionais diferentes gerenciam as coisas de maneira diferente, portanto, o autor não pode ser responsabilizado por não conseguir garantir determinado comportamento.
Martin Tuskevicius
@RobertKlemme, +1, todos nós odiamos as suposições. No entanto, pode ser impossível medir o desempenho de todos os principais sistemas operacionais, já que existem muitos sistemas operacionais principais. Outro post tentou isso, mas podemos ver muitos problemas com o benchmark, começando com "os resultados variam amplamente, dependendo do sistema operacional". Além disso, e se houver uma ovelha negra que faça coisas horríveis, como cópia de buffer em todas as E / S? Então, por causa dessas ovelhas, podemos ser forçados a impedir a gravação de código que, de outra forma, usaríamos , apenas para evitar esses piores cenários.
Pacerier 18/08/14
@RobertKlemme Concordo. Há muita adivinhação aqui. É improvável que a JVM aloque matrizes de bytes esparsamente, por exemplo.
Marquis of Lorne
@ Edwin Dalorzo: Por que precisamos de um buffer de byte no mundo real? Eles são inventados como um hack para compartilhar memória entre o processo? Por exemplo, por exemplo, a JVM é executada em um processo e seria outro processo executado na rede ou na camada de enlace de dados - responsável pela transmissão dos dados - esses buffers de byte são alocados para compartilhar memória entre esses processos? Por favor me corrijam se eu estiver errado ..
Tom Taylor
25

Não há razão para esperar que buffers diretos sejam mais rápidos para acessar dentro da jvm. A vantagem deles é quando você os passa para o código nativo - como o código por trás de canais de todos os tipos.

bmargulies
fonte
De fato. Por exemplo, quando é necessário fazer IO no Scala / Java e chamar bibliotecas nativas Python / nativas com grandes dados de memória para processamento algorítmico ou alimentar dados diretamente em uma GPU no Tensorflow.
SemanticBeeng
21

como o DirectByteBuffers é um mapeamento direto da memória no nível do SO

Eles não são. Eles são apenas memória normal do processo do aplicativo, mas não estão sujeitos a realocação durante o Java GC, o que simplifica consideravelmente as coisas dentro da camada JNI. O que você descreve se aplica MappedByteBuffer.

que teria um desempenho mais rápido com as chamadas de obter / colocar

A conclusão não segue da premissa; a premissa é falsa; e a conclusão também é falsa. Eles são mais rápidos quando você entra na camada JNI e, se você está lendo e gravando a partir da mesma, DirectByteBuffereles são muito mais rápidos, porque os dados nunca precisam cruzar o limite da JNI.

Marquês de Lorne
fonte
7
Este é um ponto bom e importante: no caminho do IO, você precisa cruzar a borda Java - JNI em algum momento. Os buffers de byte direto e não direto movem apenas a borda: com um buffer direto, todas as operações de put do Java land precisam atravessar, enquanto com um buffer não direto, todas as operações de E / S precisam atravessar. O que é mais rápido depende da aplicação.
Robert Klemme
@RobertKlemme Seu resumo está incorreto. Com todos os buffers, qualquer dado vindo de e para Java deve cruzar o limite da JNI. O objetivo dos buffers diretos é que, se você está apenas copiando os dados de um canal para outro, por exemplo, fazendo upload de um arquivo, não precisa inseri-lo no Java, o que é muito mais rápido.
Marquês de Lorne
onde exatamente meu resumo está incorreto? E que "resumo" para começar? Eu estava falando explicitamente sobre "colocar operações da terra Java". Se você copiar apenas os dados entre os Canais (ou seja, nunca precisará lidar com os dados no território de Java), é uma história diferente, é claro.
Robert Klemme 21/03
@RobertKlemme Sua declaração de que 'com um buffer direto [somente] todas as operações de colocação do Java land precisam ser cruzadas' está incorreta. Os dois recebe e coloca têm que atravessar.
Marquês de Lorne
EJP, aparentemente você ainda está perdendo a distinção pretendida que o @RobertKlemme estava fazendo ao optar por usar as palavras "operações de colocação" em uma frase e usando as palavras "operações de IO" na frase contrastada da frase. Na última frase, sua intenção era se referir a operações entre o buffer e um dispositivo fornecido pelo SO de algum tipo.
Naki
18

Melhor fazer suas próprias medições. A resposta rápida parece ser que o envio de um allocateDirect()buffer leva 25% a 75% menos tempo que a allocate()variante (testada ao copiar um arquivo para / dev / null), dependendo do tamanho, mas que a alocação em si pode ser significativamente mais lenta (mesmo por um fator de 100x).

Fontes:

Raph Levien
fonte
Obrigado. Eu aceitaria sua resposta, mas estou procurando alguns detalhes mais específicos sobre as diferenças de desempenho.