Parallel Test - threading - bad Performance, kein Speedup

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Benutzeravatar
ts7343
User
Beiträge: 69
Registriert: Mittwoch 5. Mai 2010, 13:48

Hallo liebe Python-Gemeinde,

ich versuche gerade grosse Textfiles entsprechend bestimmter Regularien zu splitten und wuerde es gerne parallel aufsetzen.
Dabei teste ich zur Zeit erst einmal die Grundfunktion im Memory und erleide leider dort schon schlechte Performance.
Ich hab den Code so klein wie moeglich auf das Wesentliche runtergebrochen. Die Grundidee nochmal kurz beschrieben:

Eine Liste a enthaelt bestimmte Werte und es soll geprueft werden, ob die Werte der Liste a entweder in der Liste b oder c enthalten sind.
Dafuer erzeuge ich ein zusaetzliches index array bzw eine index liste mit dem namen a_index_listing, die dann entweder
den Wert 1 (ist in Liste b) oder den Wert 2 (ist in Liste c) bekommt. Der Grundalgorithmus ist am besten in der Funktion:
"splitting_no_parallel" zu sehen.

Code: Alles auswählen

      for i in range(self.num_lines):
         id = self.list_a[i]
         if id in self.list_b:
            self.list_a_index[i] = int(1)
         if id in self.list_c:
            self.list_a_index[i] = int(2)            
Meine Frage lautet eigentlich:

Wieso ist der parallele Job nicht schneller als der nicht-parallele Job ?
Wenn ihr mit der groesse des jeweiligen arrays bzw liste variieren wollt, koennt ihr die Groesse aendern in:
self.num_lines = int(100000)

Habt ihr da Tipps, was ich machen kann, damit man einen Speedup im parallel Modus sieht?
Auch mit 4 oder 8 Processoren hab ich die gleichen Probleme.

Vielen Dank im voraus
lp


Code: Alles auswählen

#!/usr/bin/env python

import string, sys, datetime, threading, time


# ------------------------------------------------------------------------------


class se_threading(threading.Thread):


   def __init__(self, co, name="TestThread"): 
      self.co = co 
      threading.Thread.__init__(self, name=name) 


   def run(self): 
      self.co.splitting_parallel(self.getName())
   pass 


# ------------------------------------------------------------------------------


class parallel_test():


   def __init__(self):     
      self.thr_nproc        = int(2)
      self.num_lines        = int(100000)      
      self.list_a           = []
      self.list_a_index     = []
      self.list_b           = []
      self.list_c           = []
      

   def get_time_stamp(self):
      my_time = datetime.datetime.now()
      print my_time.strftime("%H:%M:%S")


   def prepare(self):   
      print "prepare list a, b, c ..."
      self.list_a           = self.num_lines*[0]
      self.list_a_index     = self.num_lines*[0]
      self.p_list_a_index_1 = self.num_lines/2*[0]
      self.p_list_a_index_2 = self.num_lines/2*[0]
      self.list_b           = self.num_lines/2*[0]
      self.list_c           = self.num_lines/2*[0]                           
      for i in range(self.num_lines):
         self.list_a[i] = i
      for i in range(self.num_lines/2):
         self.list_b[i] = i
         self.list_c[i] = i + self.num_lines/2
       
         
# ------------------------------------------------------------------------------
   
   
   def splitting_parallel(self, thread_id):
      start_id     = int(0)
      end_id       = int(0)      
      pct_counter  = int(0)
      pct_printed  = int(0)
      pct          = int(0)    
#     prepare local list and start_id and end_id
      if ( thread_id == "1" ):
         start_id = int(0)
         end_id   = self.num_lines/2-1
      if ( thread_id == "2" ):
         start_id = self.num_lines/2 
         end_id   = self.num_lines      
      self.get_time_stamp()
      print "start threading: ", thread_id   
      print "start_id, end_id: " + str(start_id) + ", " + str(end_id   )
