ByteBuffer.allocate () contre ByteBuffer.allocateDirect ()

144

Vers allocate()ou versallocateDirect() , telle est la question.

Depuis quelques années maintenant, je suis resté fidèle à l'idée que puisque les DirectByteBuffers sont un mappage mémoire direct au niveau du système d'exploitation, cela fonctionnerait plus rapidement avec les appels get / put que HeapByteBuffers. Je n'ai jamais vraiment été intéressé à connaître les détails exacts de la situation jusqu'à présent. Je veux savoir lequel des deux types de ByteBuffers est plus rapide et à quelles conditions.

ROMANIA_engineer
la source
Pour donner une réponse précise, vous devez dire précisément ce que vous en faites. Si l'un était toujours plus rapide que l'autre, pourquoi y aurait-il deux variantes. Peut-être pouvez-vous expliquer pourquoi vous êtes maintenant "vraiment intéressé à découvrir les détails exacts" BTW: Avez-vous lu le code, en particulier pour DirectByteBuffer?
Peter Lawrey
Ils seront utilisés pour lire et écrire dans les SocketChannels qui sont configurés pour ne pas bloquer. Donc, en ce qui concerne ce que @bmargulies a dit, DirectByteBufferles performances seront plus rapides pour les chaînes.
@Gnarly Au moins, la version actuelle de ma réponse dit que les canaux devraient en bénéficier.
bmargulies

Réponses:

150

Ron Hitches dans son excellent livre Java NIO semble offrir ce que je pensais être une bonne réponse à votre question:

Les systèmes d'exploitation effectuent des opérations d'E / S sur les zones de mémoire. Ces zones de mémoire, en ce qui concerne le système d'exploitation, sont des séquences d'octets contiguës. Il n'est donc pas surprenant que seuls les tampons d'octets soient éligibles pour participer aux opérations d'E / S. Rappelons également que le système d'exploitation accédera directement à l'espace d'adressage du processus, dans ce cas le processus JVM, pour transférer les données. Cela signifie que les zones de mémoire qui sont les cibles des opérations d'E / S doivent être des séquences d'octets contiguës. Dans la JVM, un tableau d'octets peut ne pas être stocké de manière contiguë en mémoire ou le garbage collector peut le déplacer à tout moment. Les tableaux sont des objets en Java, et la façon dont les données sont stockées dans cet objet peut varier d'une implémentation JVM à une autre.

Pour cette raison, la notion de tampon direct a été introduite. Les tampons directs sont destinés à l'interaction avec les canaux et les routines d'E / S natives. Ils font de leur mieux pour stocker les éléments d'octets dans une zone de mémoire qu'un canal peut utiliser pour un accès direct ou brut en utilisant du code natif pour indiquer au système d'exploitation de drainer ou de remplir la zone de mémoire directement.

Les tampons d'octets directs sont généralement le meilleur choix pour les opérations d'E / S. De par leur conception, ils prennent en charge le mécanisme d'E / S le plus efficace disponible pour la JVM. Les tampons d'octets non directs peuvent être passés aux canaux, mais cela peut entraîner une baisse des performances. Il n'est généralement pas possible qu'un tampon non direct soit la cible d'une opération d'E / S native. Si vous passez un objet ByteBuffer non direct à un canal pour l'écriture, le canal peut implicitement effectuer les opérations suivantes à chaque appel:

  1. Créez un objet ByteBuffer direct temporaire.
  2. Copiez le contenu du tampon non direct dans le tampon temporaire.
  3. Effectuez l'opération d'E / S de bas niveau à l'aide du tampon temporaire.
  4. L'objet tampon temporaire est hors de portée et est finalement récupéré.

