C Singelthread schneller als Pthread?

mahatma andi

Cadet 4th Year
Registriert
Jan. 2007
Beiträge
97
Hallo Leute,

Ich habe gestern eine einfache Implementation eines Monte Carlo Integrals geschrieben. Einmal mit pthreads und einmal 'normal'. Da die Threads, bis sie ihr Ergebnis haben, nicht kommunizieren müssen, sollte das eigentlich extrem einfach zu parallelisieren sein.

Nun ist jedoch die pthread-Version durchgehend langsamer als die normale Version, obwohl die pthread Version 4 mal weniger Rechnen muss (4 threads). Dabei nimmt die Dauer, die das pthread-Programm braucht, um auszuführen, mit der Anzahl Threads zu.

Nun habe ich ein (möglichst einfaches) Beispielprogram zum Vergleich geschrieben, um zu sehen, ob es an Mutex usw. liegt beim Monte-Carlo-Integral. Diese beiden Beispielcodes, einmal pthread, einmal normal, addieren in einer Funktion 'Kernel' ein paar loop-Indizes und geben dann kein Ergebniss zurück, da das fürs Minimalbeispiel egal ist.
Auch hier ist die pthread-Version deutlich langsamer, etwa Faktor 'Anzahl Threads'. Kann mir jemand erklären wieso? Die Kerne sind beim Pthread alle voll ausgelastet, irgendwas wird also gerechnet, Kommunikation zwischen den Threads ist auch nicht nötig. Dass der pthread-Overhead so riesig ist, kann ich mir auch nicht vorstellen...

Hier die Codes:
pthread:
Code:
#include <stdio.h>
#include <time.h>
#include <pthread.h>

#define N 1000000000.f
#define Nthreads 4


void *kernel(void* arguments);

int main(){

		clock_t start, finish;
		double cpu_time_used;
		start = clock();

		pthread_t thread_ID[Nthreads];
		void  *exit_status;
		float arguments = 1;
		int i;
		
		
		for( i=0; i<Nthreads; i++){
				pthread_create( &thread_ID[i], NULL, kernel, &arguments);
		}

		for( i=0; i<Nthreads; i++){
				pthread_join( thread_ID[i], &exit_status);
		}



		finish = clock();
		cpu_time_used = ((double) (finish - start) / CLOCKS_PER_SEC );

		printf("time elapsed is %f\n", cpu_time_used); 

		return 0;
}



void *kernel(void* arguments){
		int i;
		float *arg;
		arg = (float*) arguments;

		for( i=0; i<((float)N/Nthreads); i++){
				*arg += i;
		}
}

singlethread:
Code:
#include <stdio.h>
#include <time.h>

#define N 1000000000.f


void kernel(float argument);

int main(){

		clock_t start, finish;
		double cpu_time_used;
		start = clock();
		float result = 0;
		float input = 0;

		kernel( input);

		finish = clock();
		cpu_time_used = ((double) (finish - start) / CLOCKS_PER_SEC );

		printf("time elapsed is %f\n", cpu_time_used); 

		return 0;
}



void kernel(float argument){
		int i;
		int l;
		for( l=0; l<4; l++){
				for( i=0; i<N/4; i++){
						argument += i;
				}
		}
}

Falls jemand Lektüre zum Thema empfehlen kann, wäre ich dankbar.
Danke im Voraus,
Mahatma
 
Auf den ersten Blick wuerde ich auf cache trashing tippen, Du verwendest *arg um die Ergebnisse auzusummieren.
Mache eine lokale Variable accu, verwende diese im Loop, und addiere am Ende *arg+=accu,
damit hat jeder Thread seine lokale Variable (im Register), und auf den Speicher wird nur einmal am Ende zugegriffen.
So kloppen sich die Threads um die Cacheline, und nebenbei sollte das Ergebnis ohne Locking her zufaellig sein, oder?
 
Wielange rechnet die Simulation denn single threaded? Eventuell ist das so schnell, dass Threads erstellen und koordinieren alles langsamer macht?
 
Das Problem liegt tatsächlich im Cache begraben. Die Variable "arguments" landet beim ersten Zugriff im L1 Caches eines Kerns. Danach mag der nächste Thread vom Betriebssystem auf einem anderen Kern eingereiht sein. Das Kohärenzprotokoll des Caches sorgt jetzt für den Transfer der im L1 befindlichen Variable in den L1 Cache des neuen Kerns. Dadurch muss faktisch mit jedem Zugriff die Cacheline bewegt werden und das kostet Zeit.
Finde einfach die Größe der Cachelines raus und platziere für jeden Thread eine Variable mit einer "Cachzeile" Abstand im Speicher. Auf meiner CPU (Q6600) sind das z.B. 32 Bytes.

