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