C - filehandling und performance

Leitwolf22

Lt. Junior Grade
Registriert
Juni 2008
Beiträge
268
Hallo

Ich versuche da gerade ein paar wenige Daten von diversen Wetterstationen zu analysieren. Das ganze lauft dann in Excel unter ein paar VBA Skripts. Der Teil funktioniert und ist ok.
Die eigentlichen Quelldateien sind aber gigantisch und in "Monatspakete" (a ca. 500mb) zusammengefasst. Den ersten Datensatz habe ich noch manuell extrahiert, was aber so 3-4h dauert. Da dieser Ansatz wenig zukunftsträchtig schien, habe ich mich meiner rudimentären C Kenntnisse erinnert um das Problem so zu lösen.
Nun habe ich schon die für mich relevanten Stationen raus extrahiert und in Jahrespakete umgepackt, von denen ich nun zwei habe. Aus diesen will ich nun Files erstellen, die die gesuchten Daten pro Wetterstation über den gesamten Zeitraum von 2 Jahren enthalten. Auch das funktioniert nun prinzipiell bzw. lauft das Programm gerade, allerdings halt nicht sehr performant. Der schreckliche Code dazu folgt unten (relevant ist eigentlich nur der 2. Teil)...

Meine Frage ist nun. Das Programm lauft unter Codeblocks "build and run". Bringt es was, wenn man eine .exe compiliert und es dann in DOS laufen lässt? Mir ist aufgefallen, dass der Task Manager eine völlig unausgelastete CPU anzeigt während das Programm lauft. Von daher sollte also viel Luft nach oben sein. So wie es jetzt läuft, werden etwa 24h vergehen..


https://pastebin.com/embed_js/Gtbf9WFg

Code:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <conio.h>

int main ()
{
FILE *fp;
FILE *fpout;
FILE *fpinpt;

char line[255];
char temp[6];
char filedir[30] = "C:\\klima\\";
char filebase[30] = "C:\\klima\\exctracth";
char fileopen[30];
int vflag;
int i;
int a;
int x;
char locks[473][6];
//char filenames [12][20] = {"201601hourly.txt","201602hourly.txt","201603hourly.txt","201604hourly.txt","201605hourly.txt","201606hourly.txt","201607hourly.txt","201608hourly.txt","201609hourly.txt","201610hourly.txt","201611hourly.txt","201612hourly.txt"};
//char filenames [12][20] = {"201601daily.txt","201602daily.txt","201603daily.txt","201604daily.txt","201605daily.txt","201606daily.txt","201607daily.txt","201608daily.txt","201609daily.txt","201610daily.txt","201611daily.txt","201612daily.txt"};
char filenames [2][30] = {"C:\\klima\\extract 2015h.txt", "C:\\klima\\extract 2016h.txt"};
fpinpt = fopen("C:\\klima\\statcode.txt", "r");
for (i= 0; i < 472; i++) {
  fgets (locks[i],6, fpinpt);
  fgets (temp,2, fpinpt);
}
fclose (fpinpt);

/* End of part 1, start or part 2 */

for (a=0; a<472;a++) {
  strcpy (fileopen, filebase);
  strcat (fileopen, locks[a]);
  strcat (fileopen, ".txt");
printf ("%d und dateiname %s\n", a, fileopen);
  fpout = fopen(fileopen, "w");  // Write open
for (x = 0; x<2; x++) {
  fp = fopen(filenames[x], "r");
  if (fp != NULL) {
    while (!feof(fp)) {
      fgets(line,255, fp);
        if (strstr(line, locks[a])) {
        fputs (line, fpout);
        }
    }
  }
  fclose (fp);
}
  fclose (fpout); // Write close
}
return 0;
}
 
Zuletzt bearbeitet:
Kleiner Tip: C ist für sowas nicht unbedingt die beste Wahl Was für ein Format haben die Dateien? CSV? XML? JSON? Am besten du postest mal einen kleinen Ausschnitt vom Datensatz.

Mit Python dürfte sich dein Vorhaben sehr leicht umsetzen lassen

PS: dein Code ist so unformatiert absolut unlesbar. Bitte einen Pasteservice wie etwa https://pastebin.com/ benutzen
 
Zuletzt bearbeitet:
Code lässt sich via <CODE></CODE> einbetten, die spitzen klammern müssen nur durch Eckige [] ersetzt werden.

Code:
#include stdio.h

int main(){
    printf("Hello World\n");
}
 
Keine Ahnung wie ich das einbetten soll, egal, der Code ist über den Link einsehbar.
 
