[C/Philosophie]fread: Welche Grösse pro Vorgang lesen?

cwriter

Erfahrenes Mitglied
Hallo Welt

Wiedereinmal bin ich in den Wirrungen meiner Gedanken auf eine grundsätzliche Frage gestossen:
Ist es besser, mit fread() kleine Grössen zu lesen, oder ist es besser, den ganzen Array direkt zu füllen?
Prämissen:
  • Die Daten werden schon im Lesevorgang verarbeitet, d.h.: Per Multithreading wird im einen Thread gelesen, im anderen werden die gelesenen Daten (aber nie die noch nicht gelesenen Daten - logischerweise ;-) ) verarbeitet.
  • Dazu ist der Array im Speicher schon komplett alloziert.
  • Die Grösse der zu lesenden Dateien sollte variabel sein, nie aber eine Grösse von 32bit überschreiten (~3.5 GB).

Der Gedanke: fread() blockt solange, bis alle Daten gelesen sind. Nun haben gerade herkömmliche Festplatten eine lange Antwortzeit, die sich da wieder auf die Geschwindigkeit des Programms auswirkt.

Konkret:
Ist bei einer arraysize von 5'000'000 (5MB) das
C:
fread(arr, 1, arraysize, f);
oder
C:
for(int i = 0;i<1000;i++)
    fread(arr+(i*5000), 1, 5000, f);
besser?

Ich freue mich auf alle Anregungen!

Gruss
cwriter
 
Hi, die Antwort dazu würde mich auch interessieren. ;)
Da du aber scheinbar ein konkretes Problem hast, wieso testest du nicht einfach mal beide Varianten auf Laufzeiten?

Grüße,
Jennesta
 
[...], wieso testest du nicht einfach mal beide Varianten auf Laufzeiten?
Kann ich schon, aber da fragt sich einfach, wie repräsentativ ein i7 4700MQ, 16 GB DDR3 L und 7200rpm HDD sind... *firstworldproblems*

Ich mache einfach mal. Moment...

So: Bei einer Grösse von 1'600'045 Bytes ist der Vollladegang 1 Clock Tick schneller (170 statt 171).
Bei einer Grösse von 160'000'045 Bytes ist der Teilweise-Ladegang schneller (16617 gegen 16733).

Drehe ich die CPU aber auf 0.8GHz runter (bei 160MB), dann steht (Gott war ich geduldig) 41423 ct (Teilweise) gegen 40162 ct.
-> Vorhersehbar. Nur: Was ist nun besser?

Gruss
cwriter
 
Zuletzt bearbeitet:
Hi

du schreibst, dass sich die Lesesache auf die Programmgeschwindigkeit auswirkt.
Warum?
Es passiert in einem eigenen Thread, verarbeitet werden müssen sowieso alle Daten, usw.
Unterschied 1 ist die Leerlaufzeit der Verarbeitung, während der Buffer das erste Mal gefüllt wird,
und die Verarbeitung des letzten Teils, nachdem das Lesen fertig ist.
Kleinere Portionen = Schneller. Je kleiner die Gesamtdaten sind desto mehr Auswirkung.
Unterschied 2:Synchronisationsaufwand zw. den Threads. Größere Portionen ist hier besser.
Bis herher würde das heißen, ein vernünftiges Mittelmaß zu ermessen (Geschwindigkeit, Prozessorlast).

Punkt 3, der ganz für große Portionen spricht:
Eine Festplattenlesung hat Overhead, und nicht wenig.
Das ganze Zeug in der C-Runtimelibrary, Betriebssystemebene, Treiber, Suchzeit der Platte...
Versuch einmal 5000000x 1 Byte zu lesen, statt 1x 5MB. Da merkt man das richtig.

Punkt 4: Oben steht die Wahl 5MB oder 1000x 5KB.
Wenn, dann besser 4KB (Sektorgrößenzeug. Eventuell 8KB lesen, obwohl man nur 5KB will)
(und 5000 ist sowieso daneben :p
5*1024 wäre für den Computer angenehmer)

Kenn ja deine Messergebnisse (noch) nicht, aber ich wette für große Portionen :D


Noch paar Sachen:
32bit unsigned kann genau 4GB zählen (mit 1024 statt 1000 als Maß, sonst noch mehr)

Und dass fread alle geforderten Byte liest ist nicht sicher.
Eigentlich muss man den Returnwert prüfen,
und ggf. für die fehlenden Byte noch einen Aufruf machen
(und gar-nichts-gelesen-Fehler gibts auch noch).
 
