Dernièrement, on m'a demandé d'écrire une fonction qui lit le fichier binaire dans le std::vector<BYTE>
où BYTE
est un unsigned char
. Très vite, je suis arrivé avec quelque chose comme ça:
#include <fstream>
#include <vector>
typedef unsigned char BYTE;
std::vector<BYTE> readFile(const char* filename)
{
// open the file:
std::streampos fileSize;
std::ifstream file(filename, std::ios::binary);
// get its size:
file.seekg(0, std::ios::end);
fileSize = file.tellg();
file.seekg(0, std::ios::beg);
// read the data:
std::vector<BYTE> fileData(fileSize);
file.read((char*) &fileData[0], fileSize);
return fileData;
}
qui semble être inutilement compliqué et la distribution explicite à char*
que j'ai été obligé d'utiliser tout en appelant file.read
ne me fait pas sentir mieux à ce sujet.
Une autre option consiste à utiliser std::istreambuf_iterator
:
std::vector<BYTE> readFile(const char* filename)
{
// open the file:
std::ifstream file(filename, std::ios::binary);
// read the data:
return std::vector<BYTE>((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
}
ce qui est assez simple et court, mais je dois quand même utiliser le std::istreambuf_iterator<char>
même quand je lis dans std::vector<unsigned char>
.
La dernière option qui semble parfaitement simple consiste à utiliser std::basic_ifstream<BYTE>
, qui exprime un peu explicitement que "Je veux un flux de fichier d'entrée et que je veux l'utiliser pour lire BYTE
s" :
std::vector<BYTE> readFile(const char* filename)
{
// open the file:
std::basic_ifstream<BYTE> file(filename, std::ios::binary);
// read the data:
return std::vector<BYTE>((std::istreambuf_iterator<BYTE>(file)),
std::istreambuf_iterator<BYTE>());
}
mais je ne sais pas si basic_ifstream
est un choix approprié dans ce cas.
Quel est le meilleur moyen de lire un fichier binaire dans le vector
? J'aimerais aussi savoir ce qui se passe "dans les coulisses" et quels sont les problèmes que je pourrais éventuellement rencontrer (mis à part le fait que le flux n’est pas ouvert correctement, ce qui pourrait être évité par un simple is_open
cocher).
Y a-t-il une bonne raison pour laquelle on préférerait utiliser std::istreambuf_iterator
ici?
(le seul avantage que je peux voir est la simplicité)
Lors des tests de performances, j'inclurais un scénario de test pour:
std::vector<BYTE> readFile(const char* filename)
{
// open the file:
std::ifstream file(filename, std::ios::binary);
// Stop eating new lines in binary mode!!!
file.unsetf(std::ios::skipws);
// get its size:
std::streampos fileSize;
file.seekg(0, std::ios::end);
fileSize = file.tellg();
file.seekg(0, std::ios::beg);
// reserve capacity
std::vector<BYTE> vec;
vec.reserve(fileSize);
// read the data:
vec.insert(vec.begin(),
std::istream_iterator<BYTE>(file),
std::istream_iterator<BYTE>());
return vec;
}
Je pense que le constructeur de la méthode 1 touche les éléments du vector
, puis que le read
touche à nouveau chaque élément.
Les méthodes 2 et 3 semblent les plus prometteuses, mais pourraient en souffrir un ou plusieurs resize
. D'où la raison de reserve
avant de lire ou d'insérer.
Je voudrais aussi tester avec std::copy
:
...
std::vector<byte> vec;
vec.reserve(fileSize);
std::copy(std::istream_iterator<BYTE>(file),
std::istream_iterator<BYTE>(),
std::back_inserter(vec));
Au final, je pense que la meilleure solution évitera operator >>
de istream_iterator
(et tous les frais généraux et la bonté de operator >>
essayant d’interpréter des données binaires). Mais je ne sais pas quoi utiliser qui vous permet de copier directement les données dans le vecteur.
Enfin, mes tests avec des données binaires montrent que ios::binary
n'est pas honoré. D'où la raison de noskipws
de <iomanip>
.
std::ifstream stream("mona-Lisa.raw", std::ios::in | std::ios::binary);
std::vector<uint8_t> contents((std::istreambuf_iterator<char>(stream)), std::istreambuf_iterator<char>());
for(auto i: contents) {
int value = i;
std::cout << "data: " << value << std::endl;
}
std::cout << "file size: " << contents.size() << std::endl;
Puisque vous chargez le fichier entier en mémoire, la version la plus optimale consiste à mapper le fichier en mémoire. Cela est dû au fait que le noyau charge le fichier dans le cache de pages du noyau et qu'en mappant le fichier, vous exposez simplement ces pages du cache dans votre processus. Aussi appelé zéro copie.
Quand vous utilisez std::vector<>
_ il copie les données du cache de page du noyau dans std::vector<>
qui est inutile lorsque vous voulez simplement lire le fichier.
De plus, lors du passage de deux itérateurs d’entrée à std::vector<>
il augmente sa mémoire tampon lors de la lecture car il ne connaît pas la taille du fichier. Lors du redimensionnement std::vector<>
à la taille du fichier d’abord, il supprime inutilement son contenu car il va de toute façon être écrasé par les données du fichier. Les deux méthodes sont sous-optimales en termes d'espace et de temps.
J'aurais pensé que la première méthode, utilisant la taille et utilisant stream::read()
, serait la plus efficace. Le "coût" du casting pour char *
est probablement zéro - les lancers de ce type disent simplement au compilateur que "Hey, je sais que tu penses que c'est un type différent, mais je veux vraiment ce type ici ...", et n'ajoute pas d'instructions supplémentaires - si vous souhaitez le confirmer, essayez de lire le fichier dans un tableau de caractères et comparez le code assembleur réel. Mis à part un peu de travail supplémentaire pour trouver l'adresse de la mémoire tampon à l'intérieur du vecteur, il ne devrait y avoir aucune différence.
Comme toujours, le seul moyen de déterminer avec certitude ce qui est le plus efficace est de le mesurer. "Demander sur Internet" n'est pas une preuve.