Hmm:
- printf tut immer weh (da gehen ~ 100 kB/s drüber, daher darauf verzichten, wenn nicht notwendig). Insgesamt ist Konsolenausgabe langsam.
- fopen/fclose ist teuer (flush), versuch, dass du die so wenig wie möglich aufrufst.
- Initialisiere deine Variablen direkt bei definieren, der C89-Stil ist alt und Scheiße.
- Designe dein Algorithmus etwas besser, dann geht es von O(n²) zu O(n) (ich lese deine filenames nur noch einmal ein, statt 472 mal, das geht dann etwas schneller :)).
- Mein VS will den Code gar nicht kompilieren, da die Funktionen alle als veraltet markiert sind ( die ganzen string-Funktionen brauchen ein _s und eine Größe).

Original:
Code:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <conio.h>

int main()
{
	FILE *fp;
	FILE *fpout;
	FILE *fpinpt;

	char line[255];
	char temp[6];
	char filedir[30] = "C:\\klima\";
		char filebase[30] = "C:\\klima\\exctracth";
	char fileopen[30];
	int vflag;
	int i;
	int a;
	int x;
	char locks[473][6];
	//char filenames [12][20] = {"201601hourly.txt","201602hourly.txt","201603hourly.txt","201604hourly.txt","20 1605hourly.txt","201606hourly.txt","201607hourly.txt","201608hourly.txt","201609 hourly.txt","201610hourly.txt","201611hourly.txt","201612hourly.txt"};
	//char filenames [12][20] = {"201601daily.txt","201602daily.txt","201603daily.txt","201604daily.txt","201605 daily.txt","201606daily.txt","201607daily.txt","201608daily.txt","201609daily.tx t","201610daily.txt","201611daily.txt","201612daily.txt"};
	char filenames[2][30] = { "C:\\klima\\extract 2015h.txt", "C:\\klima\\extract 2016h.txt" };
	fpinpt = fopen("C:\\klima\\statcode.txt", "r");
	for (i = 0; i < 472; i++) {
		fgets(locks[i], 6, fpinpt);
		fgets(temp, 2, fpinpt);
	}
	fclose(fpinpt);

	/* End of part 1, start or part 2 */

	for (a = 0; a<472; a++) {
		strcpy(fileopen, filebase);
		strcat(fileopen, locks[a]);
		strcat(fileopen, ".txt");
		printf("%d und dateiname %s\n", a, fileopen);
		fpout = fopen(fileopen, "w"); // Write open
		for (x = 0; x<2; x++) {
			fp = fopen(filenames[x], "r");
			if (fp != NULL) {
				while (!feof(fp)) {
					fgets(line, 255, fp);
					if (strstr(line, locks[a])) {
						fputs(line, fpout);
					}
				}
			}
			fclose(fp);
		}
		fclose(fpout); // Write close
	}
	return 0;
}
Modifiziert:
Code:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <conio.h>

int main()
{
	
	char *filedir = "C:\\klima\\";
	char *filebase = "C:\\klima\\exctracth";
	
	FILE*fpinpt = fopen("C:\\klima\\statcode.txt", "r");

	char locks[473][7];
	for (int i = 0; i < 472; i++) {
		fgets(locks[i], 6, fpinpt);
		char temp[3] = {};
		fgets(temp, 2, fpinpt);
	}
	fclose(fpinpt);

	/* End of reading locations, start or reading data */

	FILE*flocs[472];
	
	for (int i = 0; i < 472; ++i) {
		char fileopen[255] = {};
		strcpy(fileopen, filebase);
		strcat(fileopen, locks[i]);
		strcat(fileopen, ".txt");
		flocs[i] = fopen(fileopen, "w");
	}

	char filenames[2][30] = { "C:\\klima\\extract 2015h.txt", "C:\\klima\\extract 2016h.txt" };
	for (int x = 0; x < 2; x++) {
		FILE *fp = fopen(filenames[x], "r");
		while (!feof(fp)) {
			char line[255] = {};
			fgets(line, 255, fp);
			for (int a = 0; a < 472; a++) {
				if (strstr(line, locks[a])) {
					fputs(line, flocs[a]);
				}
			}
		}
		fclose(fp);
	}
	for (int i = 0; i < 472; ++i)
		fclose(flocs[i]);
	return 0;
}
 
denke auch dass eine programmiersprache, bei der du direkt ein read-buffering hast, fuer solche anwendungen wesentlich besser ist, als das brecheisen, das C mit fgets hier zur verfuegung stellt. bspw. java oder C#. moeglicherweise ist auch python in ordnung.

soweit ich das nach kurzem druebergucken verstehe suchst du dir aus zwei dateien die zeilen raus, die einen von 472(?) "statcodes" beinhalten?