#     go through the list        
      for i in range(start_id,end_id):
         id = self.list_a[i]
         if id in self.list_b:
            self.list_a_index[i] = int(1)
         if id in self.list_c:
            self.list_a_index[i] = int(2)
#        percent calculation
         pct_counter = pct_counter + 1
         pct = int( float(pct_counter)/float(len(self.list_a)/2)*100 )
         if ( (pct%20 == 0) and (pct > 0) and (pct_printed!=pct) ):
            print pct*"." + " - " + str(pct) + "%" + "   - " + thread_id
            pct_printed = pct


   def task_parallel(self):         
      print "parallel task ..."      
      ct1 = se_threading(self, "1")
      ct2 = se_threading(self, "2")         
      ct1.start()
      ct2.start()     
      ct1.join()
      ct2.join()


# ------------------------------------------------------------------------------


   def splitting_no_parallel(self, dummy):
      pct_counter  = int(0)
      pct_printed  = int(0)
      pct          = int(0)      
      print "splitting w/o parallel ..."   
      self.list_a_index     = self.num_lines*[0]
      for i in range(self.num_lines):
         id = self.list_a[i]
         if id in self.list_b:
            self.list_a_index[i] = int(1)
         if id in self.list_c:
            self.list_a_index[i] = int(2)            
#        percent calculation
         pct_counter = pct_counter + 1
         pct = int( float(pct_counter)/float(self.num_lines)*100 )
         if ( (pct%20 == 0) and (pct > 0) and (pct_printed!=pct) ):
            print pct*"." + " - " + str(pct) + "%" + "   - " + dummy
            pct_printed = pct
         
# ------------------------------------------------------------------------------


p = parallel_test()
p.get_time_stamp() 

# no parallel
p.prepare()
p.get_time_stamp()
p.splitting_no_parallel("0")
p.get_time_stamp() 

# parallel
p.prepare()
p.get_time_stamp()
p.task_parallel()
p.get_time_stamp() 



Dav1d
User
Beiträge: 1437
Registriert: Donnerstag 30. Juli 2009, 12:03
Kontaktdaten:

Verwende anstatt des Threading-Moduls besser das Multiprocessing-Modul, warum Threading in Python meistens langsamer ist als in C liegt am GIL, andere können dir das aber besser erklären.
the more they change the more they stay the same
BlackJack

@ts7343: Bevor man an Parallelisierung denkt weil's zu langsam ist, sollte man vielleicht erst einmal versuchen den Algorithmus zu überarbeiten. Das sieht mir nach ziemlich ungünstiger quadratischer Laufzeit aus, weil Du mit ``in`` auf Enthaltensein von Elementen in *Listen* prüfst. Da muss die ganze Liste Element für Element durchgegangen werden. Das ist ein ziemlich klarer Anwendungsfall für `set()` würde ich sagen.

Was sollen die ganzen sinnfreien `int()`-Aufrufe auf literalen Ganzzahlen eigentlich bewirken!? Das sieht insgesamt an einigen Stellen aus, als wolltest Du da eigentlich in einer anderen Programmiersprache schreiben. Zum Beispiel dieses Anlegen von Listen einer bestimmten Länge mit 0en um sie dann mit Werten zu füllen, ist nicht besonders "pythonisch". Da würde man eher Listen mit den Werten aufbauen in dem man die Werte an eine Liste anhängt. Oder bei einfachen Sachen "list comprehensions" verwendet. Füllen mit den Zahlen von 0 bis n-1 geht sogar noch einfacher: Schau doch mal was die `range()`-Funktion als Ergebnis liefert. Dann verstehst Du vielleicht warum das hier extrem umständlich und "unpythonisch" für Python-Programmierer aussieht:

Code: Alles auswählen

    self.list_a = self.num_lines * [0]
    for i in range(self.num_lines):
        self.list_a[i] = i
Neben der groben Zeitmessung wie lange ein bestimmter Code-Abschnitt braucht, kann man auch ein wenig Profiling betreiben um zu sehen wo am meisten Zeit verbraucht wird. Python bringt dafür in der Standardbibliothek schon Module mit.
BlackJack

