web-dev-qa-db-fra.com

Amélioration des performances de lecture du fichier mmap memcpy

J'ai une application qui lit séquentiellement les données d'un fichier. Certaines sont lues directement à partir d'un pointeur sur le fichier mmaped et d'autres parties sont memcpyed du fichier dans un autre tampon. J'ai remarqué des performances médiocres lorsque je faisais beaucoup memcpy de toute la mémoire dont j'avais besoin (blocs de 1 Mo) et de meilleures performances lorsque je passais beaucoup d'appels memcpy plus petits (lors de mes tests, j'ai utilisé 4 Ko, la taille de la page, qui prenait le temps de courir.) Je crois que le problème est un très grand nombre de défauts de page majeurs lorsqu’on utilise une grande memcpy.

J'ai essayé divers paramètres de réglage (MAP_POPUATE, MADV_WILLNEED, MADV_SEQUENTIAL) sans amélioration notable.

Je ne sais pas pourquoi beaucoup de petits appels memcpy devraient être plus rapides; cela semble contre-intuitif. Y a-t-il un moyen d'améliorer cela?

Les résultats et le code de test suivent.

Fonctionnant sur CentOS 7 (linux 3.10.0), compilateur par défaut (gcc 4.8.5), lisant un fichier de 29 Go à partir d’une matrice RAID de disques normaux.

Courir avec /usr/bin/time -v:

4 Ko memcpy:

User time (seconds): 5.43
System time (seconds): 10.18
Percent of CPU this job got: 75%
Elapsed (wall clock) time (h:mm:ss or m:ss): 0:20.59
Major (requiring I/O) page faults: 4607
Minor (reclaiming a frame) page faults: 7603470
Voluntary context switches: 61840
Involuntary context switches: 59

1 Mo memcpy:

User time (seconds): 6.75
System time (seconds): 8.39
Percent of CPU this job got: 23%
Elapsed (wall clock) time (h:mm:ss or m:ss): 1:03.71
Major (requiring I/O) page faults: 302965
Minor (reclaiming a frame) page faults: 7305366
Voluntary context switches: 302975
Involuntary context switches: 96

MADV_WILLNEED n'a pas semblé avoir beaucoup d'impact sur le résultat de la copie de 1Mo.

MADV_SEQUENTIAL a tellement ralenti le résultat de la copie de 1 Mo que je n'ai pas attendu qu'il soit terminé (au moins 7 minutes).

MAP_POPULATE a ralenti le résultat de la copie de 1 Mo d'environ 15 secondes.

Code simplifié utilisé pour le test:

#include <algorithm>
#include <iostream>
#include <stdexcept>

#include <fcntl.h>
#include <stdint.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>

int
main(int argc, char *argv[])
{
  try {
    char *filename = argv[1];

    int fd = open(filename, O_RDONLY);
    if (fd == -1) {
      throw std::runtime_error("Failed open()");
    }

    off_t file_length = lseek(fd, 0, SEEK_END);
    if (file_length == (off_t)-1) {
      throw std::runtime_error("Failed lseek()");
    }

    int mmap_flags = MAP_PRIVATE;
#ifdef WITH_MAP_POPULATE
    mmap_flags |= MAP_POPULATE;  // Small performance degredation if enabled
#endif

    void *map = mmap(NULL, file_length, PROT_READ, mmap_flags, fd, 0);
    if (map == MAP_FAILED) {
      throw std::runtime_error("Failed mmap()");
    }

#ifdef WITH_MADV_WILLNEED
    madvise(map, file_length, MADV_WILLNEED);    // No difference in performance if enabled
#endif

#ifdef WITH_MADV_SEQUENTIAL
    madvise(map, file_length, MADV_SEQUENTIAL);  // Massive performance degredation if enabled
#endif

    const uint8_t *file_map_i = static_cast<const uint8_t *>(map);
    const uint8_t *file_map_end = file_map_i + file_length;

    size_t memcpy_size = MEMCPY_SIZE;

    uint8_t *buffer = new uint8_t[memcpy_size];

    while (file_map_i != file_map_end) {
      size_t this_memcpy_size = std::min(memcpy_size, static_cast<std::size_t>(file_map_end - file_map_i));
      memcpy(buffer, file_map_i, this_memcpy_size);
      file_map_i += this_memcpy_size;
    }
  }
  catch (const std::exception &e) {
    std::cerr << "Caught exception: " << e.what() << std::endl;
  }

  return 0;
}
12
Alex