damit gibt's ein paar probleme:
  • die aeussere schleife iteriert ueber die statcodes, die innere fuehrt file reads aus. sind die dateien sehr gross, dann bedeutet das, dass du jene grossen dateien 472 mal durchs betriebssystem komplett ungecached ausliest. das ist katastrophal und alleine durch das vertauschen der schleifen (innere und aeussere) solltest du einen signifikanten performanceboost erhalten, da stringvergleiche (strstr) rein im speicher/cpu ablaufen und nicht auf der lahmen ssd/hdd)
  • kein buffering, du liest dir lediglich jede zeile (fgets) bis zu 255 zeichen ein (reicht das?). sinnvoller waere z.B. in java ein BufferedReader, der dich effizienter auf den archivspeicher (festplatte, ssd) zugreifen laesst, und damit das I/O bottleneck minimiert
  • moeglicherweise ist strcmp bei korrekter anwendung minimal schneller als strstr, aber das ist dann detailarbeit.
  • ein kern deiner cpu wird nur benutzt, weil du keinerlei multithreading betreibst. wuerde auch nicht viel sinn machen bei so simplen aufgaben wie stringvergleichen, die in deinem fall grob i/o capped sind.
  • writes sind ebenfalls nicht gepuffert. je nach verfuegbarkeit von speicher auf der maschine und der erwarteten groesse des ergebnissets wuerde ich darueber nachdenken, den ausgabestring komplett im speicher zu puffern, bevor ich ihn schreibe, da bei staendig alternierenden lese- (staendig)/schreib- (bei korrektem code) zugriffen ein quasi-random access szenario zustande kommt, was festplatten miserabel und SSDs auch nicht uebertrieben gut abhandeln.
 
Zuletzt bearbeitet:
Nope, awk ist meinen Erfahrungen nach sehr langsam, und für große Dateimengen nicht geeignet.

@TS
Warum arbeitest Du nicht über eine Datenbank?
 
Ok, vielen Dank mal

Mir ist schon klar, dass es da effizientere Programmiersprachen geben mag. Nur bis ich mich da eingearbeitet hab ist das Skript längst fertig ;)

Grundlegend ist das Problem, dass es 472 Wetterstationen in den beiden Jahresfiles gibt (beide nun etwa 800mb groß). Daraus will ich 472 Files erstellen, in denen jeweils die gesamten Daten einer Station stecken. Die haben dann zugleich das Format, das ich für Excel benötige.

Es ist natürlich sehr ineffizient jetzt mal den Code der ersten Station zu nehmen, die beiden großen Files danach abzugreifen um diese Zeilen dann ins jeweilige File der Station zu packen. Und diese Prozedur fängt dann wieder von vorne an, für jede einzelne Station. Ich hätte mir auch vorstellen können jede Zeile durchzugehen, und sie auf das passende File zuzuordnen, dann müsste ich aber 472 Files gleichzeitig offen halten, und ich glaub nicht, dass C das schafft.

@Hancock Das ist glaub ich genau das, was du da vorschlägst. Leider, ich hab den code probiert, crasht der Ansatz jedes Mal.

Die printf ausgaben sind mir wichtig und kosten überhaupt keine Zeit, auch nicht das öffnen und schließen der Dateien. Die Vorgangsweise hat vielmehr den Vorteil, dass ich das Programm jederzeit stoppen und neu starten kann (ich weiß wie weit es ist, die schon generierten Files bleiben ja..)
Das Programm hängt zu >99,9% in diesem Prozedere fest (also auslesen der Megafiles und kopieren der relevanten Zeilen)

Code:
    while (!feof(fp)) {
      fgets(line,255, fp);
        if (strstr(line, locks[a])) {
        fputs (line, fpout);
        }
    }
Ergänzung ()

Da fällt mir noch ein: eine wesentlich Beschleunigung würde es vielleicht bringen auf die strstr() zu verzichten. Da der Code der Wetterstation ja am Anfang der Zeile steht, könnte man es auf eine "==" Abfrage umstellen. Damit hab ich zwar rumgespielt, konnte aber erstens nie eine spezifischen String in locks[] so identifizieren, bzw. müsste ich auch die ersten fünf Zeichen der jeweiligen Zeile isolieren. Keine Ahnung wie das richtig geht..
 
Nun ja, ich hab dir geschrieben, dass mein Compiler es nicht kompilieren wollte (da deprecated) und ich die Dateien nicht hab, also auch nicht debuggen kann.

C kann so viele Dateien offen haben, wie FOPEN_MAX groß ist. (Bei mir 20, daher crasht es wahrscheinlich, dann nimmste halt raw Zeiger oder öffnest die Dateien on the fly.)