Anhang: Lektüre
http://www.amazon.de/Computer-Architecture-A-Quantitative-Approach/dp/0123704901
 
Zuletzt bearbeitet:
@ice-breaker
Die Rechnung dauer momentan (hab N auf 5 000 000 000 gesetzt) etwa 15 Sekunden, für den singlethread etwa 4 Sekunden für den pthread. :)

Letzteres dank leboh.
Die Anwendung wurde wirklich deutlich schneller durch die lokale Variable, daran habe ich nicht gedacht, muss nachher noch schauen, ob das auch beim Monte-Carlo-Integral Ursache sein kann.

Was ausserdem jetzt klar ist, ist, dass die Zeitmessung mit clock_t und clock() wahrscheinlich nicht thread-safe ist, oder zumindest nicht vergleichbar mit singel-thread Anwendungen. Denn die Zeit, die mir die beiden Programme ausgeben sind quasi identisch - die Echtzeit aber (etwa) um einen Faktor 4 unterschiedlich.

Erklären würde ich mir das spontan dadurch, dass beim pthread-Programm die Takte (es werden ja afaik Takte gezählt) aller 4 Kerne addiert werden, aber vielleicht weiss da jemand mehr?

Ich schau jetzt noch den MC-Integral-Code an, danke.

edit @aphex.matze

Cachelines und Cachezeile sind aktuell für mich noch Bahnhof, aber ich werf nacher mal google and und schau mir das Buch an, auch dir vielen dank!
 
Zuletzt bearbeitet:
clock() ist generell schwierig für deine Zwecke zu benutzen und das hat viele Gründe (Einheit, frequency scaling, Portabilität, Auflösung, application vs. real time). Wenn du **nix progammierst, dann wirf' mal ein Auge auf gettimeofday().
 
clock() liefert die Prozessorzeit des Prozesses, das ist also erwartet dass da ungefaehr das selbe rauskommt, die Prozessorzeit ist proportional zur Anzahl der Cycles die er rechnet.
Was Du brauchst ist die Ellapsed Time or Wallclock Time, also die real verstrichene Zeit, denn die wird kuerzer.

#include <sys/time.h>
double second()
{
struct timeval t;
gettimeofday(&t, (struct timezone*)0);
return (double)(t.tv_sec + (double)t.tv_usec / 1000000.0);
}
Ergänzung ()

Und das empfohlene Buch sollte in keinem Regal fehlen ;-)
Ergänzung ()

Gut, beim Baecker braucht man es nicht, aber sonst ist es schon zu empfehlen.
 
Generelle Anmerkung, je nachdem wofuer Du das brauchst... ich wuerde mir Pthreads nicht freiwillig antun, die meisten Compiler koennen OpenMP, und das ist einfacher. Siehe www.openmp.org, da haette man das mit ein paar Kommentaren parallelisiert, den Fehler allerdings evtl trotzdem gemacht.
 
Das Buch suche ich mal in der Unibibliothek und wenns was taugt, werde ich wohl die 80 Euro oder so dafür bezahlen. Obwohl eigentlich zuerst einmal die Feynman-Lectures dran wären, bin eben eigentlich Physik-student, beschäftige mich aber aktuell mit C-Programmieren mit der Idee, dann grössere Simulationen machen zu können - N-Körperprobleme, Fluid-Dynamik usw., evtl. gibts auch eine Bachelorarbeit in Hochenergiephysik mit Assistenten vom Cern, da wäre es wichtig oder zumindest reizvoll, möglichst viel Leistung abrufen zu können (auch privat).

Darum, weil ich eben viel Leistung brauchen könnte, beschäftige ich mich aktuell mit Pthreads und Cuda, wobei ich versuche, einfache Beispiele zuerst auf einem Kern zu implementieren, dann parallelisieren für pthreads, und dann letzten Endes für cuda.

Ich dachte, pthreads sei der Standard für Multithreading-Anwendungen in C? Aber schau mir gerne auch openMP an.

speziell @leboh, danke für die Funktion second, funktioniert wunderbar und zeigt auch ausreichend genau die Zeit an für meine Zwecke.

Sehe ich das richtig, dass ich clock() dann benutzen kann, um den Overhead von pthread-Anwendungen zu analysieren? D.h. wenn der Overhead klein ist, sollte kaum ein Unterschied zwischen pthread- und singlethread-Anwendung bestehen?