du schreibst, dass sich die Lesesache auf die Programmgeschwindigkeit auswirkt.
Warum?
Es passiert in einem eigenen Thread, verarbeitet werden müssen sowieso alle Daten, usw.
Unterschied 1 ist die Leerlaufzeit der Verarbeitung, während der Buffer das erste Mal gefüllt wird,
und die Verarbeitung des letzten Teils, nachdem das Lesen fertig ist.
Weil ich bei den kleinen Einheiten schon lesen kann, bevor alles eingelesen ist. Grundsätzliche Annahme: Der Computer des Users X hat einen 15GHz-Prozessor und eine 100rpm HDD (ein bisschen extrem ;-) ). Die langsame HDD kann so von der CPU ausgeglichen werden.

...
So, vielleicht ist Code hier klarer:
C:
DWORD WINAPI Thread(readinfo** ri)
{
	size_t size = (*ri)->filesize;
	for(int i = 0;i<(*ri)->filesize;i++)
	{
		if(size > (*ri)->arrsize) size -= fread((*ri)->arr+(*ri)->read,1,(*ri)->arrsize,(*ri)->f);
		else size -= fread((*ri)->arr+(*ri)->read,1,size,(*ri)->f);
		
		(*ri)->read = (*ri)->filesize - size;
		if(size == 0) break;
	}
	return 0;
}
Int ist threadsafe und somit ist die Synchronisierung relativ einfach. Grundprinzip: Die Parse-Funktion liest, bis der Verarbeitungspointer (der "Fortschritt" des Verarbeitens) nur noch 50 oder weniger Bytes vom Fortschritt des Lesens (readinfo.read) entfernt ist (keine Angst, mehr als 10 Bytes am Stück werden nie verarbeitet, es besteht also keine Chance, dass sich die beiden in die Quere kommen).

Eigentlich muss man den Returnwert prüfen,
und ggf. für die fehlenden Byte noch einen Aufruf machen
(und gar-nichts-gelesen-Fehler gibts auch noch).

Hö? fread() war bei mir immer zuverlässig (gut, ich habe auch noch nie meine Platte geschrottet, so gesehen...). Beim tatsächlich genutzten Code wird das aber schon berücksichtigt.

Die Ergebnisse habe ich oben eingefügt, sie sind nicht sooo unterschiedlich. Ausser, wenn man CPU wegnimmt... (Lesegrösse 1000Bytes).

Wozu gibt es virtuelle Maschinen?
Habe ich *leider* kein Windows drauf :-(
(Ja, könnte man gratis ziehen usw., aber ich brauche das so selten, dass es sich nicht wirklich lohnt.)

Gruss
cwriter
 
Und dass fread alle geforderten Byte liest ist nicht sicher.
Eigentlich muss man den Returnwert prüfen,
und ggf. für die fehlenden Byte noch einen Aufruf machen
(und gar-nichts-gelesen-Fehler gibts auch noch).
fread gibt nur einen kleineren als den angefragten Wert zurück, wenn entweder a) das Ende der Datei erreicht wurde oder b) ein Lesefehler aufgetreten ist. In beiden Fällen wird auch ein erneuter Versuch fehlschlagen. Hast du vielleicht an read gedacht?

So, vielleicht ist Code hier klarer: …
Mir hat dieser Code überhaupt nicht geholfen. Die Formatierung ist für meinen Geschmack zu eng (zu wenige Leerzeichen und Zeilenumbrüche), die Variablennamen sind nichtssagend, die Semantik der Member von readinfo ist nicht zweifelsfrei erschließbar, es ist nicht ersichtlich, wieso ri als Zeiger-auf-Zeiger anstatt Zeiger übergeben wird etc. Und schlussendlich liest die Funktion nur einen Teil einer Datei ein und macht nichts weiter. Was wolltest du uns damit zeigen?

Int ist threadsafe und somit ist die Synchronisierung relativ einfach. Grundprinzip: Die Parse-Funktion liest, bis der Verarbeitungspointer (der "Fortschritt" des Verarbeitens) nur noch 50 oder weniger Bytes vom Fortschritt des Lesens (readinfo.read) entfernt ist (keine Angst, mehr als 10 Bytes am Stück werden nie verarbeitet, es besteht also keine Chance, dass sich die beiden in die Quere kommen).
Was ist „Int“? Was ist die „Parse-Funktion“?

Die Ergebnisse habe ich oben eingefügt, sie sind nicht sooo unterschiedlich. Ausser, wenn man CPU wegnimmt... (Lesegrösse 1000Bytes).
Das könnte daran liegen, dass fread intern einen eigenen Lesepuffer verwendet. Das ist zwar nicht vorgeschrieben, aber auch nicht verboten.