Kleiner Nachtrag:

Code: Alles auswählen

14:49:10
prepare list a, b, c ...
14:49:10
splitting w/o parallel ...
.................... - 20%   - 0
........................................ - 40%   - 0
............................................................ - 60%   - 0
................................................................................ - 80%   - 0
.................................................................................................... - 100%   - 0
14:49:10
So sieht das aus wenn man für `list_b` und `list_c` statt Listen `set()`\s verwendet. Dann läuft die normale, nicht-parallelisierte Variante die ich vorher nach einer Minute abgebrochen hatte, als sie noch nicht einmal die ersten 20% berechnet hatte, in unter einer Sekunde.

Edit: zum Nachtrag. :-) Ich habe das ursprüngliche Programm mal durchlaufen lassen, das hat statt unter einer Sekunde, über fünf Minuten gebraucht.
Benutzeravatar
ts7343
User
Beiträge: 69
Registriert: Mittwoch 5. Mai 2010, 13:48

Hallo,

@Dav1d:
thanx for this hint, ich versuche gerade ein bisschen multiprocessing zu lesen, hoert sich gut an,
weiss nur noch nicht so richtig, wie ich das bei mir einbauen muss,
muss also erst mal wieder mit kleinen Beispielen anfangen, die multiprocessing benutzen, damit
ich mit der Materie vertrauter werde,

@BlackJack:
- das set() war mir als solches noch nicht so gelaeufig, aber du hast Recht, die Performance
ist brutal gegenueber meines Zweifach-Loop-Beispiels. Ich dachte, dass Python nicht unbedingt
einen Zweifach-Loop einbaut, wenn ich mit "in" das Enthaltensein von Listen ueberpruefe.
- das Verwenden der int() Aufrufe ist mehr der Ordnung halber, damit ich oben ein paar wichtige Variablen deklariere,
find ich irgendwie uebersichtlicher. Weiterhin hatte ich mal Typenprobleme, wenn ich davor nicht mit int() arbeitete, so
hat sich das eingebuergert.
- listname.append() verwende ich haeufig bei string Listen aber fuer index Listen bin ich da wohl von anderen Sprachen noch vorbelastet.
- wie kann man denn verstehen, das set() auf einmal so schneller ist, beide Varianten muessen doch irgendwie
den entsprechenden Wert suchen, oder?
Benutzeravatar
cofi
Python-Forum Veteran
Beiträge: 4432
Registriert: Sonntag 30. März 2008, 04:16
Wohnort: RGFybXN0YWR0

Ein Set arbeitet mit Hashes, deshalb laesst sich `in` in konstanter Zeit erledigen. Bei einer Liste (oder bei Arrays) hast du keine andere Moeglichkeit als den gesamten Datensatz durchzugehen, d.h. du hast im schlechtesten Fall immer `n` Vergleiche.
Bei Dictionary-Schluesseln ist es auch so, da ebenfalls Hashes benutzt werden.
BlackJack

@ts7343: Python baut keine "Zweifach-Loop" bei ``in`` sondern nur eine einfache Schleife über alle Elemente der Liste. Wie willst Du denn sonst prüfen ob ein Element in einer Liste enthalten ist? Da muss man halt alle der Reihe nach durchgehen und vergleichen bis man entweder einen Treffer gefunden hat, oder eben alle durchhat und weiss, dass das gesuchte Element nicht enthalten ist. Die zweite Schleife hast Du selbst drumherum gebaut. Beide hängen von der Anzahl der Zeilen (`self.num_lines`) ab und damit hast Du dort quadratische Laufzeit in Abhängigkeit von der Anzahl der Zeilen.

Das mit den `int()`-Aufrufen hat alle Anzeichen von "cargo cult programming".

