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

144

Para allocate()o para allocateDirect(), esa es la pregunta.

Desde hace algunos años, me he quedado pegado a la idea de que, dado que DirectByteBuffers es un mapeo directo de memoria a nivel del sistema operativo, funcionaría más rápido con las llamadas get / put que HeapByteBuffers. Nunca estuve realmente interesado en averiguar los detalles exactos sobre la situación hasta ahora. Quiero saber cuál de los dos tipos de ByteBuffers es más rápido y en qué condiciones.

RUMANIA_ingeniero
fuente
Para dar una respuesta específica, debe decir específicamente qué está haciendo con ellos. Si uno siempre fue más rápido que el otro, ¿por qué habría dos variantes? Quizás pueda ampliar por qué ahora está "realmente interesado en conocer los detalles exactos" Por cierto: ¿ha leído el código, especialmente para DirectByteBuffer?
Peter Lawrey
Se usarán para leer y escribir en correos SocketChannelelectrónicos que están configurados para no bloquearse. Entonces, con respecto a lo que dijo @bmargulies, DirectByteBuffers funcionará más rápido para los canales.
@Gnarly Al menos la versión actual de mi respuesta dice que se espera que los canales se beneficien.
bmargulies

Respuestas:

150

Ron Hitches en su excelente libro Java NIO parece ofrecer lo que pensé que podría ser una buena respuesta a su pregunta:

Los sistemas operativos realizan operaciones de E / S en áreas de memoria. Estas áreas de memoria, en lo que respecta al sistema operativo, son secuencias contiguas de bytes. No sorprende entonces que solo los buffers de bytes sean elegibles para participar en las operaciones de E / S. Recuerde también que el sistema operativo accederá directamente al espacio de direcciones del proceso, en este caso el proceso JVM, para transferir los datos. Esto significa que las áreas de memoria que son objetivos de las operaciones de E / S deben ser secuencias contiguas de bytes. En la JVM, una matriz de bytes no puede almacenarse contiguamente en la memoria, o el recolector de basura podría moverla en cualquier momento. Las matrices son objetos en Java, y la forma en que se almacenan los datos dentro de ese objeto podría variar de una implementación de JVM a otra.

Por esta razón, se introdujo la noción de un buffer directo. Los buffers directos están destinados a la interacción con canales y rutinas de E / S nativas. Hacen un gran esfuerzo para almacenar los elementos de bytes en un área de memoria que un canal puede usar para acceso directo o sin formato mediante el uso de código nativo para indicar al sistema operativo que drene o llene el área de memoria directamente.

Los buffers de byte directo suelen ser la mejor opción para las operaciones de E / S. Por diseño, admiten el mecanismo de E / S más eficiente disponible para la JVM. Los búferes de bytes no directos se pueden pasar a los canales, pero al hacerlo puede incurrir en una penalización de rendimiento. Por lo general, no es posible que un búfer no directo sea el objetivo de una operación de E / S nativa. Si pasa un objeto ByteBuffer no directo a un canal para escritura, el canal puede hacer lo siguiente implícitamente en cada llamada:

  1. Cree un objeto ByteBuffer directo temporal.
  2. Copie el contenido del búfer no directo al búfer temporal.
  3. Realice la operación de E / S de bajo nivel utilizando el búfer temporal.
  4. El objeto de búfer temporal queda fuera de alcance y finalmente se recolecta basura.

Potencialmente, esto puede dar lugar a la copia del búfer y la rotación de objetos en cada E / S, que son exactamente el tipo de cosas que nos gustaría evitar. Sin embargo, dependiendo de la implementación, las cosas pueden no ser tan malas. El tiempo de ejecución probablemente almacenará en caché y reutilizará memorias intermedias directas o realizará otros trucos inteligentes para aumentar el rendimiento. Si simplemente está creando un búfer para un solo uso, la diferencia no es significativa. Por otro lado, si va a usar el búfer repetidamente en un escenario de alto rendimiento, es mejor que asigne los búferes directos y los reutilice.

Las memorias intermedias directas son óptimas para E / S, pero pueden ser más caras de crear que las memorias intermedias de bytes no directas. La memoria utilizada por las memorias intermedias directas se asigna llamando al código nativo específico del sistema operativo, sin pasar por el montón JVM estándar. La configuración y el desmantelamiento de los buffers directos podrían ser significativamente más caros que los buffers residentes en el montón, dependiendo del sistema operativo host y la implementación de JVM. Las áreas de almacenamiento de memoria de los buffers directos no están sujetas a recolección de basura porque están fuera del montón JVM estándar.

Las compensaciones de rendimiento del uso de buffers directos versus no directos pueden variar ampliamente según la JVM, el sistema operativo y el diseño del código. Al asignar memoria fuera del montón, puede someter su aplicación a fuerzas adicionales que JVM desconoce. Cuando ponga en juego partes móviles adicionales, asegúrese de lograr el efecto deseado. Recomiendo la vieja máxima del software: primero haz que funcione, luego hazlo rápido. No se preocupe demasiado por la optimización por adelantado; concentrarse primero en lo correcto. La implementación de JVM puede realizar el almacenamiento en caché del búfer u otras optimizaciones que le brindarán el rendimiento que necesita sin un esfuerzo innecesario de su parte.