Dein Ansatz ist O(n*m) und damit schnarch lahm, während meiner O(n+m) ist.

Ich empfehle dir ein guten Debugger, dann findest du auch raus, warum dein Programm abstürzt.

Und falls du ne richtig schnelle Lösung willst: 64 bit (falls mehr als 4 GB), ne Bib/Sprache mit Containersupport und alles in Vektoren abspeichern, die du dann rausschreibst. (z.B. C++ mit std::map<std::string,std::vector<std::string>>)
Code:
#include <string>
#include <vector>
#include <map>
#include <fstream>
using namespace std;

int main()
{
	
	string filedir = "C:\\klima\\";
	string filebase = "C:\\klima\\exctracth";
	
	auto fpinpt = ifstream("C:\\klima\\statcode.txt");

	map<string,vector<string>>data;

	
	for (int i = 0; i < 472; i++) {
		char locks[7];
		fpinpt.getline(locks, 6);
		data[locks] = vector<string>();
		char temp[3] = {};
		fpinpt.getline(temp, 2);
	}

	/* End of reading locations, start or reading data */

	char *filenames[2] = { "C:\\klima\\extract 2015h.txt", "C:\\klima\\extract 2016h.txt" };
	for (int x = 0; x < 2; x++) {
		auto fp = ifstream(filenames[x]);
		while (fp) {
			char line[255] = {};
			fp.getline(line, 255);
			for (auto&&a:data) {
				if (string(line).find(a.first)!=string::npos) {
					a.second.push_back(line);
				}
			}
		}
	}
	for (auto&&i : data) {
		auto f = ofstream((filebase + i.first + ".txt").c_str());
		for (auto&j : i.second)
			f << j << "\n";
	}
	return 0;
}
 
http://www.theunixschool.com/2012/06/awk-10-examples-to-split-file-into.html

Wobei ich nicht erwarten würde, dass awk langsamer ist als dein Selbstbau.
Möglichkeiten zur Optimierung:
*Du kannst deine beiden Ausgangsdateien parallel abarbeiten lassen und die Ergebnisse danach zusammenführen
*Du kannst die beiden Ausgangsdateien weiter aufspalten und entsprechend soviel Threads mit der Aufgabe betreuen wie die CPU gleichzeitig schafft

Wobei ich auch vorschlagen würde, dass du den Datensatz je nach spezifischer Anforderung wirklich in eine Datenbank kippst.


Nebenschauplatz:
Printf ist langsam! ;)
https://rxwen.blogspot.de/2010/07/performance-impact-of-printf.html


@Hancock
_s Funktionen sind für den Compiler VisualStudio veraltet. Für glibc gibt es _s nicht und in den Projekteinstellungen vom VisualStudio kannst du auch einstellen, dass Funktionen ohne _s verwendet werden können.
 
zoz schrieb:
Was für ein Format haben die Dateien? CSV? XML? JSON? Am besten du postest mal einen kleinen Ausschnitt vom Datensatz.
Das ist für mich eigentlich die wichtigste und bisher unbeantwortete Frage. Eigentlich jede Datenbank die ich kenne hat Methoden, um große Datenmengen in kurzer Zeit zu importieren. Wenn die Quelldaten einen derartigen Import erlauben, dann ist es aus meiner Sicht unsinnig, selbst irgend etwas zu entwickeln ...

Wenn die Daten dann in einer Datenbank erfasst sind, dann ist es auch ein leichtes die Daten für einzelne Stationen zu exportieren.
 
Die Frage welches Format lässt sich eigentlich aus dem Programmcode ersehen. Es sind allesamt .txt Files. Mit einer Datenbank müsst ich es ausprobieren, allerdings sind es (vorgefiltert) etwa 10 Mio Einträge und mein Rechner ist etwas Altersschwach. Vor dem Filtern waren es ca. 50 -100 Mio. Ich glaube gehört zu haben, dass es da bei Access zB gewisse Limits gibt.
 
.txt sagt halt über die interne Formatierung nix aus. In einer .txt kannst du Spalten entsprechend einer .csv mit Texttrennern wie ";" formatiert haben. Genausogut kannst du da XML hineinschreiben, Entsprechend schwingt bei den Fragen die Frage nach der internen Formatierung der Daten mit.

