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 !