In Python deklariert man keine Variablen. Das sind Definitionen und was an ``int(0)`` oder ``int(2)`` nun typsicherer oder übersichtlicher ist, verstehe ich nicht. Es wird viele Leser einfach nur verwirren und sie werden sich fragen was das soll und eventuell sogar Zeit drauf verwenden nach der Funktionsdefinition von `int()` zu suchen, denn die eingebaute `int()`-Funktion kann ja eigentlich nicht gemeint sein -- das wäre ja sinnlos.

Das Du überhaupt in Index-Listen denkst ist wohl noch eine Vorbelastung. Denn in den allermeisten Fällen kann man sich den Index in Python sparen. Ein Variablenname für den Index weniger und eine Indirektion in der Programmlogik weniger. Das macht Programme kürzer und einfacher.

`set()` verwendet eine Hashtabelle. Es muss also nur der Hashwert vom Objekt erfragt werden und dann kann in den allermeisten Fällen direkt darauf zugegriffen werden, ohne das man sich alle Objekte im `set()` anschauen muss.

Noch ein wenig mehr Kritik am Quelltext: PEP8 beschreibt einige Konventionen, die von vielen Python-Programmierern befolgt werden. Einiges ist wichtiger anderes nicht so, aber für die Zusammenarbeit -- und dazu gehört auch Quelltext in Foren zu posten, damit andere ihn mal ausprobieren oder verbessern können -- ist Einrückung mit 4 Leerzeichen pro Ebene *ziemlich* wichtig. Wenn jemand Tabs verwendet können die meisten Editoren das noch einfach umwandeln, aber bei drei Leerzeichen pro Ebene gibt der eine oder andere Editor oder Benutzer dann doch auf.

Bei der Namensgebung kann man über vieles reden, aber über Klassennamen sind sich eigentlich alle einig: MixedCase. Damit man weiss wovon man erben kann und damit man immer ohne nachzudenken zu müssen einen allgemeinen Namen für ein Exemplar so eines Typs hat: ``some_thing = SomeThing()``. Das ginge nicht, wenn die Klasse schon `some_thing` hiesse.

Die `se_threading`-Klasse ist zu umständlich wenn sie einfach nur dazu dient eine Funktionen oder Methoden asynchron auszuführen. Das kann man auch nur mit der `Thread`-Klasse machen.

Code: Alles auswählen

    def run_tasks_parallel(self):
        print "parallel task ..."
        threads = [Thread(target=self.splitting_parallel, args=[x], name=x)
                   for x in '12']
        for thread in threads:
            thread.start()
        for thread in threads:
            thread.join()
Abkürzungen sind Sch…. Was soll zum Beispiel `thr_nproc` bedeuten? Wenn ich raten müsste `thr` für "Thread" und `nproc` für "Anzahl" und "Processes"? Oder "Processors"? Und ist "Thread" *und* "Processes" nicht etwas zuviel des Guten in einem Namen? `thread_count` ist IMHO viel lesbarer und verständlicher. Das Attribut wird aber überhaupt nicht benutzt!?

`get_time_stamp()` macht erstens nicht das was der Name suggeriert und ist zweitens keine Methode.

`prepare()` zeigt deutlich, dass Du von dem "Indexdenken" wegkommen solltest und versuchen solltest ohne Schleifen auszukommen, wo es Möglichkeiten gibt das kompakter auszudrücken.

Code: Alles auswählen

    def prepare(self):
        print "prepare a, b, c ..."
        self.list_a = range(self.num_lines)
        self.list_a_index = list()
        mid_index = self.num_lines // 2
        self.list_b = set(self.list_a[:mid_index])
        self.list_c = set(self.list_a[mid_index:])