Cela peut potentiellement entraîner une copie de la mémoire tampon et une rotation des objets sur chaque E / S, ce qui est exactement le genre de choses que nous aimerions éviter. Cependant, selon l'implémentation, les choses peuvent ne pas être si mauvaises. Le moteur d'exécution mettra probablement en cache et réutilisera les tampons directs ou effectuera d'autres astuces intelligentes pour augmenter le débit. Si vous créez simplement un tampon pour une utilisation unique, la différence n'est pas significative. D'un autre côté, si vous allez utiliser le tampon à plusieurs reprises dans un scénario hautes performances, il vaut mieux allouer des tampons directs et les réutiliser.

Les tampons directs sont optimaux pour les E / S, mais ils peuvent être plus coûteux à créer que les tampons d'octets non directs. La mémoire utilisée par les tampons directs est allouée en appelant du code natif spécifique au système d'exploitation, en contournant le tas JVM standard. Selon le système d'exploitation hôte et l'implémentation de la JVM, la configuration et la suppression de tampons directs peuvent être beaucoup plus coûteux que les tampons résidents du tas. Les zones de stockage en mémoire des tampons directs ne sont pas soumises au nettoyage de la mémoire car elles sont en dehors du tas JVM standard.

Les compromis en termes de performances entre l'utilisation de tampons directs et non directs peuvent varier considérablement selon la JVM, le système d'exploitation et la conception du code. En allouant de la mémoire en dehors du tas, vous pouvez soumettre votre application à des forces supplémentaires dont la JVM n'a pas connaissance. Lorsque vous mettez en jeu des pièces mobiles supplémentaires, assurez-vous d'obtenir l'effet souhaité. Je recommande l'ancienne maxime du logiciel: faites-le d'abord fonctionner, puis faites-le rapidement. Ne vous inquiétez pas trop de l'optimisation à l'avance; concentrez-vous d'abord sur l'exactitude. L'implémentation JVM peut être en mesure d'effectuer la mise en cache de la mémoire tampon ou d'autres optimisations qui vous donneront les performances dont vous avez besoin sans trop d'efforts inutiles de votre part.

Edwin Dalorzo
la source
9
Je n'aime pas cette citation car elle contient trop de devinettes. De plus, la JVM n'a certainement pas besoin d'allouer un ByteBuffer direct lors de l'exécution d'E / S pour un ByteBuffer non direct: il suffit de malaxer une séquence d'octets sur le tas, de faire l'IO, de copier les octets vers le ByteBuffer et de libérer les octets. Ces zones pourraient même être mises en cache. Mais il est totalement inutile d'allouer un objet Java pour cela. Les vraies réponses ne seront obtenues qu'en mesurant. La dernière fois que j'ai fait des mesures, il n'y avait aucune différence significative. Je devrais refaire des tests pour trouver tous les détails spécifiques.
Robert Klemme
4
Il est douteux qu'un livre qui décrit NIO (et les opérations natives) puisse contenir des certitudes. Après tout, différentes JVM et différents systèmes d'exploitation gèrent les choses différemment, de sorte que l'auteur ne peut pas être blâmé pour ne pas pouvoir garantir certains comportements.
Martin Tuskevicius
@RobertKlemme, +1, nous détestons tous les conjectures.Cependant, il peut être impossible de mesurer les performances de tous les principaux systèmes d'exploitation, car il y a tout simplement trop de systèmes d'exploitation majeurs. Un autre article a tenté cela, mais nous pouvons voir de nombreux problèmes avec son benchmark, à commencer par "les résultats fluctuent considérablement en fonction du système d'exploitation". Et s'il y a un mouton noir qui fait des trucs horribles comme la copie de tampon sur chaque E / S? Ensuite, à cause de ce mouton, nous pourrions être obligés d'empêcher l'écriture de code que nous utiliserions autrement, juste pour éviter ces pires scénarios.
Pacerier
@RobertKlemme Je suis d'accord. Il y a beaucoup trop de conjectures ici. Il est peu probable que la machine virtuelle Java alloue des tableaux d'octets de manière clairsemée, par exemple.
Marquis de Lorne du
@Edwin Dalorzo: Pourquoi avons-nous besoin d'un tel tampon d'octets dans le monde réel? Sont-ils inventés comme un hack pour partager la mémoire entre les processus? Disons par exemple que la JVM s'exécute sur un processus et que ce serait un autre processus qui s'exécute sur la couche réseau ou liaison de données - qui est responsable de la transmission des données - ces tampons d'octets sont-ils alloués pour partager la mémoire entre ces processus? Veuillez me corriger si je me trompe ..
Tom Taylor
25