edit: Ok, das Monte-Calo-Problem ist damit noch nicht gelöst, muss aber zuerst sowieso noch eine thread-safe random-Funktion schreiben, bevor ich das seriös angehen kann.(und jetzt kurz sport machen gehen)
 
Zuletzt bearbeitet:
Ich dachte, pthreads sei der Standard für Multithreading-Anwendungen in C? Aber schau mir gerne auch openMP an.
Ist er auch.

Wenn du es dir einfach machen willst legst du einfach für jeden Thread ein eigenes und nur exklusiv von ihm benutztes struct an was alle Argumente beinhaltet.

Also zB sowas:
Code:
// Parameter eines Threads
struct ThreadData{
  int  thread_number;
};

// Mehrfach gleichzeitig aufgerufene Funktion
void * thread_function(void *threadarg) {
  ThreadData *my_data = (ThreadData *) threadarg;
  printf("Hier ist Thread Nummer %d\n", my_data->thread_number);
}

pthread_t thread[Nthreads];  // Alle Threads
ThreadData args[Nthreads]; // Argumente fuer die Thread-Funktion

// Argumente vorbereiten
for (int i = 0; i < Nthreads ; ++i) {
  args[i].thread_number = i;
}

// Threads starten
for (int i = 0; i < Nthreads ; ++i) {
  pthread_create(&thread[i], NULL, thread_function, &arg[i]); 
}

// Threads joinen, damits danach erst weiter geht wenn alle fertig sind
for(int i = 0; i < Nthreads; ++i) {
  pthread_join(thread[i], NULL);
}

ohne garantie auf 100% richtigkeit..nur ma kurz hingehauen.
Da du bei Partikelfiltern / SMC Algorithmen keine Kommunikation zwischen den Threads brauchst und jeder eingenen exklusiven Speicher hat kann man das wunderbar über Kerne linear hochskalieren.

Trotzdem würde ich dir raten nicht zu viel Zeit in sowas zu stecken solange noch nicht 100% klar ist, dass du wirklich an Laufzeitgrenzen kommst. Manchmal tuns ja auch nur 25% der Partikel :) bzw wenn man seine Arbeitszeit in andere Dinge steckt erreicht man mehr.
 
Zuletzt bearbeitet:
@kudlmuddl

Danke, das weiss ich aber schon. Im eingangs erwähnten Monte-Carlo-Integral bekommt jeder Thread seine Anwesungen über einen struct, der dann im Thread selber gecastet wird, damit die Elemente verarbeitet werden können.

Mittlerweilen läuft auch alles befriedigend, zumindest im Minimalbeispiel ist jetzt fast ein Faktor 4 bei der benötigten Zeit, d.h. multithreading ist etwa 4 mal schneller + ein bisschen overhead, stört mich aber nicht gross.

Momentan sitze ich am Monte-Carlo-Integral und versuche, einen thread-sicheren Zufallszahlen-Generator zu implementieren, da rand() zwar thread-safe ist, aber durch mutex die anderen Threads blockiert und das scheinbar bereits ein Flaschenhals darstellt.

Das wird schon noch bis Mitternacht.

Und wegen der Zeit, die ich in das hineinstecke: Es sind Ferien, ich bilde mich privat auf meine Initiative weiter und selbst wenn ich es nicht brauche fürs Studium habe ich trotzdem einiges gelernt und ich lerne gern um des Wissens willen :D
 
Danke, das weiss ich aber schon.
Sah oben halt nicht so aus.. ist eine der naheliegendsten Fehlerquellen bei ersten Versuchen mit Parallelisierung fand ich. Aber gut :)

Momentan sitze ich am Monte-Carlo-Integral und versuche, einen thread-sicheren Zufallszahlen-Generator zu implementieren
Warum generierst du die Zufallszahlen in jedem Thread? Ich würde die Zufallszahlen vorher generieren. Evtl auch jedem Thread ne eigene Liste. Oder jedem Thread nen Pointer auf eine riesige Liste geben und nur den Startindex zufällig generieren ab wo die Zahlen aus der Liste rausgegriffen werden.
(Je nach Algo machts natürlich keinen Sinn wenn nacheinander jede Instanz irgendwann die selben Zufallszahlen verwendet.. manchmal geht das aber)

