Datei in eine std::map einlesen und nach Value sortieren



  • Hallo Leute,

    ich habe eine große Datei (min. 100MB), bestehend aus vielen Zeilen. Jede Zeile hat den Aufbau Name Value , wobei Name vom Typ std::string und Value vom Typ unsigned ist. Ein Name kommt beliebig oft vor.

    Beispiel:

    Hans 6
    Max 3
    John 9
    Max 4
    Hans 7
    Hans 5
    

    Was ich will:
    1. Die zu jedem Namen gehörigen Werte aufaddieren.
    2. Das Ganze absteigend nach den Werten sortieren.
    ➡ Letztendlich will ich alle Summen absteigend mit dazugehörigen Namen ausgeben:

    18 Hans
    7 Max
    9 John
    

    Dies soll so effizient wie nur möglich geschehen. Schön wäre es, wenn beides zusammmen nicht länger als 1 Sekunde dauert. Ich habe dazu einen Versuch gemacht, indem ich alles in einer std::map unterbringe und anschließend die Keys und Values aller std::pair s vertausche und sie in einer std::multimap unterbringe:

    #include <algorithm>
    #include <ctime>
    #include <fstream>
    #include <functional>
    #include <iterator>
    #include <map>
    #include <string>
    #include <utility>
    using namespace std;
    
    void create_file(unsigned size) // Erzeugt einfach nur eine 100 MB große Beispiel-Textdatei.
    {
    	ofstream file( "test.txt" );
    	char key = 'A';
    	for(unsigned i=0; i<size; ++i,++key)
    	{
    		key = ( key <= 'Z' ? key : 'A' );
    		file << key << ' ' << rand()%10 << '\n';
    	}
    }
    
    void read_file(map<string,unsigned>& content) // Liest den Dateiinhalt in eine std::map ein, wobei alle Werte aufaddiert werden.
    {
    	ifstream file( "test.txt" );
    	string   key;
    	unsigned value;
    	while( file>>key>>value )
    	{
    		content[key] += value;
    	}
    }
    
    pair<unsigned,string> flip_pair(const pair<string,unsigned>& p) // Argument für std::transform.
    {
        return pair<unsigned,string>( p.second, p.first );
    }
    void flip_map(const map<string,unsigned>& src, multimap<unsigned,string,greater<unsigned>>& des) // Für jeden std::map Eintrag: Tausche Key mit Value aus.
    {
        transform ( src.begin(), src.end(), inserter(des,des.begin()), flip_pair );
    }
    
    int main()
    {
    	srand( time(NULL) );
    	create_file( 20 * 1024 * 1024 ); // Eine Zeile hat 5 Zeichen (Windows) ---> 100 MB
    
    	// Zeitmessung start
    	map<string,unsigned> content;
    	read_file( content );
    	// Zeitmessung stop: 12,5 Sekunden ---> flop
    
    	// Zeitmessung start
    	multimap<unsigned,string,greater<unsigned>> sorted_by_value;
    	flip_map( content, sorted_by_value );
    	// Zeitmessung stop: 28 us ---> top
    }
    

    Zu 1: Wie ihr seht, dauert das Einlesen mit 12,5 Sekunden viel zu lange. Habt ihr eine Idee, wie ich das viel effizienter machen kann? Mir fällt dazu irgendwie nichts ein, außer vielleicht den kompletten Inhalt mit istream::read in einen std::string lesen und diesen dann parsen 😕

    Zu 2: Das Sortieren nach Values geht mit 28 Mikrosekunden schon ziemlich flott. Hier wäre eigentlich nur noch die Frage, ob es sich eleganter lösen ließe? Wie würdet ihr Keys mit Values vertauschen und anschließend nach Values sortieren? Oder gibt es schon einen Container, der automatisch nach Values sortiert und einen schnellen Zugriff auf einen Value via Key bietet?

    Danke für eure Antworten. 🙂



  • Hiho,

    ich glaube kaum, dass du das Einlesen einer 100MB-Datei von der Festplatte in 1 Sekunde schaffst, außer du nutzt eine SSD, da habe ich jetzt die Transferraten nicht im Kopf. Aber bei einer normalen Festplatte setzt dir da einfach die Hardware ein technisches Limit.



  • Die C++-Streams sind leider unglaublich langsam. Wenn du also Geschwindigkeit haben willst, musst du C-Funktionen benutzen. Eine Änderung zu:

    void read_file(map<string,unsigned>& content) // Liest den Dateiinhalt in eine std::map ein, wobei alle Werte aufaddiert werden.
    {
    	const size_t BSZ = 1024*1024*10; // or more
    	char text[80];   // Hast du eine garantierte maximale Zeilenlänge?
    	int n1;
    	FILE* fp = fopen("test.txt", "r");
    	if(fp)
    	{
    		//setvbuf(fp,0,_IOFBF,BSZ); // No need to free buffers explicitly
    		while( fgets(text, sizeof(text), fp) )
    		{
    			char* p = strtok(text," ");
    			p = strtok(NULL, " ");
    			n1 = atol(p);
    			content[text] = n1;
    		}
    	}
    	fclose(fp);
    }
    

    Brachte bei mir eine Beschleunigung von 32 Sekunden auf 9 Sekunden und immer noch CPU-limitiert (schwache CPU, SSD). Der Code ist übrigens schnell über Google zusammengestellt. Lass also einen mit Ahnung von C nochmal drüberschauen.

    So eine dramatische Beschleunigung wirst du zwar nicht beobachten können, weil irgendwann tatsächlich die Festplatte nicht mehr schneller kann, aber im Moment bist du bei 8 MB/s. Da hat die Festplatte noch hinreichend Luft. Wenn der ausführende CPU-Kern nicht mehr voll ausgelastet ist, kannst du aufhören zu optimieren.



  • out schrieb:

    1. Die zu jedem Namen gehörigen Werte aufaddieren.
    2. Das Ganze absteigend nach den Werten sortieren.

    Warum liest du dann nicht sofort in eine map? 😕



  • out schrieb:

    Ich habe dazu einen Versuch gemacht, indem ich alles in einer std::map unterbringe und anschließend die Keys und Values aller std::pair s vertausche und sie in einer std::multimap unterbringe

    Würde ich nicht so machen. Um die Werte aufzunehmen bietet sich eine std::unordered_map (aka hashtable) an, gibt es seit C++ und ist um einiges schneller.

    Sortieren würde ich anders. Das in einem Baum zu tun ist relativ ineffizient.
    Ich hätte am Schluss alle Werte in einen std::vector<std::pair<int, std::string*> > reinkopiert und den sortiert.



  • Michael E. schrieb:

    Brachte bei mir eine Beschleunigung von 32 Sekunden auf 9 Sekunden und immer noch CPU-limitiert (schwache CPU, SSD).

    Damit ist es schon viel schneller. Von 12,5 auf 4,7 Sekunden. 🙂

    cooky451 schrieb:

    out schrieb:

    1. Die zu jedem Namen gehörigen Werte aufaddieren.
    2. Das Ganze absteigend nach den Werten sortieren.

    Warum liest du dann nicht sofort in eine map? 😕

    Ich kann dir nicht ganz folgen, cooky451. Was meinst du genau?

    Habe noch eine Weile getüftelt. Dieses mal lese ich alles via istream::read in einen std::string , den ich anschließend parse. istream::read ist unglaublich schnell, jedoch ist das parsen nicht so schnell. Dennoch benötigt beides zusammen nur noch 3,5 Sekunden:

    #include <algorithm>
    #include <ctime>
    #include <fstream>
    #include <functional>
    #include <iterator>
    #include <map>
    #include <string>
    #include <utility>
    using namespace std;
    
    void create_file( unsigned size )
    {
    	ofstream file( "test.txt" );
    	char key = 'A';
    	for(unsigned i=0; i<size; ++i,++key)
    	{
    		key = ( key <= 'Z' ? key : 'A' );
    		file << key << ' ' << rand()%10 << '\n';
    	}
    }
    
    void read_file( string& content )
    {
    	ifstream file( "test.txt" );
    
    	file.seekg(0, ios_base::end);
    	const unsigned size = file.tellg();
    	file.seekg(0, ios_base::beg);
    
    	content.resize( size );
    	file.read( &content[0], size );
    }
    
    void evaluate_file( string& content, map<string,unsigned>& evaluation )
    {
    	replace( content.begin(),content.end(), '\n', ' ' ); 
    
    	string::size_type start = content.find_first_not_of( ' ', 0 );
    	string::size_type stop  = content.find( ' ' );
    
    	while( string::npos!=start )
    	{
    		const string& key = content.substr(start,stop-start);
    
    		start = content.find_first_not_of( ' ', stop  );
    		stop  = content.find             ( ' ', start );
    
    		if( string::npos==start )
    			break;
    
    		evaluation[key] += stoi( content.substr(start,stop-start) );
    
    		start = content.find_first_not_of( ' ', stop  );
    		stop  = content.find             ( ' ', start );
    	}
    }
    
    pair<unsigned,string> flip_pair(const pair<string,unsigned>& p)
    {
        return pair<unsigned,string>( p.second, p.first );
    }
    void flip_map(const map<string,unsigned>& src, multimap<unsigned,string,greater<unsigned>>& des)
    {
        transform ( src.begin(), src.end(), inserter(des,des.begin()), flip_pair );
    }
    
    int main()
    {
    	srand( time(NULL) );
    	create_file( 20 * 1024 * 1024 ); // Eine Zeile hat 5 Zeichen (Windows) ---> 100 MB
    
    	string content;
    	read_file( content ); // Einlesen: 0,2 Sekunden
    
    	map<string,unsigned> evaluation;
    	evaluate_file( content, evaluation ); // Parsen: 3,3 Sekunden
    
    	multimap<unsigned,string,greater<unsigned>> sorted_by_value; // Nach Value sortieren: 28 us
        flip_map( evaluation, sorted_by_value );
    }
    

    engoaona schrieb:

    Würde ich nicht so machen. Um die Werte aufzunehmen bietet sich eine std::unordered_map (aka hashtable) an, gibt es seit C++ und ist um einiges schneller.
    Sortieren würde ich anders. Das in einem Baum zu tun ist relativ ineffizient.
    Ich hätte am Schluss alle Werte in einen std::vector<std::pair<int, std::string*> > reinkopiert und den sortiert.

    Dasgehe ich jetzt mal noch an. 🙂

    Der Flaschenhals scheint nun das Parsen zu sein, falls also jemand eine Idee hat, wie man den std::string

    Hans 6\nMax 3\nJohn 9\nMax 4\nHans 7\nHans 5\n

    effizient in std::pair<string,unsigned> zerlegen kann, immer raus damit. 🙂 Ich habe es einfach so gemacht, dass ich '\n' durch ein Leerzeichen ersetze und mich dann von Leerzeichen zu Leerzeichen durchkämpf.



  • Ich bin gerade nicht mehr ganz im Bilde, aber das Problem an den C++-Streams ist doch nicht, dass sie per se so langsam sind, sondern dass die normalen get/getline-Methoden jedes Mal ein neues sentry-Objekt erstellen und wieder freigeben.

    Wenn man es schneller machen möchte, kann man sich eigene Leseoperatoren schreiben, die eben für die gesamte Leseoperation ein Objekt anlegen. Und natürlich sollte das alles auch im Releasemodus geschehen. Irgendwann ist dann aber tatsächlich das Problem, dass die Festplatte und nicht das Parsen der Flaschenhals ist.

    Ein Beispiel dafür findet sich z.B. hier in Werners letztem Post: http://www.c-plusplus.net/forum/308667-30

    Ob streambuf::sbumpc() schneller ist als fgets von C weiß ich jedoch nicht. Man könnte damit zumindest Lesevorgang und Speichervorgang in die map-Struktur vereinigen. Und immer brav im Release-Modus ausführen...



  • std::string ist imo ein ziemlich ungeeigneter Datentyp fuers Parsen. Probiers mal mit eine ref_string Klasse, damit sparst du dir Kopien.



  • engoaona schrieb:

    Würde ich nicht so machen. Um die Werte aufzunehmen bietet sich eine std::unordered_map (aka hashtable) an, gibt es seit C++ und ist um einiges schneller.
    Sortieren würde ich anders. Das in einem Baum zu tun ist relativ ineffizient.
    Ich hätte am Schluss alle Werte in einen std::vector<std::pair<int, std::string*> > reinkopiert und den sortiert.

    Danke, die beiden Tipps waren echt gut. Ich die Zeit dadurch noch von 3,5 auf 2,8 Sekunden im Schnitt drücken. 🙂
    Hier nochmal die Version, für alle die, die es interessiert:

    #include <algorithm>
    #include <ctime>
    #include <fstream>
    #include <functional>
    #include <iterator>
    #include <unordered_map>
    #include <string>
    #include <utility>
    using namespace std;
    
    void create_file( unsigned size )
    {
    	ofstream file( "test.txt" );
    	char key = 'A';
    	for(unsigned i=0; i<size; ++i,++key)
    	{
    		key = ( key <= 'Z' ? key : 'A' );
    		file << key << ' ' << rand()%10 << '\n';
    	}
    }
    
    void read_file( string& content )
    {
    	ifstream file( "test.txt" );
    
    	file.seekg(0, ios_base::end);
    	const unsigned size = file.tellg();
    	file.seekg(0, ios_base::beg);
    
    	content.resize( size );
    	file.read( &content[0], size );
    }
    
    void evaluate_file( string& content, unordered_map<string,unsigned>& evaluation )
    {
    	replace( content.begin(),content.end(), '\n', ' ' ); 
    
    	string::size_type start = content.find_first_not_of( ' ', 0 );
    	string::size_type stop  = content.find( ' ' );
    
    	while( string::npos!=start )
    	{
    		const string& key = content.substr(start,stop-start);
    
    		start = content.find_first_not_of( ' ', stop  );
    		stop  = content.find             ( ' ', start );
    
    		if( string::npos==start )
    			break;
    
    		evaluation[key] += stoi( content.substr(start,stop-start) );
    
    		start = content.find_first_not_of( ' ', stop  );
    		stop  = content.find             ( ' ', start );
    	}
    }
    
    void sort_by_value( const unordered_map<string,unsigned>& evaluation, vector<pair<const string*,unsigned>>& evaluation_sorted )
    {
    	for( auto it=begin(evaluation); it!=end(evaluation); ++it )
    	{
    		evaluation_sorted.push_back( make_pair( &(it->first), it->second ) );
    	}
    
    	sort( begin(evaluation_sorted), end(evaluation_sorted), 
    
    		[]( const pair<const string*,unsigned>& lhs,  const pair<const string*,unsigned>& rhs )
    		{
    			return lhs.second>rhs.second;
    		}
    	);
    }
    
    int main()
    {
    	srand( time(NULL) );
    	create_file( 20 * 1024 * 1024 ); // Eine Zeile hat 5 Zeichen (Windows) ---> 100 MB
    
    	string content;
    	read_file( content ); // Einlesen: 0,18 Sekunden
    
    	unordered_map<string,unsigned> evaluation;
    	evaluate_file( content, evaluation ); // Parsen: 2,6 Sekunden 
    
    	vector<pair<const string*,unsigned>> evaluation_sorted;
    	sort_by_value( evaluation, evaluation_sorted ); // Nach Value sortieren: 16 us 
    }
    

    Nebenbei noch:
    Compiler: MSVC 2012
    Optimierung: /O2
    BS: Win7 64-bit
    CPU: i5-2500
    RAM: 8GB

    Von 12,5 auf 2,8 Sekunden, ich glaub schneller ist es nicht mehr machbar. Und gerade sehe ich, dass ich mir angewöhnen sollte, alle C++11 Features zu nutzen, wenn ich schon eines nutz 🙄 .



  • Kellerautomat schrieb:

    std::string ist imo ein ziemlich ungeeigneter Datentyp fuers Parsen. Probiers mal mit eine ref_string Klasse, damit sparst du dir Kopien.

    Gute Idee, gleich mal teste. 🙂



  • out schrieb:

    cooky451 schrieb:

    out schrieb:

    1. Die zu jedem Namen gehörigen Werte aufaddieren.
    2. Das Ganze absteigend nach den Werten sortieren.

    Warum liest du dann nicht sofort in eine map? 😕

    Ich kann dir nicht ganz folgen, cooky451. Was meinst du genau?

    Du brauchst doch offensichtlich keine Zuordnung Name->Nummer, sondern nur Nummer->Name. Wenn du das einfach in eine (multi)map<unsigned, std::string> lesen würdest, ist alles sofort sortiert so wie du es haben willst und du musst nicht sinnloserweise irgendwelche Paare switchen. Irgendwas muss ich falsch verstanden haben, denn das macht so genau 0 Sinn. 😕



  • cooky451: Die Punkte eines Namens muss man aufsummieren.



  • cooky451 schrieb:

    out schrieb:

    cooky451 schrieb:

    out schrieb:

    1. Die zu jedem Namen gehörigen Werte aufaddieren.
    2. Das Ganze absteigend nach den Werten sortieren.

    Warum liest du dann nicht sofort in eine map? 😕

    Ich kann dir nicht ganz folgen, cooky451. Was meinst du genau?

    Du brauchst doch offensichtlich keine Zuordnung Name->Nummer, sondern nur Nummer->Name. Wenn du das einfach in eine (multi)map<unsigned, std::string> lesen würdest, ist alles sofort sortiert so wie du es haben willst und du musst nicht sinnloserweise irgendwelche Paare switchen. Irgendwas muss ich falsch verstanden haben, denn das macht so genau 0 Sinn. 😕

    ALle Nummer, die zu einem Namen gehören, muss ich aufaddieren. Danach muss ich alle gewonnenen Summen absteigend sortieren. Wie würdest du dann die Nummern aufaddieren?



  • Hallo out,

    auch wenn ich folgenden Code niemanden empfehlen würde, so ersetze doch damit bitte Deine read_file -Funktion und poste uns die Ergebnisse Deiner Zeitmessung.

    #include <fstream>
    #include <iostream>
    #include <map>
    #include <string>
    #include <locale>
    
    namespace special
    {
        struct map_reader
        {
            map_reader( std::map< std::string, unsigned >& destination )
                : destination_( &destination )
            {}
            std::map< std::string, unsigned >* destination_;
        };
    
        template< typename E, typename Traits >
        std::basic_istream< E, Traits >& operator>>( std::basic_istream< E, Traits >& in, map_reader mr )
        {
            std::basic_istream< E, Traits >::sentry ok( in );
            if( ok )
            {
                std::ios_base::iostate state = std::ios_base::goodbit;
                try
                {
                    const std::ctype< E >& ctype_ = std::use_facet< std::ctype< E > >( in.getloc() );
                    for(;;)
                    {
                        // --   skip leading whitespace
                        Traits::int_type m = in.rdbuf()->sgetc();
                        for( ;; m = in.rdbuf()->snextc() )
                        {
                            if( Traits::eq_int_type( m, Traits::eof() ) )
                            {
                                state |= std::ios_base::eofbit;
                                break;
                            }
                            else if( !ctype_.is( std::ctype_base::space, Traits::to_char_type(m) ) )
                                break;    // kein whitespace
                        }
                        if( state != std::ios_base::goodbit )
                            break; // EOF
    
                        // --   read text
                        std::basic_string< E > txt(1,Traits::to_char_type(m));
                        for( m = in.rdbuf()->snextc();; m = in.rdbuf()->snextc() )
                        {
                            if( Traits::eq_int_type( m, Traits::eof() ) )
                            {
                                state |= std::ios_base::eofbit | std::ios_base::failbit;
                                break;
                            }
                            const E c = Traits::to_char_type(m);
                            if( ctype_.is( std::ctype_base::space, c ) )
                                break;
                            txt.append( 1, c );
                        }
                        if( state != std::ios_base::goodbit )
                            break; // fail
    
                        // --   skip leading whitespace before number
                        for( m = in.rdbuf()->snextc();; m = in.rdbuf()->snextc() )
                        {
                            if( Traits::eq_int_type( m, Traits::eof() ) )
                            {
                                state |= std::ios_base::eofbit | std::ios_base::failbit;
                                break;
                            }
                            else if( !ctype_.is( std::ctype_base::space, Traits::to_char_type(m) ) )
                                break;    // kein whitespace
                        }
                        if( state != std::ios_base::goodbit )
                            break; // fail
    
                        // --   read number
                        unsigned val = 0;
                        bool read_some = false;
                        for( ;; m = in.rdbuf()->snextc(), read_some = true )
                        {
                            if( Traits::eq_int_type( m, Traits::eof() ) )
                            {
                                state |= std::ios_base::eofbit;
                                break;
                            }
                            const E c = Traits::to_char_type( m );
                            if( !ctype_.is( std::ctype_base::digit, c ) )
                                break;
                            (val *= 10) += int(c - '0');
                        }
                        if( !read_some )
                        {
                            state |= std::ios_base::failbit;
                            break;
                        }
                        mr.destination_->operator[]( txt ) += val; // <== hier wird der Wert addiert
                    }
                }
                catch( ... )
                {
                    state |= std::ios_base::badbit;
                    if( in.exceptions() & std::ios_base::badbit )
                        throw;
                }
                in.setstate( state );
            }
            return in;
        }
    }
    
    void read_file( std::map<std::string,unsigned>& content )
    {
        using namespace std;
        ifstream file( "test.txt" );
        if( !(file >> special::map_reader( content )) )
            cerr << "Fehler beim Lesen von 'content'" << endl;
    }
    

    auf meinem PC schlägt er die Variante von Michael um den Faktor 2.

    Gruß
    Werner



  • Hallo Werner 🙂

    Meine beste Version, welche ich zuletzt gepostet habe, benötigt 2,8 Sekunden. Verwende ich deine read_file -Funktion, dauert er nur noch 1,39 Sekunden 🙂 . Und wenn ich bei dir jetzt noch std::map durch std::unordered_map ersetze, bin ich bei unglaublichen 1,12 Sekunden :). Danke für die Hilfe, die Zeit ist echt der Hammer, auch wenn ich den Quellcode nicht ganz verstehe. Allerdings möchte ich noch Nachfrage, wieso du diesen Code niemandem empfehlen würdest?



  • Was soll ich sagen, Werner ist einfach ein Gott mit den C++-Streams.

    Dann muss ich meine Aussage wohl korrigieren: C++-Streams sind unglaublich langsam, wenn man sie so naiv wie ich verwendet 😉



  • Dachte ich mir's doch. 🙂

    Habe mir den Code wieder abgelegt. Diesmal habe ich ctype dazugelernt, das ist ja auch ein wirklich praktisches Ding. V.a. wenn man zeichenweise Hexadezimal- oder Fließkommazahlen lesen möchte, bietet es sich wohl auch an.



  • Hallo alle,

    out schrieb:

    Allerdings möchte ich noch nachfragen, wieso du diesen Code niemandem empfehlen würdest?

    Geschwindigkeit ist nicht alles. Die erste Variante von out - ganz klassisch:

    //
        string key;
        unsigned val;
        while( file >> key >> val ) { // usw.
    

    .. kann jeder mit ein wenig Erfahrung in der Anwendung von istream in Minuten hinschreiben. Dafür erhält man einen funktionsfähigen, einfachen, sicheren und ausbaufähigen Programmcode. So wäre eine Änderung des Typs von 'val' oder ein anderes Zahlenformat, wenn es z.B. Fließkommazahlen wären, kein Problem.

    Die Variante von Michael war dann schon anders. Zunächst springt einem der Kommentar ins Auge:

    char text[80];   // Hast du eine garantierte maximale Zeilenlänge?
    

    Aha - was ist, wenn die Zeilen länger werden? Vielleicht nicht in dieser und der nächsten Version, aber dann irgendwann?
    Anschließend bekomme ich beim Compilieren drei Sicherheitswarnungen um die Ohren gehauen. Ok - ist ja nur zum Test. Und dann packe ich die Datei 'test.txt' in ein falsches Directorie und .. crash! Nach 5 Minuten debuggen wird klar: fclose(fp) hätte in den Scope if(fp){.. gehört. fp darf nicht =0 sein, wenn fclose gerufen wird. Mit dem RAII der streams passiert sowas einfach nicht.

    Meine Variante sollte sicher sein, aber das Risiko, dass dort noch ein Fehler in der Fehlerbehandlung ist, ist hoch. Die Variante ist ausgelutscht - ein paar Beispiele:

    - Zeile 56:

    txt.append( 1, c );
    

    .. warum nicht ein txt.push_back(c) ? Nun, weil dass in der von mir verwendeten Dinkumware C++-Lib signifikant langsamer ist. Das muss überhaupt nicht so sein, dass ist einfach so.

    - Zeile 45

    // --   read text
                        std::basic_string< E > txt(1,Traits::to_char_type(m));
                        for( m = in.rdbuf()->snextc();; m = in.rdbuf()->snextc() )
    

    müsste sauber heißen:

    // --   read text
                        std::basic_string< E > txt;
                        for( m = in.rdbuf()->sgetc();; m = in.rdbuf()->snextc() )
    

    hier mache ich mir zunutze, dass im Vorfeld bereits festgestellt wurde, dass m weder EOF noach ein White-Space-Character ist. Man kann es also direkt in den txt übernehmen. Aber was, wenn der Code noch mal überarbeitet wird? Ist das dann auch noch klar?

    - Zeile 62

    // --   skip leading whitespace before number
                        for( m = in.rdbuf()->snextc();; m = in.rdbuf()->snextc() )
    

    gleiches Spiel wie oben. Ich überspringe frech ein Zeichen, da ich hier noch weis, es gehört nicht dazu.

    Um es kurz zu machen, kein 'schöner' Code, den man gern bearbeitet. Und er ist sehr speziell auf das Bedürfnis von out zugeschnitten. So z.B. das Einlesen des unsigned- Wertes. Das ist zum Glück das einfachste Zahlenformat, was man sich vorstellen kann. Einen möglichen Overflow habe ich hier mal großzügig ignoriert.

    Aber wo geht jetzt die Zeit von der klassischen Variante 1 hin?
    Wie Eisflamme schon erwähnte, wird mit jedem Streaming-Operator-Aufruf ein sentry-Objekt erstellt, das wäre jetzt nicht so schlimm, aber das sentry holt sich mit use_facet<>() die ctype-Facette. Wobei hier eine Kopie des std::locale-Objekts erzeugt wird (Methode std::ios_base::getloc()). Was auch keine großen Datenmengen bewegt, denn es funktioniert in meiner C++-Lib via Referenz-Counting, was aber Multi-Thread-Sicher implementiert ist - und das kostet!
    Im eigentlichen Lesecode wird dann nochmal use_facet aufgerufen - mit std::ctype<> beim string lesen und mit std::num_get<> beim Zahl lesen. D.h. man kommt pro Datensatz auf 4 use_facet-Aufrufe.
    Erschwerend kommt hier hinzu, dass die von out erzeugt Datei den GAU für das Streaming darstellt, da alle Werte nur aus einem einzigen Zeichen bestehen. Das obige betrifft ja immer den Overhead pro Wert, und der macht sich umso mehr bemerkbar, umso weniger Zeichen pro Wert die Datei füllen. Oder anders ausgedrückt - eine Datei gleicher Größe mit mehr Zeichen pro Wert, hätte weniger Werte und damit auch weniger use_facet-Aufrufe.
    Ja und dann ist da noch noch die num_get-Implementierung der Dinkumware-C++-Lib. C++ sei Dank ist ja der genaue Zahlentyp, der da eingelesen wird, schon seit der Compilezeit bekannt - und tief unten in den Niederungen von num_get wird ein scanf-Format-String zusammen gebaut und mit diesem ein sscanf gerufen. Da braucht man sich wirklich nicht wundern, wenn das Gesamtpaket langsamer ist als die C-Version.

    Und so kommt einiges zusammen, was das Streamen 'langsam' macht.
    Btw.: Ich war bisher der Meinung, dass das Einlesen von 1MB Textdatei in unter einer Sekunde flott genug ist. Ich habe schon C-Code gesehen, der dafür eine ganze Minute gebraucht hat. Ergo sind 12,5s für 100MB doch eine stolze Leistung - oder nicht?

    Michael E. schrieb:

    Dann muss ich meine Aussage wohl korrigieren: C++-Streams sind unglaublich langsam, wenn man sie so naiv wie ich verwendet 😉

    .. sooo langsam nun auch wieder nicht. Und nicht naiv, sondern einfach, sicher und ausbaufähig.

    Gruß
    Werner

    ... und das war mein 2000.Beitrag als registrierter User 🕶



  • Kellerautomat schrieb:

    std::string ist imo ein ziemlich ungeeigneter Datentyp fuers Parsen. Probiers mal mit eine ref_string Klasse, damit sparst du dir Kopien.

    👍 Das ist so alles viel zu langsam, weil man dauernd strings kopiert.

    Ich behaupte mal das ganze geht mit einem eigenen Parser und ~4MB großen Cache locker in 1/10 Sekunde. Wenn nicht noch schneller. Ich habe leider keine Zeit das gerade zu bauen, aber nur so als Idee, wenn du wirklich Performance willst.

    Siehe auch http://www.c-plusplus.net/forum/294002-10, nicht besonders schön, aber du siehst die Grundidee.



  • cooky:
    Wieso ist denn die Stream-Variante langsamer? Ich finde nicht, dass man den Ansatz in dem von Dir verlinkten Post sieht. Du benutzt Schleifen und dann mystische Funktionen wie parseFloat. Hier iterieren wir ein einziges Mal über die ganze Datei. Was imo halt langsam sein könnte ist das Hinzufügen von Zeichen zu einem string, wie hast Du das denn gelöst? Oder an welcher Stelle ist Deine Variante optimierter?


Anmelden zum Antworten