Accélérer la lecture et l'écriture de fichiers sur machine distante en utilisant un cache

Alcatîz

La lecture ou l'écriture de fichier enregistrement par enregistrement ou ligne par ligne sur une machine distante peut, pour des fichiers possédant un très grand nombre d'éléments, prendre beaucoup de temps. Afin d'accélérer substantiellement ces opérations, il est possible de passer par un cache mémoire et lire/écrire le fichier en une seule opération.

Par contre, sur une seule et unique machine, l'intérêt de cette méthode est très limité, voire nul, car le système utilise déjà implicitement une mise en cache.

Commentez

- Titre : Accélérer la lecture et l'écriture de fichiers sur machine distante en utilisant un cache

- Auteur : Alcatîz

- Parution : 14 novembre 2011

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2011 Jean-Luc Gofflot. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- Introduction

- Lecture de fichier

- Création du cache

- Chargement du fichier dans le cache

- Lecture des données dans le cache

- Destruction du cache

- Écriture de fichier

- Création du cache

- Écriture des données dans le cache

- Sauvegarde du cache dans le fichier

- Destruction du cache

- Un exemple concret

- Tests

- Conclusion

- Remerciements

Introduction

Dans le processus de développement d'une application, un des postes importants est la détection des nœuds d'étranglement sur lesquels il faut faire un effort particulier d'optimisation. Les lectures et sauvegardes de données sur disque peuvent faire partie de ces points plus délicats, surtout s'ils ont lieu vers ou depuis une machine distante et si le nombre d'accès disque est très élevé.
Alors que la lecture et l'écriture de fichiers sur une machine locale sont généralement bien optimisées par les systèmes d'exploitation, il n'en va pas de même pour des accès via le réseau et des lectures ou écritures de fichiers contenant un très grand nombre d'éléments (d'enregistrements ou de lignes de texte) peuvent devenir pénalisants pour une application.

Cet article présente une solution parmi d'autres pour optimiser les lectures et écritures de fichiers : l'utilisation d'un cache mémoire, dans lequel vont se faire toutes les lectures et écritures de données. Pour la lecture, on allouera un cache ayant la même taille que le fichier et on y chargera tout le contenu du fichier en une seule fois. Pour l'écriture, on calculera la taille du fichier à créer, on allouera un cache de cette taille, on y copiera toutes les données et on écrira l'entièreté du fichier en une seule fois.

Ami lecteur, il est préférable pour comprendre l'article d'être rompu à l'utilisation des pointeurs. Si ce n'est pas le cas, prenez le temps de consulter les très bons tutorielsà votre disposition sur ce site.

Le compilateur utilisé pour l'exemple développé ci-dessous est Virtual Pascal 2.1. Ce sont les fonctions de l'API Windows qui sont utilisées. Il faudra donc déclarer l'unité Windows dans la clause Uses.

Lecture de fichier

L'objectif de la présente méthode est donc de charger le contenu d'un fichier dans un cache mémoire, en un seul accès disque. Cela fait, nous pourrons lire les données du fichier directement dans le cache puis détruire celui-ci.

Création du cache

Il est évident que nous devons créer un cache suffisamment grand que pour pouvoir recevoir l'entièreté du fichier. Pour cela, il faut connaître la taille de ce dernier.

La fonction GetFileSize de l'API Windows permet de le faire mais il faut lui communiquer le handle du fichier. Nous ouvrons donc le fichier au préalable avec la fonction CreateFile :

Var HandleFichier : tHandle;
Taille : Cardinal;

(* Ouverture du fichier *)
HandleFichier := CreateFile(Chemin,GENERIC_READ,FILE_SHARE_READ,Nil,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,0);
if HandleFichier <> INVALID_HANDLE_VALUE
then
begin
(* Détermination de la taille *)
Taille := GetFileSize(HandleFichier,Nil);
end;

Si la valeur renvoyée par GetFileSize est -1 ($FFFFFFFF), c'est qu'il y a une erreur.

Une fois que la taille du fichier est connue, nous pouvons allouer le cache avec GetMem :

Var Cache : pChar;
Taille : Cardinal;

GetMem(Cache,Taille);

Vous remarquerez que la variable Cacheest de type pChar(de l'unité Strings). Ce n'est bien sûr pas une obligation et vous pouvez utiliser simplement le type Pointer. Le type pCharest toutefois intéressant car il permet des additions et soustractions d'adresses et d'entiers avec beaucoup de souplesse, sans transtypage. C'est juste une question de goût. :)

Chargement du fichier dans le cache

À présent que le cache est alloué dans le tas global, nous allons y charger tout le contenu du fichier, en une seule opération, à l'aide de la fonction ReadFile :

Var Cache : pChar;
NbBytesLus : DWord;
Taille : Cardinal;

if ReadFile(HandleFichier,Cache^,Taille,NbBytesLus,Nil) and (NbBytesLus = Taille)
then
(* ... *)

Pour s'assurer que tout s'est bien déroulé, il faut tester que ReadFile a bien retourné True et que le nombre d'octets lus correspond bien à la taille du fichier.

Lecture des données dans le cache

L'accès aux données dans le cache peut à présent se faire séquentiellement ou aléatoirement, exactement de la même manière que si les données étaient dans le fichier. Il faudra simplement connaître l'adresse de la donnée à accéder dans le cache.
La routine StrMove de l'unité Strings ne se limite pas aux chaînes de caractères ; elle permet de transférer n'importe quel contenu. Ainsi, pour lire un nombre entier dont on connaît l'adresse dans le cache :

Var Entier : LongInt;
Adresse : pChar;

StrMove(pChar(@Entier),Adresse,SizeOf(Entier));

Ou une chaîne de caractères :

Var Chaine : String;
Adresse : pChar;

StrMove(pChar(@Chaine),Adresse,SizeOf(Chaine));

Le calcul de Adresse est simplement l'adresse du cache augmentée du déplacement nécessaire pour atteindre la donnée désirée. Par exemple, pour atteindre le 10e enregistrement d'un fichier d'enregistrements de taille fixe :

Adresse := Cache + 9 * SizeOf(Enregistrement);

Simple, non ?

Dans le cas d'un fichier texte ou d'un fichier d'enregistrements de tailles variables, évidemment, il ne sera pas question d'accès aléatoire dans le cache, tout comme il ne serait pas question d'accès aléatoire dans le fichier lui-même. Le cache devra être parcouru séquentiellement.

Destruction du cache

La technique d'utilisation du cache n'étant intéressante que pour des fichiers de grande taille, cela signifie trivialement que le cache utilise beaucoup de mémoire. Il vaut donc mieux le détruire dès qu'il n'est plus utile. C'est FreeMem qui va s'en charger :

Var Cache : pChar;
Taille : Cardinal;

FreeMem(Cache,Taille);

Écriture de fichier

Pour l'enregistrement sur disque, la technique consiste à stocker toutes les données dans un cache et à enregistrer le contenu du cache en un seul accès disque.

Création du cache

Il va falloir calculer la taille définitive du fichier pour allouer un cache suffisamment grand pour recevoir toutes les données à enregistrer. Le calcul dépend bien entendu du type de données : s'il s'agit d'enregistrements de taille fixe, il suffit de multiplier le nombre d'enregistrements par leur taille ; mais s'il s'agit de texte ou d'enregistrements de tailles variables, il va peut-être falloir parcourir toutes les données pour additionner leur taille respective.

Vous pourriez alors penser qu'il s'agit là d'une perte de temps : d'abord parcourir les données pour calculer la taille totale du fichier, puis les parcourir une seconde fois pour les écrire dans le cache, puis enfin écrire le contenu du cache sur disque. Mais ce n'est pas le cas : le gain en rapidité sera malgré tout substantiel par rapport à un parcours simple des données et à leur écriture directe sur disque.

C'est de nouveau GetMem qui va nous permettre d'allouer le cache dans le tas global :

Var Cache : pChar;
Taille : Cardinal;

GetMem(Cache,Taille);

Écriture des données dans le cache

Nous utiliserons de nouveau la procédure StrMove (de l'unité Strings) pour transférer les données. Par exemple, pour un enregistrement :

Var Enreg = record
Nom, Prenom : String [50];
Age : DWord;
end;
Adresse : pChar;

StrMove(Adresse,pChar(@Enreg),SizeOf(Enreg));

Le calcul de Adresse est simple : chaque fois qu'une donnée est écrite dans le cache, il suffit de l'augmenter de la taille de la donnée qui vient d'être écrite pour obtenir l'adresse exacte où la donnée suivante pourra être écrite.

Sauvegarde du cache dans le fichier

Une fois que le cache contient toutes les données, nous enregistrons son contenu sur disque en une seule opération.

Nous faisons appel pour cela à la fonction CreateFile pour créer le fichier puis à WriteFile pour en écrire le contenu :

Var Cache : pChar;
HandleFichier : tHandle;
NbBytesEcrits : DWord;
Taille : Cardinal;

HandleFichier := CreateFile(Chemin,GENERIC_WRITE,FILE_SHARE_WRITE,Nil,CREATE_ALWAYS,FILE_ATTRIBUTE_ARCHIVE,0);
if HandleFichier <> INVALID_HANDLE_VALUE
then
WriteFile(HandleFichier,Cache^,Taille,NbBytesEcrits,Nil);

Pour s'assurer que tout s'est bien déroulé, il faut tester que WriteFile retourne bien True.

Destruction du cache

C'est FreeMem qui va de nouveau se charger de détruire le cache :

Var Cache : pChar;
Taille : Cardinal;

FreeMem(Cache,Taille);

Un exemple concret

Voici le codesource complet d'un programme qui lit un fichier de plusieurs centaines de milliers d'enregistrements en utilisant la technique du cache. Les enregistrements sont chargés dans une liste chaînée, modifiés puis réenregistrés sur disque.
Je ne me suis pas cassé la tête, il tourne dans une petite fenêtre WinCRT.

Tout à la fin de la page, vous trouverez un code source permettant de créer le fichier de test.

Program DEMO_CACHE;

(* ---------------------------------------------------------------------------------------------------------------------- *)
(*                                                                                                                        *)
(*  Exemple de lecture/écriture de fichier en utilisant un cache.                                                         *)
(*                                                                                                                        *)
(*  30-10-2011 par Alcatîz pour Developpez.com.                                                                           *)
(*  Compilateur : Virtual Pascal 2.1 build 279.                                                                           *)
(*                                                                                                                        *)
(*  Pour la lecture, on alloue un cache ayant la même taille que le fichier et on y charge tout le contenu du fichier en  *)
(*  une seule fois.                                                                                                       *)
(*  Pour l'écriture, on calcule la taille du fichier à créer, on alloue un cache de cette taille, on y copie toutes les   *)
(*  données et on écrit l'entièreté du fichier en une seule fois.                                                         *)
(*                                                                                                                        *)
(* ---------------------------------------------------------------------------------------------------------------------- *)

{$B-,D+,H-,I-,J+,P-,Q-,R-,S-,T-,V+,W-,X+,Z-}
{&AlignCode+,AlignData+,AlignRec+,Asm-,Cdecl-,Comments-,Delphi+,Frame+,G5+}
{&LocInfo+,Open32-,Optimise+,OrgName-,SmartLink+,Speed+,Use32+,ZD-}
{$M 32768}

Uses Strings,   (* Chaînes AZT *)
WinCRT,    (* Emulation de console *)
Windows;   (* API Windows *)

Type (* Liste chaînée bidirectionnelle contenant les données à modifier *)

pDonnees = ^tDonnees;

tDonnees = Record
Entier : LongInt;
Chaine : String;
pPrec, pSuiv : pDonnees;
end;

Const (* Tête et queue de la liste chaînée *)

pTete : pDonnees = Nil;
pQueue : pDonnees = Nil;

Var hWindow : HWnd;                 (* Handle de la fenêtre WinCRT *)
p : pDonnees;                   (* Elément de la liste chaînée *)
NbEnreg : Cardinal;             (* Nombre d'enregistrements *)
HrDebut, HrFin : tSystemTime;   (* Heure de début et de fin pour le chronomètre *)

(* ---------------------------------------------------------------------------------------------------------------------- *)
(*                               Désallocation de la liste chaînée utilisée par le programme                              *)
(* ---------------------------------------------------------------------------------------------------------------------- *)

Procedure DESALLOCATION_LISTE (
var pTete, pQueue : pDonnees   (* Tête et queue de la liste chaînée *)
);
(* Désallocation de la liste chaînée *)
Var p1, p2 : pDonnees;
Begin
p1 := pTete;
while p1 <> Nil do
begin
p2 := p1^.pSuiv;
Dispose(p1);
p1 := p2;
end;
pTete := Nil;
pQueue := Nil;
End;

(* ---------------------------------------------------------------------------------------------------------------------- *)
(*                                                       Chronomètre                                                      *)
(* ---------------------------------------------------------------------------------------------------------------------- *)

Function TEMPS_ECOULE (
HrDebut, HrFin : tSystemTime   (* Mesure du début et de fin *)
) : Cardinal;                   (* Différence en millisecondes *)
(* Retourne le temps écoulé entre deux mesures, en millisecondes.
Par facilité, ne gère pas le cas où la mesure s'est effectuée sur plus d'une seule journée. *)
Begin
TEMPS_ECOULE := Cardinal(HrFin.wHour) * 3600000 +
Cardinal(HrFin.wMinute) * 60000 +
Cardinal(HrFin.wSecond) * 1000 +
Cardinal(HrFin.wMilliseconds) -
Cardinal(HrDebut.wHour) * 3600000 -
Cardinal(HrDebut.wMinute) * 60000 -
Cardinal(HrDebut.wSecond) * 1000 -
Cardinal(HrDebut.wMilliseconds);
End;

(* ---------------------------------------------------------------------------------------------------------------------- *)
(*                                   Chargement et sauvegarde de fichier via un cache                                     *)
(* ---------------------------------------------------------------------------------------------------------------------- *)

Function CACHE_LECTURE (
hWindow : hWnd;          (* Handle de la fenêtre appelante *)
Chemin : pChar;          (* Chemin complet du fichier à charger *)
var Cache : pChar;       (* Adresse du cache *)
var Taille : Cardinal    (* Taille du cache *)
) : LongBool;             (* True si pas d'erreur *)
(* Alloue un cache mémoire et y charge l'entièreté du fichier spécifié *)
Var HandleFichier : tHandle;          (* Handle du fichier *)
NbBytesLus : DWord;               (* Nombre de bytes lus dans le fichier lors de la dernière opération *)
OkOuverture : LongBool;           (* Résultat de l'ouverture du fichier *)
OkTaille : LongBool;              (* Lecture de la taille du fichier *)
OkLecture : LongBool;             (* Résultat de la lecture du fichier *)
OkAllocation : LongBool;          (* Résultat de l'allocation de mémoire dynamique *)
OkFermeture : LongBool;           (* Résultat de la fermeture du fichier *)
Titre : Array [0..51] of Char;    (* Titre de la boîte à messages *)
Texte : Array [0..299] of Char;   (* Texte de la boîte à messages *)
Begin
Cache := Nil;
OkTaille := True;
OkOuverture := True;
OkLecture := True;
OkAllocation := True;
OkFermeture := True;
(* Ouverture du fichier *)
HandleFichier := CreateFile(Chemin,GENERIC_READ,FILE_SHARE_READ,Nil,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,0);
OkOuverture := (HandleFichier <> INVALID_HANDLE_VALUE);
if OkOuverture
then
begin
(* Lecture de la taille du fichier *)
Taille := GetFileSize(HandleFichier,Nil);
OkTaille := (Taille <> $FFFFFFFF);
if OkTaille
then   (* Allocation du cache mémoire *)
begin
GetMem(Cache,Taille);
OkAllocation := (Cache <> Nil);
if OkAllocation
then   (* Copie de l'entièreté du fichier dans le cache *)
begin
if ReadFile(HandleFichier,Cache^,Taille,NbBytesLus,Nil)
then
begin
if NbBytesLus < Taille
then
OkLecture := False;
end
else
OkLecture := False;
end;
end;
(* Fermeture du fichier *)
OkFermeture := CloseHandle(HandleFichier);
end;
(* Message d'erreur éventuel *)
if not OkTaille
then
StrCopy(Texte,'Impossible de d'#233'terminer la taille du fichier')
else
if not OkOuverture
then
StrCopy(Texte,'Impossible d''ouvrir le fichier')
else
if not OkLecture
then
StrCopy(Texte,'Erreur de lecture du fichier')
else
if not OkFermeture
then
StrCopy(Texte,'Erreur lors de la fermeture du fichier')
else
if not OkAllocation
then
StrCopy(Texte,'M'#233'moire insuffisante pour cr'#233'er le cache de lecture du fichier');
if (not OkTaille) or (not OkOuverture) or (not OkLecture) or (not OkFermeture) or (not OkAllocation)
then
begin
StrCat(Texte,#10#13);
StrCat(Texte,Chemin);
StrCopy(Titre,'Chargement de fichier');
MessageBox(hWindow,Texte,Titre,mb_IconHand or mb_Ok);
end;
(* Résultat de la fonction *)
CACHE_LECTURE := OkTaille and OkOuverture and OkLecture and OkFermeture and OkAllocation;
End;

Function CACHE_ECRITURE (
hWindow : hWnd;      (* Handle de la fenêtre appelante *)
Chemin : pChar;      (* Chemin complet du fichier à écrire *)
var Cache : pChar;   (* Adresse du cache à sauvegarder *)
Taille : Cardinal    (* Taille du cache *)
) : LongBool;         (* True si pas d'erreur *)
(* Ecrit le contenu du cache mémoire dans un fichier *)
Var HandleFichier : tHandle;          (* Handle du fichier *)
NbBytesEcrits : DWord;            (* Nombre de bytes écrits dans le fichier lors de la dernière opération *)
OkCreation : LongBool;            (* Résultat de la création du fichier *)
OkEcriture : LongBool;            (* Résultat de l'écriture du fichier *)
OkFermeture : LongBool;           (* Résultat de la fermeture du fichier *)
Titre : Array [0..51] of Char;    (* Titre de la boîte à messages *)
Texte : Array [0..299] of Char;   (* Texte de la boîte à messages *)
Begin
OkCreation := True;
OkEcriture := True;
OkFermeture := True;
HandleFichier := CreateFile(Chemin,GENERIC_WRITE,FILE_SHARE_WRITE,Nil,CREATE_ALWAYS,FILE_ATTRIBUTE_ARCHIVE,0);
OkCreation := (HandleFichier <> INVALID_HANDLE_VALUE);
if OkCreation
then
begin
OkEcriture := WriteFile(HandleFichier,Cache^,Taille,NbBytesEcrits,Nil);
OkFermeture := CloseHandle(HandleFichier);
end;
(* Message d'erreur éventuel *)
if not OkCreation
then
StrCopy(Texte,'Erreur lors de la cr'#233'ation du fichier')
else
if not OkEcriture
then
StrCopy(Texte,'Erreur d'''#233'criture dans le fichier')
else
if not OkFermeture
then
begin
StrCopy(Texte,'Erreur lors de la fermeture du fichier');
StrCat(Texte,#10#13);
StrCat(Texte,Chemin);
end;
if (not OkCreation) or (not OkEcriture) or (not OkFermeture)
then
begin
StrCat(Texte,#10#13);
StrCat(Texte,Chemin);
StrCopy(Titre,'Sauvegarde de fichier');
MessageBox(hWindow,Texte,Titre,mb_IconHand or mb_Ok);
end;
(* Résultat de la fonction *)
CACHE_ECRITURE := OkCreation and OkEcriture and OkFermeture;
End;

Function CHARGEMENT (
hWindow : hWnd;                 (* Handle de la fenêtre appelante *)
FichierSource : pChar;          (* Chemin complet du fichier à charger *)
var pTete, pQueue : pDonnees;   (* Tête et queue de la liste chaînée *)
var NbEnreg : Cardinal          (* Compteur d'enregistrements *)
) : LongBool;                    (* True si pas d'erreur *)
(* Chargement de fichier dans une liste chaînée bidirectionnelle *)
Var AncienCurseur : hCursor;          (* Ancien curseur *)
Cache : pChar;                    (* Cache dans lequel le fichier est entièrement chargé *)
Taille : Cardinal;                (* Taille du fichier *)
Index : Cardinal;                 (* Index dans le parcours séquentiel du cache *)
Adresse : pChar;                  (* Adresse dans le cache depuis laquelle il faut transférer les données *)
OkCache : LongBool;               (* Indique si la mise en cache s'est bien déroulée *)
OkAllocation : LongBool;          (* Résultat de l'allocation de mémoire dynamique *)
p : pDonnees;                     (* Elément à ajouter dans la liste chaînée *)
Titre : Array [0..51] of Char;    (* Titre de la boîte à messages *)
Texte : Array [0..299] of Char;   (* Texte de la boîte à messages *)
Begin
AncienCurseur := SetCursor(LoadCursor(0,IDC_WAIT));
(* Mise en cache du fichier *)
OkCache := CACHE_LECTURE(hWindow,FichierSource,pChar(Cache),Taille);
if OkCache
then
begin
NbEnreg := 0;
Adresse := Cache;
Index := 0;
while (Index < Taille) and OkAllocation do
begin
(* Allocation de l'élément suivant à insérer dans la boîte liste *)
New(p);
if p <> Nil
then
begin
Inc(NbEnreg);
FillChar(p^,SizeOf(p^),0);
(* Entier *)
StrMove(pChar(@p^.Entier),Adresse,SizeOf(p^.Entier));
Inc(Index,SizeOf(p^.Entier));
Adresse := Cache + Index;
(* Chaîne *)
StrMove(pChar(@p^.Chaine),Adresse,SizeOf(p^.Chaine));
Inc(Index,SizeOf(p^.Chaine));
Adresse := Cache + Index;
(* Ajout dans la liste chaînée *)
if pQueue = Nil
then
pTete := p
else
begin
pQueue^.pSuiv := p;
p^.pPrec := pQueue;
end;
pQueue := p;
end
else   (* Mémoire insuffisante *)
OkAllocation := False;
end;
(* Désallocation du cache *)
FreeMem(Cache,Taille);
end;
SetCursor(AncienCurseur);
(* Message d'erreur éventuel (les erreurs liées au fichier sont affichées par Cache_Lecture) *)
if not OkAllocation
then
begin
StrCopy(Texte,'M'#233'moire insuffisante pour charger le fichier');
StrCat(Texte,#10#13);
StrCat(Texte,FichierSource);
StrCopy(Titre,'Chargement de fichier');
MessageBox(hWindow,Texte,Titre,mb_IconHand or mb_Ok);
end;
(* Résultat de la fonction *)
CHARGEMENT := OkCache and OkAllocation;
End;

Function TAILLE_CACHE_SAUVEGARDE (
NbEnreg : Cardinal   (* Nombre d'enregistrements *)
) : Cardinal;         (* Taille en octets *)
(* Calcule la taille du cache d'écriture du fichier *)
Begin
TAILLE_CACHE_SAUVEGARDE := NbEnreg * (SizeOf(Cardinal) + SizeOf(String));
End;

Function SAUVEGARDE (
hWindow : hWnd;             (* Handle de la fenêtre appelante *)
FichierDest : pChar;        (* Chemin complet du fichier à sauvegarder *)
pTete, pQueue : pDonnees;   (* Tête et queue de la liste chaînée *)
NbEnreg : Cardinal          (* Nombre d'enregistrements *)
) : LongBool;                (* True si pas d'erreur *)
(* Sauvegarde du contenu de la liste chaînée dans un fichier *)
Var AncienCurseur : hCursor;          (* Ancien curseur *)
OkAllocation : LongBool;          (* Résultat de l'allocation de mémoire dynamique *)
OkEcriture : LongBool;            (* Résultat de l'écriture dans le fichier *)
Cache : pChar;                    (* Cache dans lequel l'entièreté du fichier est chargée *)
TailleCache : Cardinal;           (* Taille du cache *)
Adresse : pChar;                  (* Adresse courante dans le cache *)
OkCache : LongBool;               (* Indique si la mise en cache s'est bien déroulée *)
Index : Cardinal;                 (* Index courant dans le cache *)
n : Cardinal;                     (* Compteur d'enregistrements *)
p : pDonnees;                     (* Elément à sauvegarder *)
Titre : Array [0..51] of Char;    (* Titre de la boîte à messages *)
Texte : Array [0..299] of Char;   (* Texte de la boîte à messages - Buffer d'écriture dans le fichier *)
Begin
AncienCurseur := SetCursor(LoadCursor(0,IDC_WAIT));
(* Création du cache *)
OkAllocation := True;
TailleCache := TAILLE_CACHE_SAUVEGARDE(NbEnreg);
GetMem(Cache,TailleCache);
OkCache := (Cache <> Nil);
if OkCache
then   (* Mise en cache *)
begin
Adresse := Cache;
Index := 0;
p := pTete;
while p <> Nil do
begin
(* Entier *)
Adresse := Cache + Index;
StrMove(Adresse,pChar(@p^.Entier),SizeOf(p^.Entier));
Inc(Index,SizeOf(p^.Entier));
(* Chaîne *)
Adresse := Cache + Index;
StrMove(Adresse,pChar(@p^.Chaine),SizeOf(p^.Chaine));
Inc(Index,SizeOf(p^.Chaine));
(* Passage à l'élément suivant *)
p := p^.pSuiv;
end;
(* Ecriture du fichier *)
OkEcriture := CACHE_ECRITURE(hWindow,FichierDest,Cache,TailleCache);
(* Désallocation du cache *)
FreeMem(Cache,TailleCache);
end
else
OkAllocation := False;
SetCursor(AncienCurseur);
(* Message d'erreur éventuel (les erreurs liées au fichier sont affichées par CACHE_ECRITURE) *)
if not OkAllocation
then
begin
StrCopy(Texte,'M'#233'moire insuffisante pour cr'#233'er le cache d'''#233'criture du fichier');
Strcat(Texte,#10#13);
StrCat(Texte,FichierDest);
StrCopy(Titre,'Sauvegarde de fichier');
MessageBox(hWindow,Texte,Titre,mb_IconHand or mb_Ok);
end;
(* Résultat de la fonction *)
SAUVEGARDE := OkCache and OkAllocation and OkEcriture;
End;

(* ---------------------------------------------------------------------------------------------------------------------- *)
(*                                                   Programme principal                                                  *)
(* ---------------------------------------------------------------------------------------------------------------------- *)

Begin
InitWinCRT;
WriteLn('D'#233'monstration de lecture/'#233'criture de fichier via un cache.');
WriteLn('30-10-2011 par Alcat'#238'z pour Developpez.com');
Writeln;
Write('Pressez Enter pour d'#233'marrer : ');
ReadLn;
WriteLn;
(* Récupération du handle de la fenêtre WinCRT *)
hWindow := GetFocus;
(* Chargement du contenu du fichier dans la liste chaînée *)
WriteLn('Chargement du fichier... ');
GetLocalTime(HrDebut);   (* Démarrage du chrono *)
if CHARGEMENT(hWindow,'\\Machine_Distante\Repertoire\Demo.dat',pTete,pQueue,NbEnreg)
then
begin
GetLocalTime(HrFin);   (* Arrêt du chrono *)
WriteLn('   OK - Dur'#233'e : ',TEMPS_ECOULE(HrDebut,HrFin),' millisecondes.');
(* Modification des données *)
p := pTete;
while p <> Nil do
begin
p^.Chaine := p^.Chaine + ' a '#233't'#233' modifi'#233' !';
p := p^.pSuiv;
end;
(* Sauvegarde des données modifiées *)
WriteLn('Sauvegarde du fichier... ');
GetLocalTime(HrDebut);   (* Démarrage du chrono *)
if SAUVEGARDE(hWindow,'\\Machine_Distante\Repertoire\DemoModif.dat',pTete,pQueue,NbEnreg)
then
begin
GetLocalTime(HrFin);   (* Arrêt du chrono *)
WriteLn('   OK - Dur'#233'e : ',TEMPS_ECOULE(HrDebut,HrFin),' millisecondes.');
end
else
WriteLn('   Erreur !');
(* Désallocation de la liste chaînée *)
DESALLOCATION_LISTE(pTete,pQueue);
end
else
WriteLn('   Erreur !');
Write('Pressez Enter pour quitter : ');
ReadLn;
DoneWinCRT;
End.

Tests

À titre de comparaison, vous pouvez remplacer la procédure CHARGEMENT par celle-ci, n'utilisant pas de cache :

(* ---------------------------------------------------------------------------------------------------------------------- *)
(*                                     Chargement et sauvegarde de fichier sans cache                                     *)
(* ---------------------------------------------------------------------------------------------------------------------- *)

Function CHARGEMENT_CLASSIQUE (
hWindow : hWnd;                 (* Handle de la fenêtre appelante *)
FichierSource : pChar;          (* Chemin complet du fichier à charger *)
var pTete, pQueue : pDonnees;   (* Tête et queue de la liste chaînée *)
var NbEnreg : Cardinal          (* Compteur d'enregistrements *)
) : LongBool;                    (* True si pas d'erreur *)
(* Chargement de fichier dans une liste chaînée bidirectionnelle *)
Var AncienCurseur : hCursor;          (* Ancien curseur *)
HandleFichier : tHandle;          (* Handle du fichier *)
NbBytesLus : DWord;               (* Nombre de bytes lus dans le fichier lors de la dernière opération *)
OkOuverture : LongBool;           (* Résultat de l'ouverture du fichier *)
OkLecture : LongBool;             (* Résultat de la lecture du fichier *)
OkAllocation : LongBool;          (* Résultat de l'allocation de mémoire dynamique *)
FinFichier : LongBool;            (* Indique que la fin du fichier est atteinte *)
OkFermeture : LongBool;           (* Résultat de la fermeture du fichier *)
p : pDonnees;                     (* Elément à ajouter dans la liste chaînée *)
Titre : Array [0..51] of Char;    (* Titre de la boîte à messages *)
Texte : Array [0..299] of Char;   (* Texte de la boîte à messages - Buffer de lecture du cache *)
Begin
AncienCurseur := SetCursor(LoadCursor(0,IDC_WAIT));
HandleFichier := CreateFile(FichierSource,GENERIC_READ,FILE_SHARE_READ,Nil,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,0);
OkOuverture := (HandleFichier <> INVALID_HANDLE_VALUE);
if OkOuverture
then
begin
OkLecture := True;
FinFichier := False;
OkAllocation := True;
repeat
(* Allocation de l'élément suivant à insérer dans la boîte liste *)
New(p);
if p <> Nil
then
begin
Inc(NbEnreg);
FillChar(p^,SizeOf(p^),0);
(* Entier *)
if ReadFile(HandleFichier,p^.Entier,SizeOf(p^.Entier),NbBytesLus,Nil)
then
begin
if NbBytesLus < SizeOf(p^.Entier)
then
FinFichier := True;
end
else
OkLecture := False;
(* Chaîne *)
if OkLecture and not FinFichier
then
if ReadFile(HandleFichier,p^.Chaine,SizeOf(p^.Chaine),NbBytesLus,Nil)
then
begin
if NbBytesLus < SizeOf(p^.Chaine)
then
FinFichier := True;
end
else
OkLecture := False;
(* Ajout dans la liste chaînée *)
if OkLecture and not FinFichier
then
begin
if pQueue = Nil
then
pTete := p
else
begin
pQueue^.pSuiv := p;
p^.pPrec := pQueue;
end;
pQueue := p;
end
else   (* Désallocation de l'élément *)
Dispose(p);
end
else   (* Mémoire insuffisante *)
OkAllocation := False;
until FinFichier or (not OkAllocation) or (not OkLecture);
end;
OkFermeture := CloseHandle(HandleFichier);
SetCursor(AncienCurseur);
(* Message d'erreur éventuel *)
if not OkOuverture
then
StrCopy(Texte,'Impossible d''ouvrir le fichier')
else
if not OkLecture
then
StrCopy(Texte,'Erreur de lecture du fichier')
else
if not OkFermeture
then
StrCopy(Texte,'Erreur lors de la fermeture du fichier')
else
if not OkAllocation
then
StrCopy(Texte,'M'#233'moire insuffisante pour charger le fichier');
if (not OkOuverture) or (not OkLecture) or (not OkFermeture) or (not OkAllocation)
then
begin
StrCat(Texte,#10#13);
StrCat(Texte,FichierSource);
StrCopy(Titre,'Chargement de fichier');
MessageBox(hWindow,Texte,Titre,mb_IconHand or mb_Ok);
end;
(* Résultat de la fonction *)
CHARGEMENT_CLASSIQUE := OkOuverture and OkLecture and OkFermeture and OkAllocation;
End;

Remplacez alors l'appel de CHARGEMENT dans le programme principal par celui-ci :

if CHARGEMENT_CLASSIQUE(hWindow,'\\Machine_Distante\Repertoire\Demo.dat',pTete,pQueue,NbEnreg)

Pour un fichier de 400 000 enregistrements comprenant un entier long (4 octets) et une chaîne de caractères (256 octets), le temps de chargement sur mon réseau local passe de 28,344 à 19,178 secondes grâce à l'utilisation du cache, soit un gain de plus de 30 %. Le gain enregistré sur le réseau de mon entreprise est même de l'ordre de 70 % aux heures de pointe !

Par contre, lorsque le fichier est stocké sur ma machine de test même, les performances sont pratiquement identiques, avec ou sans cache.

Pour garantir l'objectivité de vos tests (en évitant toute optimisation par le système), supprimez et recréez chaque fois votre fichier Demo.datou bien créez à chaque fois un fichier avec un nouveau nom.

Comme promis, voici le code source du petit programme de création du fichier Demo.dat :

Program CREATE_DEMO_FILE;

Type tDonnees = Record
Entier : LongInt;
Chaine : String;
end;

Var Fichier : File of tDonnees;
n : Cardinal;
s : String;
Buffer : tDonnees;

Begin
InitWinCRT;
Assign(Fichier,'\\Machine_distante\Repertoire\Demo.dat');
Rewrite(Fichier);
for n := 1 to 400000 do
begin
Buffer.Entier := n;
Str(n,s);
Buffer.Chaine := 'Enregistrement ' + s;
Write(Fichier,Buffer);
end;
Close(Fichier);
End.

Conclusion

La technique exposée dans cet article n'est peut-être pas la panacée en matière d'optimisation mais elle a été testée avec succès en production. Il est sans doute possible de l'améliorer.

Remerciements

Je remercie Claude Leloup pour sa relecture.