Lire un fichier texte en C¶
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | /*
charbychar.c
Lire un fichier texte caractère par caractère
*/
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
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 <fichier_à_lire>\n", argv[0]);
exit(1);
}
}
|
Les résultats de profilage avec la commande gprof:
$ 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 :
$ 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | /*
readToBuffer.c
Utilisation d'un buffer pour réduire le nombre de lectures
*/
#include <stdio.h>
#include <stdlib.h>
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 <fichier_à_lire>\n", argv[0]);
exit(1);
}
}
|
Les résultats de profilage
$ 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
$ 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | /*
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 <stdio.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
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 <fichier_à_lire>\n", argv[0]);
exit(1);
}
}
|
Les analyses gprof et strace, sont sans appel : mmap() est l’approche la plus performante.
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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 :
$ 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-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¶
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