Il n'y a aucune raison de s'attendre à ce que les tampons directs soient plus rapides pour l'accès à l' intérieur du jvm. Leur avantage vient lorsque vous les transmettez au code natif - comme le code derrière les canaux de toutes sortes.

bmargulies
la source
En effet. Par exemple, lorsque vous devez effectuer des E / S dans Scala / Java et appeler des bibliothèques Python / natives intégrées avec de grandes données en mémoire pour le traitement algorithmique ou alimenter des données directement vers un GPU dans Tensorflow.
SemanticBeeng
21

puisque les DirectByteBuffers sont un mappage direct de la mémoire au niveau du système d'exploitation

Ils ne le sont pas. Ce ne sont que de la mémoire de processus d'application normale, mais non sujettes à une relocalisation pendant Java GC, ce qui simplifie considérablement les choses à l'intérieur de la couche JNI. Ce que vous décrivez s'applique à MappedByteBuffer.

qu'il serait plus rapide avec les appels get / put

La conclusion ne découle pas de la prémisse; la prémisse est fausse; et la conclusion est également fausse. Ils sont plus rapides une fois que vous entrez dans la couche JNI, et si vous lisez et écrivez à partir de la même, DirectByteBufferils sont beaucoup plus rapides, car les données n'ont jamais à traverser la limite JNI.

Marquis de Lorne
la source
7
C'est un bon point important: sur le chemin de IO, vous devez traverser la frontière Java - JNI à un moment donné. Les tampons d'octets directs et non directs ne déplacent que la frontière: avec un tampon direct, toutes les opérations put de Java land doivent traverser, tandis qu'avec un tampon non direct, toutes les opérations d'E / S doivent traverser. Ce qui est le plus rapide dépend de l'application.
Robert Klemme
@RobertKlemme Votre résumé est incorrect. Avec tous les tampons, toutes les données en provenance et à destination de Java doivent traverser la limite JNI. L'intérêt des tampons directs est que si vous copiez simplement les données d'un canal à un autre, par exemple en téléchargeant un fichier, vous n'avez pas du tout besoin de l'introduire dans Java, ce qui est beaucoup plus rapide.
Marquis de Lorne
où exactement mon résumé est-il incorrect? Et par quel "résumé" commencer? Je parlais explicitement de "mettre des opérations depuis Java land". Si vous ne copiez que des données entre les canaux (c'est-à-dire ne jamais avoir à gérer les données dans Java land), c'est bien sûr une autre histoire.
Robert Klemme
@RobertKlemme Votre déclaration selon laquelle «avec un tampon direct [seulement] toutes les opérations de placement de Java land doivent être croisées» est incorrecte. Les get et les put doivent se croiser.
Marquis of Lorne
EJP, il vous manque apparemment toujours la distinction voulue que @RobertKlemme faisait en choisissant d'utiliser les mots «opérations de placement» dans une phrase et en utilisant les mots «opérations d'E / S» dans la phrase contrastée de la phrase. Dans cette dernière phrase, son intention était de se référer aux opérations entre le tampon et un périphérique fourni par le système d'exploitation.
naki
18

Il est préférable de faire vos propres mesures. La réponse rapide semble être que l'envoi depuis un allocateDirect()tampon prend 25% à 75% de temps en moins que la allocate()variante (testée comme la copie d'un fichier vers / dev / null), en fonction de la taille, mais que l'allocation elle-même peut être considérablement plus lente (même en un facteur de 100x).

Sources:

Raph Levien
la source
Merci. J'accepterais votre réponse mais je recherche des détails plus spécifiques concernant les différences de performances.