Lire un fichier texte en C ========================== .. contents:: :local: Introduction ------------ Lire un fichier texte semble être une opération basique. Cependant, selon le contexte, notamment le volume de données à lire, il existe différentes approches pour lire le contenu d’un fichier texte. Cet article est un recueil des techniques que l’on peut utiliser pour lire le contenu d’un fichier texte en C. Les temps et la performance seront évalués à l’aide des commandes **gprof** et **strace**. Le test consiste à lire un fichier de 3205864494 caractères, ayant une taille d’environ 3 Go. Caractère par caractère ----------------------- Une méthode simple permettant de lire tous les fichiers. Ce n’est pas la méthode la plus rapide, mais cela permet avant tout de lire les caractères un à un et est un moyen simple de traduire des caractères par exemple. Le fichier source **charbychar.c** ci-dessous illustre une manière de lire un fichier caractère par caractère. .. code-block:: c :linenos: /* charbychar.c Lire un fichier texte caractère par caractère */ #include #include #include void checkargs(int argc, char **argv); int main(int argc, char **argv) { char *filename; FILE * fp; int c; unsigned long counter = 0; checkargs(argc, argv); filename = argv[1]; fp = fopen(filename, "r"); if(fp==NULL) { puts("Erreur lors de l'ouverture du fichier"); exit(2); } while ((c=fgetc(fp))!=EOF) { counter++; } printf("Le fichier '%s' contient %ld caractères.\n", filename, counter); fclose(fp); return(0); } /* vérifie que la commande est bien appelée avec un argument */ void checkargs(int argc, char **argv) { if(argc==2) { puts("ok, la commande est bien appelée avec un argument."); printf("Et, la valeur de l'argument est '%s'.\n", argv[1]); } else { puts("usage:"); printf("\t%s \n", argv[0]); exit(1); } } Les résultats de profilage avec la commande **gprof**: .. code-block:: text $ gprof -p -b charbychar gmon.out Flat profile: Each sample counts as 0.01 seconds. % cumulative self self total time seconds seconds calls Ts/call Ts/call name 100.45 7.44 7.44 main 0.00 7.44 0.00 1 0.00 0.00 checkargs Et les statistiques retournées par la commande **strace** : .. code-block:: text $ strace -c ./charbychar fichier.txt ok, la commande est bien appelée avec un argument. Et, la valeur de l'argument est 'toto.txt'. Le fichier 'toto.txt' contient 3205864494 caractères. % time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 100.00 0.452846 1 782684 read 0.00 0.000002 1 3 write 0.00 0.000000 0 3 close 0.00 0.000000 0 4 fstat 0.00 0.000000 0 5 mmap 0.00 0.000000 0 4 mprotect 0.00 0.000000 0 1 munmap 0.00 0.000000 0 3 brk 0.00 0.000000 0 3 3 access 0.00 0.000000 0 1 execve 0.00 0.000000 0 1 arch_prctl 0.00 0.000000 0 3 openat ------ ----------- ----------- --------- --------- ---------------- 100.00 0.452848 782715 3 total fread() avec un buffer ---------------------- Cette méthode est beaucoup plus rapide.Utiliser un tampon accélère grandement la lecture du fichier à partir d’une taille de 8Ko. Le fichier source **readToBuffer.c** en est un exemple. L’analyse par **gprof** démontre que cette méthode est bien plus efficace que la précédente. .. code-block:: c :linenos: /* readToBuffer.c Utilisation d'un buffer pour réduire le nombre de lectures */ #include #include void checkargs(int argc, char **argv); int main(int argc, char **argv) { char *filename; char buffer[1024*1024]; FILE * fp; size_t counter = 0; checkargs(argc, argv); filename = argv[1]; fp = fopen(filename, "r"); if(fp==NULL) { puts("Erreur lors de l'ouverture du fichier"); exit(2); } else { while(!feof(fp)) { /* Utilisation de la fonction fread pour lire le contenu du fichier size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); */ counter+=fread(buffer, 1, sizeof(buffer), fp); } fclose(fp); printf("Le fichier '%s' contient %ld caractères.\n", filename, counter); } return(0); } /* vérifie que la commande est bien appelée avec un argument */ void checkargs(int argc, char **argv) { if(argc==2) { puts("ok, la commande est bien appelée avec un argument."); printf("Et, la valeur de l'argument est '%s'.\n", argv[1]); } else { puts("usage:"); printf("\t%s \n", argv[0]); exit(1); } } Les résultats de profilage .. code-block:: text $ gprof -p -b readToBuffer gmon.out Flat profile: Each sample counts as 0.01 seconds. no time accumulated % cumulative self self total time seconds seconds calls Ts/call Ts/call name 0.00 0.00 0.00 1 0.00 0.00 checkargs La commande strace permet elle aussi d’obtenir des informations .. code-block:: text $ strace -c ./readToBuffer fichier.txt ok, la commande est bien appelée avec un argument. Et, la valeur de l'argument est 'toto.txt'. Le fichier 'toto.txt' contient 3205864494 caractères. % time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 100.00 0.242386 79 3060 read 0.00 0.000008 3 3 write 0.00 0.000003 1 3 close 0.00 0.000000 0 4 fstat 0.00 0.000000 0 5 mmap 0.00 0.000000 0 4 mprotect 0.00 0.000000 0 1 munmap 0.00 0.000000 0 3 brk 0.00 0.000000 0 3 3 access 0.00 0.000000 0 1 execve 0.00 0.000000 0 1 arch_prctl 0.00 0.000000 0 3 openat ------ ----------- ----------- --------- --------- ---------------- 100.00 0.242397 3091 3 total mmap() encore plus rapide ------------------------- Voici enfin une méthode moins populaire, permettant de lire le contenu d’un fichier encore plus rapidement. Le fichier source **readmmap.c** en contient un exemple simple. .. code-block:: c :linenos: /* readmmap.c Obtenir la taille du fichier à lire avec la fonction stat() mettant à jour la structure: struct stat { dev_t st_dev; // ID du périphérique contenant le fichier ino_t st_ino; // Numéro inœud mode_t st_mode; // Protection nlink_t st_nlink; // Nb liens matériels uid_t st_uid; // UID propriétaire gid_t st_gid; // GID propriétaire dev_t st_rdev; // ID périphérique (si fichier spécial) off_t st_size; // Taille totale en octets blksize_t st_blksize; // Taille de bloc pour E/S blkcnt_t st_blocks; // Nombre de blocs alloués time_t st_atime; // Heure dernier accès time_t st_mtime; // Heure dernière modification time_t st_ctime; // Heure dernier changement état }; Puis lire le contenu du fichier à l'aide de la fonction mmap(). */ #include #include #include #include #include #include #include void checkargs(int argc, char **argv); int main(int argc, char **argv) { struct stat statbuffer; int res; char *filename; int fd; void *mmapdata; checkargs(argc, argv); puts("Lecture de la taille du fichier avec la fonction stat()."); filename = argv[1]; res = stat(filename, &statbuffer); if(res==0) { puts("fonction stat() appelée avec succès !"); printf("Taille du fichier %s est : %ld bytes.\n", filename, statbuffer.st_size); } else { puts("Erreur lors de l'appel de la fonction stat()."); exit(2); } /* void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); */ fd = open(filename, O_RDONLY, 0); mmapdata = mmap( NULL , statbuffer.st_size , PROT_READ , MAP_PRIVATE | MAP_POPULATE , fd , 0 ); if(mmapdata != MAP_FAILED) { puts("Succès de l'appel à mmap();"); munmap(mmapdata, statbuffer.st_size); } else { puts("Erreur: la fonction mmap() a retourné MAP_FAILED !"); exit(3); } return(0); } /* vérifie que la commande est bien appelée avec un argument */ void checkargs(int argc, char **argv) { if(argc==2) { puts("ok, la commande est bien appelée avec un argument."); printf("Et, la valeur de l'argument est '%s'.\n", argv[1]); } else { puts("usage:"); printf("\t%s \n", argv[0]); exit(1); } } Les analyses **gprof** et **strace**, sont sans appel : *mmap()* est l'approche la plus performante. .. code-block:: text strace -c ./readmmap fichier.txt ok, la commande est bien appelée avec un argument. Et, la valeur de l'argument est 'fichier.txt'. Lecture de la taille du fichier avec la fonction stat(). fonction stat() appelée avec succès ! Taille du fichier fichier.txt est : 3205864494 bytes. Succès de l'appel à mmap(); % time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 66.74 0.071173 11862 6 mmap 32.82 0.035003 17502 2 munmap 0.10 0.000102 34 3 3 access 0.09 0.000097 16 6 write 0.08 0.000080 20 4 mprotect 0.05 0.000054 18 3 openat 0.03 0.000037 12 3 fstat 0.02 0.000025 8 3 brk 0.02 0.000023 12 2 close 0.02 0.000016 16 1 stat 0.01 0.000014 14 1 read 0.01 0.000012 12 1 arch_prctl 0.00 0.000000 0 1 execve ------ ----------- ----------- --------- --------- ---------------- 100.00 0.106636 36 3 total Un script pour mesurer le temps ------------------------------- Plutôt que de chercher à mesurer le temps à l’intérieur du programme, ici, l’idée est d’utiliser un script en python qui exécute le programme et mesure le temps écoulé entre le début et la fin du programme. Le temps n’est pas exact, mais, il donne la tendance. Le programme en python **runtest.py** ci-dessous est très simple et permet de mesurer le temps qu’il a fallu pour exécuter un programme. Et, il devrait retourner des temps très proches de la commande **strace**. .. code-block:: python3 :linenos: import os import datetime import sys program_name = str(sys.argv[1]) program_arg = str(sys.argv[2]) system_command = "{} {}".format(program_name, program_arg) dt_start = datetime.datetime.now() os.system(system_command) dt_stop = datetime.datetime.now() dt_delta = dt_stop - dt_start print("commande: {}".format(system_command)) print("temps: {} sec {} msec".format(dt_delta.seconds, dt_delta.microseconds)) Ci-dessous les appels à notre script, pour les différentes méthodes : .. code-block:: text $ python3 runtest.py ./charbychar fichier.txt ok, la commande est bien appelée avec un argument. Et, la valeur de l'argument est 'fichier.txt'. Le fichier 'fichier.txt' contient 3205864494 caractères. commande: ./charbychar fichier.txt temps: 7 sec 37249 msec $ python3 runtest.py ./readToBuffer fichier.txt ok, la commande est bien appelée avec un argument. Et, la valeur de l'argument est 'fichier.txt'. Le fichier 'fichier.txt' contient 3205864494 caractères. commande: ./readToBuffer fichier.txt temps: 0 sec 240579 msec $ python3 runtest.py ./readmmap fichier.txt ok, la commande est bien appelée avec un argument. Et, la valeur de l'argument est 'fichier.txt'. Lecture de la taille du fichier avec la fonction stat(). fonction stat() appelée avec succès ! Taille du fichier fichier.txt est : 3205864494 bytes. Succès de l'appel à mmap(); commande: ./readmmap fichier.txt temps: 0 sec 101967 msec Résultats --------- récapitulatif des temps bruts mesurés: +-----------------+--------------------+---------------------------------------+ | **Méthode** | **Programme** | **Temps** | +-----------------+--------------------+---------------------------------------+ | *getc()* | *charbychar.c* | *7 secondes et 37242 microsecondes* | +-----------------+--------------------+---------------------------------------+ | *fread()* | *readToBuffer.c* | *0 seconde et 240579 microsecondes* | +-----------------+--------------------+---------------------------------------+ | *mmap()* | *readmmap.c* | *0 seconde et 101967 microsecondes* | +-----------------+--------------------+---------------------------------------+ utiliser la fonction *mmap()* est la méthode la plus rapide pour ouvrir et lire le contenu d’un fichier volumineux. Rappel ------ Une microseconde c’est 10\ :sup:`-6` seconde. Les temps mesurés lors des benchmarks varient d'une exécution à l'autre, mais restent toujours très proche des temps annnoncés. Et surtout, il donnent une idée du temps perçu par l'utilisateur. cpuinfo de la machine utilisée ------------------------------ .. code-block:: text Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Byte Order: Little Endian CPU(s): 8 On-line CPU(s) list: 0-7 Thread(s) per core: 2 Core(s) per socket: 4 Socket(s): 1 NUMA node(s): 1 Vendor ID: GenuineIntel CPU family: 6 Model: 158 Model name: Intel(R) Core(TM) i7-7700K CPU @ 4.20GHz Stepping: 9 CPU MHz: 848.699 CPU max MHz: 4500,0000 CPU min MHz: 800,0000 BogoMIPS: 8400.00 Virtualization: VT-x L1d cache: 32K L1i cache: 32K L2 cache: 256K L3 cache: 8192K