Access limitiert bei einer Datenabnkgröße von 2GB, wenn die Rohdaten 1,6GB groß sind, brauchst du mit Access nicht anfangen. Die kostenlose Version vom Microsoft SQL Server geht aber bis zu 10GB Datenbankgröße (Version 2013) oder du nimmst mysql, postgresql, firebird, SQLite, ... deren Limitierungen deutlich außerhalb deiner Anforderungen liegen.
Die Frage ist da dann natürlich auch, wie du die Daten in der Datenbank weiter verarbeitest.
 
Leitwolf22 schrieb:
Es sind allesamt .txt Files.
Was bitte sagt die Endung ".txt" über den Inhalt und die Struktur der Datensätze innerhalb dieser Datei aus?
Die Leute, die diese Daten exportieren, stellen vermutlich auch eine Datensatzbeschreibung zur Verfügung.

Mit 500 MB Daten kommst Du im Normalfall selbst bei Access nicht an die Grenzen, da kann eine einzelne Tabelle bis zu 2 GB groß werden. Allerdings existiert meines Wissens für Access keine Möglichkeit Bulk Daten zu importieren, so dass ich eher an Datenbanken wie MySQL oder die für Privatanwender kostenfreien (Express-) Versionen von Oracle oder Microsoft SQL Server dachte.

Piktogramm schrieb:
Access limitiert bei einer Datenabnkgröße von 2GB
Ein einzelnes Datenbankfile kann nicht größer werden. Man kann aber mehrere Datenbankfiles verlinken, so dass dieses Limit praktisch nur für eine einzelne Tabelle (+Overhead) gilt.
 
Zuletzt bearbeitet:
@Piktogramm: Ja, ob jetzt die snprintf-Konsorten oder die _n, beides mal das gleiche gemeint.

Threads sind etwas overkill (ich denke, eine gute Lösung ist IO-limitiert).
Datenbanken bieten sich natürlich bei den Datensätzen schon an. MySQL ist über PHP mit einem Browser gut managebar und ermöglicht relativ schnelle Ergebnisse. (phpMyAdmin z.B.)
SQLite find ich persönlich schön, da schnell, nicht überladen und von der Einbindung unkompliziert. Zum Rumspielen find ich allerdings MySQL+phpMyAdmin schöner.

Wenn ich mir das Datenformat so anschaue (es ist NICHT ganz eindeutig aus dem Programmtext erschließbar!), dann sind es wahrscheinlich viele Zeilen in einem fixen Format, womit die Dateien via phpMyAdmin importierbar und mit dem richtigen MySQL Stringbefehl auch entsprechend normalisierbar sein sollten. Allerdings musst du dich da vermutlich erst mal einarbeiten @Leitwolf22.

@Leitwolf22: Funktioniert die zweite Variante (in C++) von mir einigermaßen? Die sollte von der Performance nicht soo schlecht sein (und deine CPU mindestens etwas auslasten).
 
awk auf mehreren Threads lohnt bzw. kann lohnen. Zum einen weil der I/O-Cache viele Zugriffe (sowohl lesende als auch schreibende) sowieso abfängt und entsprechend die I/O-Limitierung der Hardware nicht so ins Gewicht fällt* und die meisten SSDs sowieso über eine höhere QueryDepth skalieren. Entsprechend vermute ich, dass gerade der Anwendungsfall vom TE sich lohnt um ihn auf verschiedene Threads zu verteilen.


*Annahme: Es ist genügend RAM vorhanden, damit das System auch gescheit cachen kann
 
Hancock schrieb:
Datenbanken bieten sich natürlich bei den Datensätzen schon an. MySQL ist über PHP mit einem Browser gut managebar und ermöglicht relativ schnelle Ergebnisse. (phpMyAdmin z.B.)
Nur das man eine 500 MB Datei nicht über den Browser irgendwo einfügen will ...

MySQL erlaubt es Dateien mittels LOAD DATA INFILE direkt in die Datenbank zu schreiben. Microsoft SQL Server hat dafür BULK INSERT und Oracle hat den SQL Loader.
 
Weil zu den anderen Dingen schon viel gesagt wurde, muss ich noch jemanden verteidigen, der sich selbst nicht wehren kann:

Leitwolf22 schrieb:
Bringt es was, wenn man eine .exe compiliert und es dann in DOS laufen lässt?

Seid froh, die NT-Konsole mit ihren Features zu haben und hört auf, sie als DOS zu beleidigen.

Consumerversionen von Windows beinhalten seit XP kein DOS mehr. 32Bit-Windowse hatten noch eine virtuelle Maschine, die aber unter 64Bit-Windows fehlt.
Deshalb kann ein modernes Windows (der letzten 10 Jahre) keine DOS-Programme ausführen. Es gibt keine DOS-Box. Es gibt die NT-Konsole.

NT-KONSOLE! :(
 
Zurück
Oben