2009-07-29 4 views
5

Bei der Fehlerbehebung einige Leistungsprobleme in unseren Anwendungen, fand ich heraus, dass C stdio.h Funktionen (und zumindest für unseren Hersteller, C++ fstream Klassen) Threadssafe sind. Als Ergebnis muss die RTL jedes Mal, wenn ich etwas so Einfaches wie mache, eine Sperre erfassen, ein Byte lesen und die Sperre aufheben.Nicht-ThreadSafe-Datei E/A in C/C++

Dies ist nicht gut für die Leistung.

Was ist der beste Weg, um non-threadsafe Datei-I/O in C und C++ zu bekommen, so dass ich mich selbst sperren kann und bessere Leistung bekomme?

  • MSVC bietet _fputc_nolock und GCC bietet unlocked_stdio und flockfile, aber ich kann keine ähnlichen Funktionen in meinem Compiler (CodeGear C++ Builder) finden.
  • Ich könnte die rohe Windows-API verwenden, aber das ist nicht portierbar und ich nehme an, wäre langsamer als ein entsperrtes fgetc für Zeichen-zu-Zeit-I/O.
  • Ich könnte zu etwas wie die Apache Portable Runtime wechseln, aber das könnte möglicherweise eine Menge Arbeit sein.

Wie nähern sich andere diesem?

Bearbeiten: Da ein paar Leute sich wunderten, hatte ich dies vor dem Posten getestet. fgetc führt keine Systemaufrufe aus, wenn es Lesevorgänge aus seinem Puffer erfüllen kann, aber es macht immer noch Sperren, so dass die Sperrung einen enormen Prozentsatz an Zeit beansprucht (Hunderte von Sperren für einen einzelnen Datenblock, der von der Festplatte gelesen wird)). Es wäre keine Lösung, Zeichen-auf-Zeit-E/A zu verwenden, aber die Klassen fstream von C++ Builder verwenden leider fgetc (wenn ich also iostream Klassen verwenden möchte, bin ich damit beschäftigt), und ich habe eine Menge von altem Code, der fgetc und Freunde verwendet, um Felder aus Record-style-Dateien zu lesen (was vernünftig wäre, wenn es keine Sperrprobleme gäbe).

+0

Cs stdio.h Funktionen sind nicht threadsafe; das ist auch dein Verkäufer. – MSalters

+0

Nicht nur mein Verkäufer; POSIX zum Beispiel benötigt es. –

Antwort

3

Ich würde einfach IO ein Char auf einmal nicht tun, wenn es vernünftige Leistung ist.

+0

Alle Vorgänge an Streams führen zu char IO. Sie müssen mit Stream-Puffern arbeiten. Ich habe unten geschrieben, wie ... – ovanes

1

fgetc liest fast sicher nicht jedes Mal ein Byte, wenn Sie es aufrufen (wobei "Lesen" bedeutet, einen Systemaufruf zum Ausführen von E/A aufzurufen). Suchen Sie woanders nach Ihrem Leistungsengpass, da dies wahrscheinlich nicht das Problem ist, und die Verwendung unsicherer Funktionen ist sicherlich nicht die Lösung. Jede von Ihnen durchgeführte Sperre wird wahrscheinlich weniger effizient sein als die von den Standardroutinen durchgeführte Verarbeitung.

+1

Es liest nicht ein Byte zu einer Zeit, aber es kann sehr gut eine Sperre jedes Mal nehmen. BTW, eine Sperre ist unter POSIX obligatorisch, gibt es getc_unlocked() (und _unlocked Variante von Char von Char IO-Funktionen und Sperren Funktionen, so dass sie geschützt werden können). – AProgrammer

1

Der einfachste Weg wäre, die gesamte Datei im Speicher zu lesen und dann eine eigene fgetc-ähnliche Schnittstelle für diesen Puffer bereitzustellen.

1

Warum nicht nur Speicher die Datei zuordnen? Das Speichermapping ist portabel (außer in Windows Vista, bei dem Sie durch die Hoffnung springen müssen, es jetzt zu benutzen, die Dummköpfe). Wie auch immer, mappen Sie Ihre Datei in den Speicher, und haben Sie eine eigene Sperre/Nicht-Sperre für den resultierenden Speicherort.

Das Betriebssystem verarbeitet alle Sperren, die zum Lesen von der Festplatte erforderlich sind - Sie werden diesen Aufwand NIEMALS vermeiden können. Ihr Verarbeitungsaufwand wird jedoch nicht durch Fremdverriegelungen beeinträchtigt, die Sie nicht selbst vornehmen.

1