Das ist sowieso ein wichtiges Thema
Tabellierung > Parallelisierung.
Wenn du oft mathematische Funktionen aufrufst wie rand() oder sin() oder sqrt() oder was auch immer macht es häufig Sinn die Werte vorher zu erzeugen und nur auszulesen
Siehe auch: LUT

Wenn du jetzt argumentierst "ich kann nicht von jedem möglichen float/double die Wurzel/denSinus ... vorher ausrechnen und speichern!" könnte man überlegen ob man wirklich die volle Auflösung braucht und nicht zB 10000 Werte reichen (zB Das Intervall 0 bis 1 mit einer Auflösung von 0,0001) und dann einfach immer den dichtesten zu returnen. Kommt halt auf die Rechenzeit der Funktion an. Manchmal geht rechnen natürlich auch schneller als "aus dem Ram auslesen".
 
Zuletzt bearbeitet:
mahatma andi schrieb:
Momentan sitze ich am Monte-Carlo-Integral und versuche, einen thread-sicheren Zufallszahlen-Generator zu implementieren, da rand() zwar thread-safe ist, aber durch mutex die anderen Threads blockiert und das scheinbar bereits ein Flaschenhals darstellt.

wieso überhaupt threadsicher? Bau es doch so, dass die Zufallszahlen threadlokal berechnet werden, dann hast du den Aufwand nicht. Ein Zufallszahlengenerator besteht ja nur aus einer Funktion f(Zustand0) -> Zustand1 + Zufallszahl.
Also müsstest du nur den Zustand0 (Seed) an die Threads übergeben. Generell gilt: Umso mehr du Thread-Interaktionen vermeiden kannst, umso größer ist der Gewinn aus dem Multithreading.

Ein Mersenne Twister wäre dafür auch ziemlich einfach implementiert, bzw. könnte ich wetten, dass es auf Wikipedia bestimmt schon eine Version für C und Java gibt.
 
@kuddlmuddl
War auch ganz neutral gemeint, falls das anders rübergekommen ist.
Habe jetzt ein globales array erstellt mit N Elementen. Dieses Array wird, bevor die Threads gestartet werden, mit N (im Moment einer Milliarden) Elementen gefüllt und die Threads holen sich Zufallszallen. Habe noch jedem Thread einen Wert zugeordnet von 0 bis Anzahl Threads und dann darf jeder Thread einfach die Werte RandomArray[threadnummer + i*anzahl_threads] auslesen.

Funktioniert soweit ganz gut, abgesehen davon, dass das Arrayfüllen mit etwa 8 Sekunden am meisten Zeit braucht. :lol:
Ich schau mal, ob ich das clever Aufteilen kann, s.d. der Hauptthread (ich weiss, es gibt eigentlich keine Hierarchien) die ersten x-Millionen Elemente initialisiert, die anderen Threads startet und dann während die anderen die Zufallszahlen brauchen weitere in die Liste einträgt.

@ice-breaker

ok, threadsicher war das falsche Wort, ich wollte zuerst noch einen einfachen Algorithmus implementieren, der von jedem Thread einzeln und mit eigener Seed gestartet wird, hat aber (warum auch immer) nicht funktioniert.

Danke für den vielen Input, ich bin grad relativ enthusiastisch dabei, auch wenn ich evtl. langsam vorankomme :D
Ergänzung ()

ok.... so ein Array mit einer Milliarde Zufallszahlen belastet den Ram mit etwa 3Gb, denke kaum, dass ich da mit einer text-datie dieser Grösse, die ich auslese, schneller bin... (ahja, eine Milliarde mag etwas übertrieben scheinen, aber es geht mir ja um Optimierung, die kann man imho ruhig losgelöst von der Realität betrachten.
 
Zuletzt bearbeitet:
Du kannst ja erstmal mit einem total einfachen LFSR-PRNG anfangen. Beispielcode für C ist da sogar schon gegeben. Du erzeugst in deiner main()-Funktion für jeden pthread eine Zufallszahl und übergibst diese dem. So musst du nur 4 Zufallszahlen mit dem PRNG aus C generieren, und das im Main-Thread.
Diese eine Zufallszahl in jedem Thread nutzt du als Seed für deinen eigenen PRNG, und schwups, bist du fertig ;) Du musst keine Milliarden Zufallszahlen vorberechnen, und jeder Thread kann lokal für sich seine Zufallszahlen berechnen. Keinerlei Inter-Thread-Kommunikation, keine Mutexe, keine nichts.

