Puffergröße beim Lesen einer Datei



  • Hallo @all,

    ich habe folgende Frage:
    Wenn ich die Zeichen byteweise aus einer Datei verarbeiten möchte (z.B. für einen Verschlüsselungsalgorithmus) und zum Lesen std::fstream nutze, stellt sich die Frage, welche Puffergröße man nehmen sollte bzw. welche Größe am Optimalsten wäre?

    Aus meiner Sicht gibt es 3 Möglichkeiten:
    1. Zeichen für Zeichen lesen. ( Schätze mal, dass das weniger optimal ist)
    2. Puffergröße an Blockgröße der Festplatte anpassen, also z.B. bei na normalen Festplatte mit OS=Windows -> ntfs 4096 Bytes pro Zugriff lesen ( wäre meine Wahl)
    3. Alle Zeichen auf einmal lesen(3. Option eher utopisch, Große Dateien wahrscheinlich nicht in RAM ladbar)

    Aber vllt gibt es ja dafür schon eine gute Lösung, hoffe ihr könnt mir weiterhelfen 🙂

    thx
    und
    mfG

    Hlymur


  • Mod

    und zum Lesen std::fstream nutze

    Warum nicht std::ifstream ?

    Und dann kannst du den Buffer mit stream.rdbuf()->pubsetbuf( buffer, länge ) setzen.
    Allerdings ist das Verhalten das dann daraus resultiert nicht direkt vom Standard vorgeschrieben. Hier wird für GCC, Clang und VC++ aufgelistet, wie sich setbuf verhält - bei GCC werden bspw. immer länge - 1 Zeichen auf einmal aus der Datei gelesen.

    Zeichen für Zeichen lesen scheidet sofort aus. Ich würde auch auf die Buffergröße der Festplatte setzen.



  • 4. Die Datei in den virtuellen Speicher mappen. Das geht portabel zum Beispiel mit Boost.

    Hast du einfach mal Varianten ausprobiert und gemessen?



  • //...
    std::ifstream inFile("file.txt",std::ios::binary);
    
    char* puffer = new char[4096];
    
    inFile.rdbuf( puffer, 4096 );
    
    while(!inFile.eof())
    {
        for( int i=0; i<4096; i++ )
        {
            char c;
            inFile >> c;
            //Mache iwas mit c
        }
    }
    //...
    

    Der Code würde quasi den Lesepuffer auf 4096 setzen und und immer nach diesen 4096 Zeichen erst wieder auf die Platte zugreifen?



  • Hlymur schrieb:

    Der Code würde quasi den Lesepuffer auf 4096 setzen und und immer nach diesen 4096 Zeichen erst wieder auf die Platte zugreifen?

    Nein.
    Der Kernel wird darauf spekulieren, dass die Datei sequentiell gelesen wird und sie im Hintergrund in Kernel-Speicher einlesen, solange RAM frei ist und die Festplatte Zeit hat. Du fragst einzelne Zeichen mit ifstream aus dem puffer ab, was nicht gerade kostenlos ist. operator>> enthält viel könnte/müsste/hätte-Logik, die du nicht brauchst. Wenn der puffer leer ist, kopiert ifstream den nächsten Block aus dem Kernel-Speicher in den puffer .
    Vergiss die Festplatte. Du bist nicht schlauer als heutige Betriebssysteme. Du kannst aber unnötige Kopien vermeiden. Wenn du sowieso alle Bytes nacheinander verarbeiten musst, lies entweder mit read in einen Puffer oder mappe die Datei in den Speicher. Die beste Puffergröße ist wahrscheinlich auch nicht 4096, sondern eher ab 16k. Das hängt aber von der Hardware ab. Wenn der Puffer zu groß für die kleinsten CPU-Caches ist, wird es wieder langsamer.
    Bedenke auch, dass der Kernel bereits benutzte Dateien im RAM vorhält. So eine Datei zu lesen kann 10 oder 100 mal schneller sein als von der Festplatte.



  • Hlymur schrieb:

    ... welche Größe am Optimalsten wäre?

    Aus meiner Sicht gibt es 3 Möglichkeiten:
    1. Zeichen für Zeichen lesen. ( Schätze mal, dass das weniger optimal ist)

    Richtig: Das dürfte am Pessimalsten sein, um es mal in Deiner Terminologie auszudrücken 😉



  • @Hlymur

    > 1. Zeichen für Zeichen lesen.
    Ganz schlecht. Viel zu viel Overhead pro Zeichen. fstream puffert zwar selbst, aber es muss trotzdem viel zu viel Code pro Zeichen ausgeführt werden, auch wenn nicht jedes Zeichen einzeln vom OS angefordert wird.

    > 2. Puffergröße an Blockgröße der Festplatte anpassen, also z.B. bei na normalen Festplatte mit OS=Windows -> ntfs 4096 Bytes pro Zugriff lesen ( wäre meine Wahl)
    4096 ist schonmal viel besser als zeichenweise lesen. Dass es mit der Clustergrösse des Filesystems zusammenstimmt wird sich aber nicht bis so-gut-wie-nicht auswirken.

    > 3. Alle Zeichen auf einmal lesen(3. Option eher utopisch, Große Dateien wahrscheinlich nicht in RAM ladbar)
    Wie du selbst schon schreibst doof bei wirklich grossen Files. Ansonsten eine gute Möglichkeit. Fast nie optimal schnell, aber meist sehr nah am Optimum.

    ----------

    2.1. Puffergrösse durch ausprobieren ermitteln.
    Es gibt praktisch immer einen "Sweet-Spot" irgendwo in der Mitte der möglichen Puffergrössen.

    Wenn der Puffer zu klein ist muss man zu viele Kernel-Calls machen, und das OS "versteht" u.U. nicht dass es wesentlich mehr als die pro Zugriff gelesene Datenmenge "vorlesen" sollte (such nach "read ahead optimization" wenn dich das Thema interessiert).

    Und wenn der Puffer zu gross ist, weigert sich das OS u.U. so viel "vorzulesen", und es kommt wieder zu unnötigen Verzögerungen. Weiters wird der Cache bei zu grossen Puffern nicht optimal genutzt.

    Ich würde mal darauf tippen dass der "Sweet-Spot" normalerweise irgendwo zwischen 10 K und 1 MB zu finden ist.
    Wenn ich keine Zeit hätte es experimentell zu ermitteln würde ich vermutlich einfach 64 KB nehmen.

    ps: Bei älteren Systemen, z.B. Windows 95, waren grössere Puffer (>= 1 MB) besser. Vielleicht weil Windows 95 keine Read-Ahead Optimierung hatte - keine Ahnung.



  • Hlymur schrieb:

    //...
    std::ifstream inFile("file.txt",std::ios::binary);
    
    char* puffer = new char[4096];
    
    inFile.rdbuf( puffer, 4096 );
    
    while(!inFile.eof())
    {
        for( int i=0; i<4096; i++ )
        {
            char c;
            inFile >> c;
            //Mache iwas mit c
        }
    }
    //...
    

    Der Code würde quasi den Lesepuffer auf 4096 setzen und und immer nach diesen 4096 Zeichen erst wieder auf die Platte zugreifen?

    Ja, würde es.
    Aber wie TyRoXx schon geschrieben hat: operator >> macht viel zu viel als dass du den pro Zeichen ausführen wollen würdest.
    Eher so in der Art:

    std::ifstream inFile("file.txt",std::ios::binary);
    
    while (inFile)
    {
        char buffer[64 * 1024];
        inFile.read(buffer, sizeof(buffer));
        size_t const read = inFile.gcount();
        assert(read == sizeof(buffer) || !inFile);
    
        for (size_t i = 0; i < read; i++)
        {
            char const c = buffer[i];
            //Mache iwas mit c
        }
    }
    


  • Hlymur schrieb:

    Aus meiner Sicht gibt es 3 Möglichkeiten:
    1. Zeichen für Zeichen lesen. ( Schätze mal, dass das weniger optimal ist)

    Doch, das ist schon "quasi" optimal. Du benutzt dazu ja bereits Klassen wie ifstream, die intern so einen Puffer haben. Das Verschlüsseln von Dir wird viel mehr Zeit kosten als das Bißchen Handling im ifstream. Ok, er macht noch ein wenig Quark dazu, kannst für mehr Performance gleich auf den streambuf gehen oder so.

    Klar verbietet es sich, die nativen Betriebssystemfunktionen wie ::read() zu benutzen, um einzelne Bytes zu ziehen.

    Hlymur schrieb:

    2. Puffergröße an Blockgröße der Festplatte anpassen, also z.B. bei na normalen Festplatte mit OS=Windows -> ntfs 4096 Bytes pro Zugriff lesen ( wäre meine Wahl)

    Ok, wir puffern. Aber ob die Blockgräße jetzt 2k, 4k, 8k, …, 256k ist, es ist wurstegal. So unter 2k wird's spürbar lahm. Ab 2k haste "quasi" vollen Speed. Und der bleibt auch voll bis zu utopischen Größen wie 1M und sinjt erst drüber wieder ganz langsam ab. Unter 2k sinkt er rapide ab. Ich würde generell so 8k nehmen, eine übliche Speicherseite halt bei 64-bittern.

    Hlymur schrieb:

    3. Alle Zeichen auf einmal lesen(3. Option eher utopisch, Große Dateien wahrscheinlich nicht in RAM ladbar)

    Jo, ist Unfug.

    Tyroxx schrieb:

    4. Die Datei in den virtuellen Speicher mappen.

    Jo, das ist das Mittel der Wahl. Es zwingt sich geradezu auf.
    Denkt man.
    Nee, ist aber nicht so.
    Beim Sequenzellen Lesen/Schreiben ist ::read/::write mit handlichem Puffer von 4k oder 8k nicht lahmer (naja, ich kann manchmal was messen, aber weit unter einem Prozent).

    Ich würde empfehlen, erstmal schlicht bei ifstream zu bleiben. Wenn man (oft) das Letze rausholen will, auch mal eine ähnliche und dabei viel schlankere Klasse bauen, die man dann unterm Programm wegumtauschen kann. Das macht riesig Spaß, man kann garantieren, daß man dafür nicht langsamer ist als man mit asm wäre, und am Ende setzt man es doch fast nie ein. 🙂



  • Ich danke euch für die vielen hilfreichen antworten 🙂
    Denke, jetzt ist mir vieles verständlicher geworden *grins*

    Es ist schon verwunderlich, wie viel Geheimnisse in einer Sprache noch liegen können, obwohl man schon mehrere Jahre mit dieser programmiert ^^

    DAnke und mfG

    Hlymur


Log in to reply