Si les systèmes de fichiers et de disques sous-jacents ne sont pas assez rapides, que votre utilisation mmap() ou POSIX open()/read() ou standard C fopen()/fread() ou C++ iostream importe peu.

Si les performances comptent vraiment et si le ou les systèmes de fichiers et de disque sous-jacents sont suffisamment rapides, mmap() est probablement le pire moyen de lire un fichier de manière séquentielle. La création de pages mappées est une opération relativement coûteuse et, chaque octet de données n'étant lu qu'une fois, le coût par accès réel peut être extrême. L'utilisation de mmap() peut également augmenter la pression de la mémoire sur votre système. Vous pouvez explicitement munmap() pages après les avoir lues, mais votre traitement peut alors être bloqué pendant que les mappages sont supprimés.

L'utilisation directe IO sera probablement la plus rapide, en particulier pour les gros fichiers car il n'y a pas un nombre énorme de défauts de page impliqués. Direct IO contourne le cache de page, ce qui est une bonne chose pour les données lues une seule fois. La mise en mémoire cache de données lues une seule fois - à ne jamais relire - est non seulement inutile, mais aussi potentiellement contre-productive car les cycles du processeur sont utilisés pour expulser des données utiles du cache de pages.

Exemple (en-têtes et vérification d'erreur omis pour plus de clarté):

int main( int argc, char **argv )
{
    // vary this to find optimal size
    // (must be a multiple of page size)
    size_t copy_size = 1024UL * 1024UL;

    // get a page-aligned buffer
    char *buffer;
    ::posix_memalign( &buffer, ( size_t ) ( 4UL * 1024UL ), copy_size );

    // make sure the entire buffer's virtual-to-physical mappings
    // are actually done (can actually matter with large buffers and
    // extremely fast IO systems)
    ::memset( buffer, 0, copy_size );

    fd = ::open( argv[ 1 ], O_RDONLY | O_DIRECT );

    for ( ;; )
    {
        ssize_t bytes_read = ::read( fd, buffer, copy_size );
        if ( bytes_read <= 0 )
        {
            break;
        }
    }

    return( 0 );
}

Certaines mises en garde existent lors de l’utilisation directe IO sous Linux. La prise en charge du système de fichiers peut être inégale et les implémentations de direct IO peuvent être difficiles. Vous devez probablement utiliser un tampon aligné par page pour lire les données, et vous ne pourrez peut-être pas lire la toute dernière page du fichier si ce n'est pas une page complète.

1
Andrew Henle

Je pense que la pièce importante est l'utilisation du processeur. Dans la version 4 Ko, votre code est lié au calcul, ce qui signifie qu'il s'exécutera aussi vite que le processeur le permettra. Dans la version de 1 Mo, votre code est lié aux E/S, ce qui signifie qu'il s'exécutera aussi vite que le sous-système d'E/S le permet. J'essayerais d'augmenter la taille par incréments de 4 Ko jusqu'à ce que vous trouviez le point de croisement. Vous verrez probablement votre meilleure performance à ce stade. Notez également que, les E/S étant beaucoup plus lentes que la CPU, le sous-système E/S dispose généralement d'un cache dont il tire parti. Ainsi, pendant que vous traitez le tampon plus petit, le sous-système d’E/S met en cache la lecture suivante. En outre, une lecture volumineuse peut être obligée de contourner le cache tous ensemble.

0
JonBelanger