Und wenn du das optimieren willst, musst du nur die Implementierung deines PRNG austauschen ;) Denn alle PRNG bauen auf dieses Konzept auf: du fütterst sie mit einem Initialwert (Seed) und sie spucken dir dann unendlich viele neue Zufallszahlen auf der Basis aus, irgendwann werden diese sich aber wiederholen (Periode). Ich bezweifel aber dass du die Periode des Mersenne Twister mit 2^19937 − 1 erreichst ^^
 
Habe mittlerweilen mein Arbeits-OS heruntergefahren und bin auf meinem Win7 Rechner, d.h. ich schau mir das Morgen an, jetzt noch ein wenig an anderen Projekten basteln.

Dankescheen
 
openmp ist einfacher, und ein Standard im HPC Umfeld.
Wuerde ich mir anschauen.

Ich bin kein Monte Carlo Fachmann, aber ich glaube mal ueber Zufallsgeneratoren von einem solchen gehoert zu haben dass man die nicht selber schreibt, sondern welche nimmt die Leute gemacht haben die sich mit sowas auskennen :-)
 
Ich schau mal, ob ich das clever Aufteilen kann, s.d. der Hauptthread (ich weiss, es gibt eigentlich keine Hierarchien) die ersten x-Millionen Elemente initialisiert, die anderen Threads startet und dann während die anderen die Zufallszahlen brauchen weitere in die Liste einträgt.
Sowas würd ich nich unbedingt machen.. das is ja dann wieder synchronisierung.
Gerad eine Stärke der Partikelfilter ist ja die gute Parallelisierbarkeit dank unabhängigkeit der Instanzen.
Wäre blöd wenn ein Rechenthread wartet dass neue Zahlen zur Verfügung stehen.

aber ich glaube mal ueber Zufallsgeneratoren von einem solchen gehoert zu haben dass man die nicht selber schreibt, sondern welche nimmt die Leute gemacht haben die sich mit sowas auskennen :-)
Insbesondere bei Sicherheitskritischen Anwendungen trifft dies absolut zu! Das Thema ist auch viel komplexer als das man da was brauchbares selbst Programmiert.
Ein wichtiger Unterschied ist aber meiner Meinung nach dass er keine besonders "guten" Zufallszahlen braucht. Klar, sie sollten über das Intervall gleichverteilt sein und sie sollten natürlich auch eine hohe Periode haben - aber andere Anforderungen wie "schwere Vorhersagbarkeit der nächsten Zahl", "unrekonstuierbarer seed" oder ähnliches ist total uninteressant. Wahrscheinlich ist aber auch der einfachste Zufallszahlengenerator der diese schwachen Anforderungen erfüllt langsamer als ein RAM-Zugriff für die vorher generierte Zahl.

Funktioniert soweit ganz gut, abgesehen davon, dass das Arrayfüllen mit etwa 8 Sekunden am meisten Zeit braucht.
Wie du auch schon geschrieben hattest: Generier doch einma ne ausreichende Zahl Zufallszahlen und importiere diese in Form einer Datei. Und dann wie du schon gemacht hast: Jeder Thread arbeitet auf einem Breich dieses Arrays oder bekommt ein eigenes. Das ist sicher die schnellste Lösung. Dann muss nur beim Programmstart die Zufallszahlenliste eingelesen wreden.. aber wenn man Probleme mit zu hoher Rechenzeit zur Laufzeit hat ist dies die optimale Lösung: Rechenaufwendinge Dinge nur 1x beim Programmstart durchführen und später auf die fertigen Ergebnisse zurückgreifen. Wie ich oben auch schon meinte zum Thema sin(), sqrt() etc ne LUT verwenden.
 
leboh schrieb:
aber ich glaube mal ueber Zufallsgeneratoren von einem solchen gehoert zu haben dass man die nicht selber schreibt, sondern welche nimmt die Leute gemacht haben die sich mit sowas auskennen :-)
Es geht dabei aber darum keine eigenen Algorithmen zu erfinden, denn das geht schief. Es gibt aber dutzende fertige inklusive Abhandlungen über deren Eigenschaften und welche PRNG-Tests bestanden werden (diehard usw.)
Man kann sich also durchaus einen dieser Algorithmen raussuchen und den meist Pseudocode in eine Programmiersprache übertragen, man sollte nur dabei keine Fehler machen...
Wenn der Autor nett war, dann gibt es noch 3-4 "Beispiele", also bei welchem Input welcher Output rauskommt, so dass die Wahrscheinlichkeit von Programmierfehlern beim Übertragen relativ gering ist.
 
Zurück
Oben