der Multi-Plattform-Ansatz ist ziemlich einfach. Vermeiden Sie Funktionen oder Operatoren, bei denen der Standard angibt, dass sie Sentry verwenden sollen. Sentry ist eine innere Klasse in Iostream-Klassen, die für jedes Ausgabezeichen die Stream-Konsistenz gewährleistet und in Multi-Threaded-Umgebungen den Stream-bezogenen Mutex für jedes auszugebende Zeichen sperrt.Dies vermeidet Bedingungen Rennen auf niedrigem Niveau, aber immer noch macht die Ausgabe nicht lesbar, da Strings aus zwei Threads sein könnte Ausgang gleichzeitig wie das folgende Beispiel heißt es:

Thema 1 sollte schreiben: abc
Faden 2 schreiben sollte: def

Die Ausgabe könnte wie folgt aussehen: adebcf statt abcdef oder defabc. Dies ist darauf zurückzuführen, dass Sentry zum Sperren und Entsperren pro Zeichen implementiert ist.

Der Standard definiert es für alle Funktionen und Operatoren, die istream oder ostream behandeln. Die einzige Möglichkeit, dies zu vermeiden, ist die Verwendung von Stream-Puffern und eigenen Sperren (z. B. pro Zeichenfolge).

Ich habe eine App geschrieben, die einige Daten in eine Datei ausgibt und die Geschwindigkeit misst. Wenn Sie hier eine Funktion hinzufügen, die den fstream direkt ausgibt, ohne den Puffer zu verwenden und zu spülen, sehen Sie die Geschwindigkeitsdifferenz. Es nutzt Boost, aber ich hoffe, es ist kein Problem für Sie. Versuchen Sie, alle Stream-Puffer zu entfernen und den Unterschied mit und ohne sie zu sehen. In meinem Fall war der Leistungsnachteil Faktor 2-3 oder so.

Die following article von N. Myers wird erklären, wie Locales und Sentry in C++ IOStreams arbeiten. Und Sie sollten sicher im ISO C++ Standard Dokument nachsehen, welche Funktionen Sentry verwenden.

Good Luck,
Ovanes

#include <vector> 
#include <fstream> 
#include <iterator> 
#include <algorithm> 
#include <iostream> 
#include <cassert> 
#include <cstdlib> 

#include <boost/progress.hpp> 
#include <boost/shared_ptr.hpp> 

double do_copy_via_streambuf() 
{ 
    const size_t len = 1024*2048; 
    const size_t factor = 5; 
    ::std::vector<char> data(len, 1); 

    std::vector<char> buffer(len*factor, 0); 

    ::std::ofstream 
    ofs("test.dat", ::std::ios_base::binary|::std::ios_base::out); 
    noskipws(ofs); 

    std::streambuf* rdbuf = ofs.rdbuf()->pubsetbuf(&buffer[0], buffer.size()); 

    ::std::ostreambuf_iterator<char> oi(rdbuf); 

    boost::progress_timer pt; 

    for(size_t i=1; i<=250; ++i) 
    { 
    ::std::copy(data.begin(), data.end(), oi); 
    if(0==i%factor) 
     rdbuf->pubsync(); 
    } 

    ofs.flush(); 
    double rate = 500/pt.elapsed(); 
    std::cout << rate << std::endl; 
    return rate; 
} 

void count_avarage(const char* op_name, double (*fct)()) 
{ 
    double av_rate=0; 
    const size_t repeat = 1; 
    std::cout << "doing " << op_name << std::endl; 
    for(size_t i=0; i<repeat; ++i) 
     av_rate+=fct(); 

    std::cout << "average rate for " << op_name << ": " << av_rate/repeat 
      << "\n\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n" 
      << std::endl; 
} 


int main() 
{ 
    count_avarage("copy via streambuf iterator", do_copy_via_streambuf); 
    return 0; 
} 
1

Eine Sache zu prüfen, ist eine benutzerdefinierte Laufzeit zu bauen. Die meisten Compiler stellen die Quelle für die Laufzeitbibliothek zur Verfügung (ich wäre überrascht, wenn sie nicht im C++ Builder-Paket enthalten wäre).

Das könnte am Ende viel Arbeit bedeuten, aber vielleicht haben sie die Thread-Unterstützung lokalisiert, um so etwas einfach zu machen. Zum Beispiel mit dem eingebetteten System-Compiler, den ich verwende, ist es dafür entwickelt - sie haben dokumentierte Hooks, um die Lock-Routinen hinzuzufügen. Es ist jedoch möglich, dass dies ein Maintenance-Kopfschmerz sein könnte, auch wenn es anfangs relativ einfach ist.

Eine andere ähnliche Route wäre, mit jemandem wie Dinkumware über die Verwendung einer 3rd-Party-Laufzeit zu sprechen, die die Fähigkeiten bietet, die Sie benötigen.