Ohne deinen Anwendungsfall genauer zu kennen wäre Memory Mapping der Datei vielleicht eine Alternative.

Grüße,
Matthias
 
Die Formatierung ist für meinen Geschmack zu eng (zu wenige Leerzeichen und Zeilenumbrüche), die Variablennamen sind nichtssagend, die Semantik der Member von readinfo ist nicht zweifelsfrei erschließbar, es ist nicht ersichtlich, wieso ri als Zeiger-auf-Zeiger anstatt Zeiger übergeben wird etc. Und schlussendlich liest die Funktion nur einen Teil einer Datei ein und macht nichts weiter. Was wolltest du uns damit zeigen?
Zum Layout: Also so schlimm ist das jetzt auch wieder nicht. Es garantiert einfach, dass (*ri)->arrsize Blöcke gelesen werden, wenn die verbleibende Dateimenge (size) grösser als (*ri)->arrsize ist.
Wie du darauf kommst, dass nur ein Teil der Datei eingelesen wird, ist mir schleierhaft.
ri als Zeiger zu Zeiger: Weil ich es kann ;-)
Ich habe früher noch anderes in dieser Funktion angestelllt, wozu ich Pointer to Pointer brauchte.
Int ist ein falsch geschriebenes int (ja, gefährlich wegen signed/unsigned comparision).
Dir Parsefunktion verarbeitet einfach die Daten. Oder was meinst du?

Gruss
cwriter
 
Int ist threadsafe

Leider nein. Es gibt im C Standard eine Aussage zu atomaren int-Typen: Es braucht einen Typen sig_atomic_t der atomar auslesbar und schreibbar sein muss (wie das geschieht ist implementationsabhängig). Alles andere -> Nicht threadsafe

Hö? fread() war bei mir immer zuverlässig

Zitat Standard:
The fread function reads, into the array pointed to by ptr, up to nmemb elements
whose size is speci?ed by size, from the stream pointed to by stream.



Die Ergebnisse habe ich oben eingefügt, sie sind nicht sooo unterschiedlich. Ausser, wenn man CPU wegnimmt... (Lesegrösse 1000Bytes).
 
Zum Layout: Also so schlimm ist das jetzt auch wieder nicht.
Für dich ist es vielleicht einfach zu verstehen, aber als Außenstehender tut man sich mit unkommentiertem Code erst mal schwer. Man muss erst herausfinden, dass in size die verbleibenden zu lesenden Bytes gespeichert werden (warum heißt die Variable dann nicht entsprechend?), dass if- und else-Zweig exakt dasselbe machen bis auf den 3. Parameter von fread (wieso ist der Code dupliziert?), dass die Schleife in der Form wohl nur zur Verwirrung da ist (wieso nicht einfach while (size != 0) …?), dass (*ri)->arrsize wohl nicht (wie der Name vermuten lässt) die Größe des Arrays (*ri)->arr sein kann (sonst käme es spätestens beim zweiten Schleifendurchlauf zum Überlauf)… wie du siehst gibt der Code reichlich Anlass zur Verwirrung.

Es garantiert einfach, dass (*ri)->arrsize Blöcke gelesen werden, wenn die verbleibende Dateimenge (size) grösser als (*ri)->arrsize ist.
Aha, das hättest du auch gleich so sagen können.

Wie du darauf kommst, dass nur ein Teil der Datei eingelesen wird, ist mir schleierhaft.
Du hast uns keinerlei Hinweis darauf gegeben, in welchem Zustand die einzelnen Member von ri beim Aufruf von Thread sind. Daher kann ich leider weder wissen ob (*ri)->filesize die Größe der kompletten Datei hinter dem Dateizeiger (*ri)->f ist, noch an welcher Stelle sich der Lesezeiger in der Datei befindet.

Dir Parsefunktion verarbeitet einfach die Daten. Oder was meinst du?
Den Begriff „Parse-Funktion“ hattest du bisher noch nicht verwendet und kam auch im unmittelbar darüberstehenden Codestück nicht vor, darum die Nachfrage nach dem Zusammenhang.

Habe ich das jetzt so richtig verstanden: du startest einen Thread A, der nur die Datei blockweise einliest (mit dem eingestellten Codestück) und in einem separaten Thread B läuft die Parse-Funktion, welche die eingelesenen Daten verarbeitet, sobald sie zur Verfügung stehen?

Grüße,
Matthias
 

Neue Beiträge

Zurück