Bei `splitting_parallel()` hast Du glaube ich einen Indexfehler drin, weil Du bei ``thread_id == '1'`` eins abziehst. Bei `range()` und auch `xrange()` ist der Endpunkt ja aber nicht mehr mit enthalten, da hast Du also sozusagen schon ein -1 implizit drin. Die Methode enthält ausserdem sehr viel kopierten Quelltext von `splitting_no_parallel()`, das ist unschön. So etwas tendiert früher oder später dazu voneinander abzuweichen und man weiss nicht mehr ob die beiden überhaupt noch exact das gleiche Berechnen. In `splitting_no_parallel()` wird zum Beispiel `self.list_a_index` for der Schleife neu initialisiert, in `splitting_parallel()` ist das nicht der Fall. Das macht solange keinen Unterschied wie niemand vergisst zwischen Aufrufen `prepare()` aufzurufen. Sollte man das aber mal nicht machen, kann sich das Ergebnis von den beiden Methoden unterscheiden.

In der Methode die die Verarbeitung parallel erledigt, anhand des Thread-Namens zu entscheiden welcher Teil der Daten bearbeitet werden soll ist auch äusserst unflexibel. *Eine* `splitting()`-Methode die im linearen Fall mit allen Daten und im Parallelen mehrfach mit Teilen der Daten gestartet wird, wäre vom Entwurf her besser. Die könnte zum Beispiel so aussehen:

Code: Alles auswählen

    def splitting(self, lines, thread_name):
        percent_printed = 0
        print "splitting w/o parallel ..."
        result = list()
        for i, id in enumerate(lines):
            if id in self.list_c:
                index = 2
            elif id in self.list_b:
                index = 1
            else:
                index = 0
            result.append(index)

            percent = 100 * (i + 1) / self.num_lines
            if percent % 20 == 0 and percent > 0 and percent_printed != percent:
                print percent * "." + " - %d%%  - %s" % (percent, thread_name)
                percent_printed = percent
        
        return result
Benutzeravatar
ts7343
User
Beiträge: 69
Registriert: Mittwoch 5. Mai 2010, 13:48

Hallo BlackJack,

vielen Dank erst einmal fuer diesen ausfuehrlichen Post und fuer das ausfuerhliche Anschauen der Details,
da stecken viele viele interessante Sachen drin, welche ich im einzelnen
auch erstmal ausprobieren muss, einige Stellen in deinen optimierten Zeilen kenne ich leider noch nicht so bzw. hab sie zwar
schon mal gesehen aber mich bis jetzt verweigert sie zu benutzen, z.B.:

Code: Alles auswählen

print percent * "." + " - %d%%  - %s" % (percent, thread_name)
war mir immer zu skript-lastig, da hab ich lieber noch eine Zeile draufgepackt um es irgendwie lesbarer zu gestalten.


Bzgl. des Zweifach-Loops meinte ich schon, dass ich unwissenderweise einen Zweifach-Loop eingebaut hatte, also einen direkt und der
andere ueber das "in" fuer die Abfrage der Liste. Ich hatte echt geglaubt, dass Python da selbst etwas optimiert, wenn ich nur
eine "in"-Abfrage mache und keinen eigenen Search-Loop aufsetze. Das Thema um die Hashes muss ich mir erst anlesen, kenne den
Begriff eher aus Perl ...

Das mit "cargo cult programming" ist lustig, vielen Dank fuer deine Offenheit und es stimmt, ich bin schon recht vorbelastet, mein Weg
ist ungefaehr folgender:
- Basic
- FORTRAN77
- Turbo Pascal
- Visual Basic
- PCL (Patran Command Language) und DMAP (Direct Matrix Abstraction Program)
- Python

Das mit der int()-Anweisung ist mir unter HPUX 11.11 mit Python 2.6.x passiert, dass ich eine predefinition hatte:

Code: Alles auswählen

i_01 = 1
if (i_01 == 1): 
   i_01 = 2
und er die Anweisung nicht als Integer nicht gesehen hatte und somit nicht in die if-Anweisung ging,
seitdem nehm ich halt die Brechstange und deklariere immer ueber int(), und ich hatte nie wieder
derartige Probleme, frei nach dem Motto: never change a running system,
aber wenn du meinst es geht auch so, werd ich mal langsam versuchen zurueck zur Normalitaet zu gehen.
Antworten