I. Contexte
Nous vivons dans un monde où la programmation objet est considérée comme le Graal et la STL (Standard Template Library) comme la seule API à considérer. Et la majorité des développeurs appliquent les informations qu’on leur a martelées durant leur apprentissage. Et, trop souvent, la STL est survendue comme LA panacée ! Tout outil méconnu ou mal maîtrisé peut induire de mauvais choix.
Dans cet article, nous allons comparer les API de fichiers : en l’occurrence les « stream » versus l’API C de gestion de fichiers.
II. Un petit exemple
Essayons d’écrire un programme qui copie un fichier texte dans un autre fichier et effectuons cette tâche via les 2 méthodes suivantes : l’API C et via les streams de la STL… On va ouvrir le fichier en lecture, lire les lignes une à une et les écrire dans le fichier en écriture. On va utiliser la fonction « clock() » pour mesurer les performances des 2 méthodes.
De l’API C, nous allons utiliser :
- fopen : ouverture du fichier
- fclose : fermeture du fichier
- feof : a-t-on atteint la fin du fichier
- fgets : lire une ligne
- fputs : écriture d’une ligne
De l’API de la STL, nous allons utiliser :
- std::ifstream : stream sur fichier en lecture
- std::ofstream : stream de fichier en écriture
- std::getline : lire une ligne dans un stream
test_stl.cpp #include <iostream> #include <fstream> #include <istream> #include <stdio.h> #include <string> namespace STL { void read_file(const char *filename) { char output[512]; sprintf( output, "%s.txt2", filename ); std::string text; std::ifstream s( filename, std::ios::in ); std::ofstream o( output, std::ios::out ); while( std::getline( s, text ) ) { o << text << std::endl; } o.close(); s.close(); } }
test_c.cpp #include <stdio.h> #include <stdlib.h> namespace C { void read_file(const char *filename) { char buffer[ 1024 ]; char output[ 512 ]; FILE *pFile, *pOutput; sprintf( output, "%s.txt1", filename ); pOutput = fopen( output, "w" ); pFile = fopen( filename, "r" ); while( !feof( pFile ) ) { fgets( buffer, sizeof( buffer ), pFile ); fputs(buffer, pOutput); } fclose( pFile ); fclose( pOutput ); } }
main.cpp #include <iostream> #include <time.h> #include <stdlib.h> namespace C { void read_file( const char *filename); } namespace STL { void read_file(const char *filename); } int main( int argc, char **argv ) { clock_t start, stop; const char *filename = "C:\\personnel\\C++\\blog\\c_vs_stl\\Test\\LAM.obj"; std::cout << "C:"; start = clock(); C::read_file(filename); stop = clock(); std::cout << stop - start << "\n"; std::cout << "STL:"; start = clock(); STL::read_file(filename); stop = clock(); std::cout << stop - start << "\n"; system("PAUSE"); return 0; }
Après exécution en release 64 bits (compilé avec gcc), j’ai obtenu sur mon PC :
- C : 579
- STL : 4385
L’API C est 7,5 fois plus rapide que l’API STL. La différence de performance est très importante. Aussi, en utilisant le compilateur de Visual Studio 2017, on a une différence de performance équivalente. En effet, l’API est 8.6 fois plus rapide que la STL.
III. Mais pourquoi tant de haine ?
On m’avait promis que la programmation orientée objet était le nec plus ultra. Mais aussi que le C++ était l’aboutissement de l’évolution de l’informatique ! M’aurait-on menti ?
Il y a une explication ! La STL a été créée sur des concepts : la généricité, l’abstraction… Trop de généricité, dans le cas présent, nuit à l’efficacité via une complexité du code.
1. std::getline
Une des causes de contre-performance est l’implémentation de la fonction – std::getline, qui est la suivante dans l’implémentation Microsoft :
template<class _Elem, class _Traits, class _Alloc> inline basic_istream<_Elem, _Traits>& getline( basic_istream<_Elem, _Traits>&& _Istr, basic_string<_Elem, _Traits, _Alloc>& _Str, const _Elem _Delim) { // get characters into string, discard delimiter typedef basic_istream<_Elem, _Traits> _Myis; ios_base::iostate _State = ios_base::goodbit; bool _Changed = false; const typename _Myis::sentry _Ok(_Istr, true); if (_Ok) { // state okay, extract characters _TRY_IO_BEGIN _Str.erase(); const typename _Traits::int_type _Metadelim = _Traits::to_int_type(_Delim); typename _Traits::int_type _Meta = _Istr.rdbuf()->sgetc(); for (; ; _Meta = _Istr.rdbuf()->snextc()) if (_Traits::eq_int_type(_Traits::eof(), _Meta)) { // end of file, quit _State |= ios_base::eofbit; break; } else if (_Traits::eq_int_type(_Meta, _Metadelim)) { // got a delimiter, discard it and quit _Changed = true; _Istr.rdbuf()->sbumpc(); break; } else if (_Str.max_size() <= _Str.size()) { // string too large, quit _State |= ios_base::failbit; break; } else { // got a character, add it to string _Str += _Traits::to_char_type(_Meta); _Changed = true; } _CATCH_IO_(_Istr) } if (!_Changed) _State |= ios_base::failbit; _Istr.setstate(_State); return (_Istr); }
Une implémentation que je qualifierais d’assez complexe avec un gestionnaire d’exceptions, une belle boucle, des ‘if’ et des appels de fonctions. Il y a également une chaîne de caractères (std::string) dans laquelle on ajoute les caractères un à un : réallocations de mémoire cachée… Fgets a une implémentation plus simple qui travaille sur un buffer pré-alloué : moins d’allocations.
2. Appel de fonction
Chaque ‘<<’ est un appel de fonction qui finit par aboutir à :
template<class _Elem, class _Traits, class _SizeT> inline basic_ostream<_Elem, _Traits>& _Insert_string( basic_ostream<_Elem, _Traits>& _Ostr, const _Elem * const _Data, const _SizeT _Size) { // insert a character-type sequence into _Ostr as if through a basic_string copy ios_base::iostate _State = ios_base::goodbit; _SizeT _Pad = _Ostr.width() <= 0 || static_cast<_SizeT>(_Ostr.width()) <= _Size ? 0 : static_cast<_SizeT>(_Ostr.width()) - _Size; const typename basic_ostream<_Elem, _Traits>::sentry _Ok(_Ostr); if (!_Ok) _State |= ios_base::badbit; else { // state okay, insert characters _TRY_IO_BEGIN if ((_Ostr.flags() & ios_base::adjustfield) != ios_base::left) for (; 0 < _Pad; --_Pad) // pad on left if (_Traits::eq_int_type(_Traits::eof(), _Ostr.rdbuf()->sputc(_Ostr.fill()))) { // insertion failed, quit _State |= ios_base::badbit; break; } if (_State == ios_base::goodbit && _Ostr.rdbuf()->sputn(_Data, (streamsize)_Size) != (streamsize)_Size) _State |= ios_base::badbit; else for (; 0 < _Pad; --_Pad) // pad on right if (_Traits::eq_int_type(_Traits::eof(), _Ostr.rdbuf()->sputc(_Ostr.fill()))) { // insertion failed, quit _State |= ios_base::badbit; break; } _Ostr.width(0); _CATCH_IO_(_Ostr) } _Ostr.setstate(_State); return (_Ostr); }
Je pense que nous sommes d’accord : il y a des codes plus simples ! Mais en surchargeant la fonction ‘<<’ pour un nouveau type, on peut facilement l’écrire dans un stream.
IV. Conclusion sur les API
Si votre objectif est de lire ou d’écrire dans un fichier en ayant des performances maximales, évitez alors les streams STL et les std::string. Il vaut mieux privilégier l’API C couplée à des buffers statiques. J’ai bien conscience que c’est un coup de canif dans le contrat C++/Programmation orientée objet/STL, mais utiliser un outil au motif qu’il est facile d’usage sans vraiment comprendre ce qui se cache derrière est une source de risque.
« Lorsque le seul outil que l’on ait sous la main est un marteau, alors tous les problèmes finissent par ressembler à des clous » : soyez curieux, testez et comparez ! Ne prenez pas les choses pour acquises !