Java NIO FileChannel et FileOutputstream performances / utilité

169

J'essaie de déterminer s'il y a une différence de performances (ou d'avantages) lorsque nous utilisons nio FileChannelpar rapport FileInputStream/FileOuputStreamà la normale pour lire et écrire des fichiers sur le système de fichiers. J'ai observé que sur ma machine, les deux fonctionnent au même niveau, aussi souvent la FileChannelmanière est plus lente. Puis-je connaître plus de détails en comparant ces deux méthodes. Voici le code que j'ai utilisé, le fichier avec lequel je teste est autour 350MB. Est-ce une bonne option d'utiliser des classes basées sur NIO pour les E / S de fichiers, si je ne regarde pas l'accès aléatoire ou d'autres fonctionnalités avancées?

package trialjavaprograms;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class JavaNIOTest {
    public static void main(String[] args) throws Exception {
        useNormalIO();
        useFileChannel();
    }

    private static void useNormalIO() throws Exception {
        File file = new File("/home/developer/test.iso");
        File oFile = new File("/home/developer/test2");

        long time1 = System.currentTimeMillis();
        InputStream is = new FileInputStream(file);
        FileOutputStream fos = new FileOutputStream(oFile);
        byte[] buf = new byte[64 * 1024];
        int len = 0;
        while((len = is.read(buf)) != -1) {
            fos.write(buf, 0, len);
        }
        fos.flush();
        fos.close();
        is.close();
        long time2 = System.currentTimeMillis();
        System.out.println("Time taken: "+(time2-time1)+" ms");
    }

    private static void useFileChannel() throws Exception {
        File file = new File("/home/developer/test.iso");
        File oFile = new File("/home/developer/test2");

        long time1 = System.currentTimeMillis();
        FileInputStream is = new FileInputStream(file);
        FileOutputStream fos = new FileOutputStream(oFile);
        FileChannel f = is.getChannel();
        FileChannel f2 = fos.getChannel();

        ByteBuffer buf = ByteBuffer.allocateDirect(64 * 1024);
        long len = 0;
        while((len = f.read(buf)) != -1) {
            buf.flip();
            f2.write(buf);
            buf.clear();
        }

        f2.close();
        f.close();

        long time2 = System.currentTimeMillis();
        System.out.println("Time taken: "+(time2-time1)+" ms");
    }
}
Keshav
la source
5
transferTo/ transferFromserait plus conventionnel pour la copie de fichiers. Quelle que soit la technique qui ne devrait pas rendre votre disque dur plus rapide ou plus lent, même si je suppose qu'il pourrait y avoir un problème si elle lisait de petits morceaux à la fois et obligeait la tête à passer un temps excessif à chercher.
Tom Hawtin - tackline
1
(Vous ne mentionnez pas le système d'exploitation que vous utilisez, ni le fournisseur et la version de JRE.)
Tom Hawtin - Tackline
Oups désolé, j'utilise FC10 avec Sun JDK6.
Keshav

Réponses:

202

Mon expérience avec des fichiers de plus grande taille a été java.nioplus rapide que java.io. Solidement plus rapide. Comme dans la plage> 250%. Cela dit, j'élimine les goulots d'étranglement évidents, dont je pense que votre micro-référence pourrait souffrir. Domaines potentiels à étudier:

La taille du tampon. L'algorithme que vous avez fondamentalement est

  • copie du disque vers le tampon
  • copie du tampon vers le disque

Ma propre expérience a été que cette taille de tampon est mûre pour le réglage. J'ai choisi 4 Ko pour une partie de mon application, 256 Ko pour une autre. Je soupçonne que votre code souffre d'un tampon aussi important. Exécutez des tests de performance avec des tampons de 1 Ko, 2 Ko, 4 Ko, 8 Ko, 16 Ko, 32 Ko et 64 Ko pour vous le prouver.

N'effectuez pas de tests de performances Java qui lisent et écrivent sur le même disque.

Si vous le faites, alors vous comparez vraiment le disque, et non Java. Je suggérerais également que si votre processeur n'est pas occupé, vous rencontrez probablement un autre goulot d'étranglement.

N'utilisez pas de tampon si vous n'en avez pas besoin.

Pourquoi copier en mémoire si votre cible est un autre disque ou une carte réseau? Avec des fichiers plus volumineux, la latence encourue n'est pas triviale.

Comme d'autres l'ont dit, utilisez FileChannel.transferTo()ou FileChannel.transferFrom(). Le principal avantage ici est que la JVM utilise l'accès du système d'exploitation au DMA ( Direct Memory Access ), s'il est présent. (Cela dépend de l'implémentation, mais les versions modernes de Sun et d'IBM sur les processeurs à usage général sont bonnes à faire.) Ce qui se passe, c'est que les données vont directement vers / depuis le disque, vers le bus, puis vers la destination ... en contournant tout circuit via RAM ou CPU.

L'application Web sur laquelle j'ai passé mes journées et mes nuits à travailler est très lourde en IO. J'ai aussi fait des micro-benchmarks et des benchmarks du monde réel. Et les résultats sont sur mon blog, jetez un œil:

Utiliser les données et les environnements de production

Les micro-benchmarks sont sujets à la distorsion. Si vous le pouvez, faites l'effort de collecter des données à partir de ce que vous prévoyez de faire exactement, avec la charge attendue, sur le matériel que vous attendez.

Mes benchmarks sont solides et fiables car ils se sont déroulés sur un système de production, un système costaud, un système sous charge, rassemblés en logs. Pas le disque SATA 7200 tr / min 2,5 "de mon ordinateur portable alors que je regardais intensément la JVM travailler sur mon disque dur.

Sur quoi courez-vous? Cela compte.

Stu Thompson
la source
@Stu Thompson - Merci pour votre message. Je suis tombé sur votre réponse car je fais des recherches sur le même sujet. J'essaie de comprendre les améliorations du système d'exploitation que nio expose aux programmeurs java. Quelques-uns d'entre eux étant - DMA et fichiers mappés en mémoire. Avez-vous rencontré d'autres améliorations de ce type? PS - Vos liens de blog sont rompus.
Andy Dufresne
@AndyDufresne mon blog est en panne pour le moment, sera disponible plus tard cette semaine - en train de le déplacer.
Stu Thompson
1
Que diriez-vous simplement de copier des fichiers d'un répertoire à un autre? (Chaque lecteur de disque différent)
Deckard
1
Intéressant: mailinator.blogspot.in/2008/02 / ...
Jeryl Cook
38

Si la chose que vous voulez comparer est la performance de la copie de fichiers, alors pour le test de canal, vous devriez le faire à la place:

final FileInputStream inputStream = new FileInputStream(src);
final FileOutputStream outputStream = new FileOutputStream(dest);
final FileChannel inChannel = inputStream.getChannel();
final FileChannel outChannel = outputStream.getChannel();
inChannel.transferTo(0, inChannel.size(), outChannel);
inChannel.close();
outChannel.close();
inputStream.close();
outputStream.close();

Cela ne sera pas plus lent que de vous mettre en mémoire tampon d'un canal à l'autre et sera potentiellement beaucoup plus rapide. Selon les Javadocs:

De nombreux systèmes d'exploitation peuvent transférer des octets directement du cache du système de fichiers vers le canal cible sans les copier réellement.

Uckelman
la source
7

Sur la base de mes tests (Win7 64bit, 6 Go de RAM, Java6), NIO transferFrom est rapide uniquement avec de petits fichiers et devient très lent sur des fichiers plus volumineux. Le retournement du tampon de données NIO surpasse toujours les E / S standard.

  • Copie de 1 000 x 2 Mo

    1. NIO (transferFrom) ~ 2300 ms
    2. NIO (flip direct du tampon de données 5000b) ~ 3500 ms
    3. E / S standard (tampon 5000b) ~ 6000 ms
  • Copie 100x20mb

    1. NIO (flip direct du tampon de données 5000b) ~ 4000 ms
    2. NIO (transferFrom) ~ 5000 ms
    3. E / S standard (tampon 5000b) ~ 6500ms
  • Copie 1x1000mb

    1. NIO (flip direct du tampon de données 5000b) ~ 4500 s
    2. E / S standard (tampon 5000b) ~ 7000ms
    3. NIO (transferFrom) ~ 8000 ms

La méthode transferTo () fonctionne sur des morceaux d'un fichier; n'était pas conçu comme une méthode de copie de fichiers de haut niveau: comment copier un fichier volumineux sous Windows XP?

Erkki Nokso-Koivisto
la source
6

Répondre à la partie «utilité» de la question:

Un piège assez subtil de l'utilisation de FileChannelover FileOutputStreamest que l'exécution de l'une de ses opérations de blocage (par exemple read()ou write()) à partir d'un thread qui est dans un état interrompu provoquera la fermeture brusque du canal avecjava.nio.channels.ClosedByInterruptException .

Maintenant, cela pourrait être une bonne chose si tout ce qui a FileChannelété utilisé fait partie de la fonction principale du thread, et la conception en a tenu compte.

Mais cela peut aussi être embêtant s'il est utilisé par une fonctionnalité auxiliaire telle qu'une fonction de journalisation. Par exemple, vous pouvez trouver votre sortie de journalisation soudainement fermée si la fonction de journalisation est appelée par un thread également interrompu.

C'est dommage que ce soit si subtil car ne pas en tenir compte peut entraîner des bogues qui affectent l'intégrité en écriture. [1] [2]

antak
la source
3

J'ai testé les performances de FileInputStream par rapport à FileChannel pour le décodage des fichiers encodés en base64. Dans mes expériences, j'ai testé des fichiers assez volumineux et io traditionnel était toujours un peu plus rapide que nio.

FileChannel pourrait avoir eu un avantage dans les versions précédentes du jvm en raison de la surcharge de synchronisation dans plusieurs classes liées à io, mais jvm moderne est assez bon pour supprimer les verrous inutiles.

Jörn Horstmann
la source
2

Si vous n'utilisez pas la fonction transferTo ou les fonctions non bloquantes, vous ne remarquerez pas de différence entre les E / S traditionnelles et NIO (2) car les E / S traditionnelles sont mappées à NIO.

Mais si vous pouvez utiliser les fonctionnalités NIO telles que transferFrom / To ou si vous souhaitez utiliser des tampons, alors bien sûr NIO est la voie à suivre.

eckes
la source
0

Mon expérience est que NIO est beaucoup plus rapide avec de petits fichiers. Mais quand il s'agit de fichiers volumineux, FileInputStream / FileOutputStream est beaucoup plus rapide.

tangens
la source
4
Avez-vous confondu cela? Ma propre expérience est que java.nioc'est plus rapide avec des fichiers plus volumineux que java.io, pas plus petits.
Stu Thompson
Non, mon expérience est l'inverse. java.nioest rapide tant que le fichier est suffisamment petit pour être mappé en mémoire. S'il s'agrandit (200 Mo et plus), java.ioc'est plus rapide.
tangens
Sensationnel. Le contraire de moi. Notez que vous n'avez pas nécessairement besoin de mapper un fichier pour le lire - on peut lire à partir du fichier FileChannel.read(). Il n'y a pas qu'une seule approche pour lire les fichiers en utilisant java.nio.
Stu Thompson
2
@tangens avez-vous vérifié cela?
sinedsem