Edwin Dalorzo
fuente
9
No me gusta esa cita porque contiene demasiadas conjeturas. Además, la JVM ciertamente no necesita asignar un ByteBuffer directo al hacer IO para un ByteBuffer no directo: es suficiente colocar mal una secuencia de bytes en el montón, hacer el IO, copiar de los bytes al ByteBuffer y liberar los bytes. Esas áreas incluso podrían almacenarse en caché. Pero es totalmente innecesario asignar un objeto Java para esto. Las respuestas reales solo se obtendrán de la medición. La última vez que hice mediciones no hubo diferencias significativas. Tendría que rehacer las pruebas para obtener todos los detalles específicos.
Robert Klemme
44
Es cuestionable si un libro que describe NIO (y operaciones nativas) puede tener certezas. Después de todo, las diferentes JVM y sistemas operativos manejan las cosas de manera diferente, por lo que no se puede culpar al autor por no poder garantizar cierto comportamiento.
Martin Tuskevicius
@RobertKlemme, +1, todos odiamos las conjeturas. Sin embargo, puede ser imposible medir el rendimiento para todos los sistemas operativos principales, ya que hay demasiados sistemas operativos principales. Otra publicación intentó eso, pero podemos ver muchos problemas con su punto de referencia, comenzando con "los resultados fluctúan ampliamente dependiendo del sistema operativo". Además, ¿qué pasa si hay una oveja negra que hace cosas horribles como copiar el búfer en cada E / S? Luego, debido a esa oveja, es posible que nos veamos obligados a evitar escribir código que de otro modo usaríamos, solo para evitar estos peores escenarios.
Pacerier
@RobertKlemme Estoy de acuerdo. Hay demasiadas conjeturas aquí. Es improbable que la JVM asigne escasamente conjuntos de bytes, por ejemplo.
Marqués de Lorne
@Edwin Dalorzo: ¿Por qué necesitamos tal búfer de bytes en el mundo real? ¿Se inventan como un truco para compartir memoria entre el proceso? Digamos, por ejemplo, que JVM se ejecuta en un proceso y sería otro proceso que se ejecuta en la red o en la capa de enlace de datos, que es responsable de transmitir los datos, ¿están estos búferes de bytes asignados para compartir memoria entre estos procesos? Por favor, corrígeme si estoy equivocado ..
Tom Taylor
25

No hay razón para esperar que los buffers directos sean más rápidos para acceder dentro de jvm. Su ventaja viene cuando los pasa al código nativo, como el código detrás de canales de todo tipo.

bmargulies
fuente
En efecto. Por ejemplo, cuando se necesita hacer IO en Scala / Java y llamar a Python / libs nativas integradas con gran cantidad de datos de memoria para procesamiento algorítmico o alimentar datos directamente a una GPU en Tensorflow.
SemanticBeeng
21

ya que DirectByteBuffers es un mapeo directo de memoria a nivel del sistema operativo

No lo son Son solo memoria de proceso de aplicación normal, pero no están sujetas a reubicación durante Java GC, lo que simplifica considerablemente las cosas dentro de la capa JNI. Lo que usted describe se aplica a MappedByteBuffer.

que funcionaría más rápido con llamadas get / put

La conclusión no se sigue de la premisa; la premisa es falsa; y la conclusión también es falsa. Son más rápidos una vez que ingresas a la capa JNI, y si estás leyendo y escribiendo desde el mismo DirectByteBuffer, son mucho más rápidos, porque los datos nunca tienen que cruzar el límite JNI.

Marqués de Lorne
fuente
77
Este es un punto bueno e importante: en el camino de IO debe cruzar la frontera Java - JNI en algún momento. Los búferes de bytes directos y no directos solo mueven el borde: con un búfer directo, todas las operaciones de colocación desde tierra Java deben cruzarse, mientras que con un búfer no directo, todas las operaciones de E / S deben cruzarse. Lo que es más rápido depende de la aplicación.
Robert Klemme
@RobertKlemme Su resumen es incorrecto. Con todos los búferes, cualquier dato que llegue y salga de Java debe cruzar el límite JNI. El punto de los buffers directos es que si solo está copiando los datos de un canal a otro, por ejemplo, cargando un archivo, no tiene que ingresarlos a Java, lo cual es mucho más rápido.
Marqués de Lorne
¿Dónde está exactamente mi resumen incorrecto? ¿Y qué "resumen" para empezar? Estaba hablando explícitamente de "poner operaciones desde tierra Java". Si solo copia datos entre canales (es decir, nunca tiene que lidiar con los datos en tierra Java), esa es una historia diferente, por supuesto.
Robert Klemme
@RobertKlemme Su afirmación de que "con un búfer directo [solo] todas las operaciones de colocación desde tierra Java deben cruzarse" es incorrecta. Ambos consiguen y ponen tienen que cruzar.
Marqués de Lorne
EJP, aparentemente todavía te falta la distinción que @RobertKlemme estaba haciendo al elegir usar las palabras "poner operaciones" en una frase y usar las palabras "operaciones IO" en la frase contrastada de la oración. En la última frase, su intención era referirse a las operaciones entre el búfer y un dispositivo proporcionado por el sistema operativo de algún tipo.
naki
18

Lo mejor es hacer tus propias medidas. La respuesta rápida parece ser que el envío desde un allocateDirect()búfer lleva entre un 25% y un 75% menos de tiempo que la allocate()variante (probado como copiar un archivo a / dev / null), dependiendo del tamaño, pero que la asignación en sí puede ser significativamente más lenta (incluso si un factor de 100x).

Fuentes:

Raph Levien
fuente
Gracias. Acepto su respuesta, pero estoy buscando algunos detalles más específicos sobre las diferencias en el rendimiento.