concurrent.futures - Frage(n) zur Performance

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
noisefloor
User
Beiträge: 3856
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,

ich hatte neulich folgenden "Problemstellung": gegeben ist ein SHA384 Hash-Wert, welcher sich aus einer bestimmten Geokoordinate berechnet. Per "Brute Force" soll nun aus einem Bereich von Koordinaten (in dem die Zielkoordinaten auf jeden Fall liegen die Zielkoordinaten bestimmt werden, in dem man für jede mögliche Koordinate den SHA384 Hash berechnet und mit dem Zielhash vergleich.

Simpel linear runter geschrieben sieht das so aus:

Code: Alles auswählen

import hashlib
import sys
import time

# Sollwert für die Zielkoordinaten
HashSoll = '6b80992d3cd3c975d1b14fa87a7f91bdbd6a13ed10692463965b61dd51bc6b5fc86e5c72cbbc62537ced4f9d035740f7'

# Struktur der Zielkoords  : N54° AA.XXX' E013° BB.XXX'
# Zu testende Wertebereiche :
#     AA:  21 – 26
#     BB:  28 – 35
#     XXX: 000 - 999
start = time.perf_counter()
for i in range(21,27,1):
    for j in range (28,36,1):
       for i_min in range (0,1000,1):
          for j_min in range (0,1000,1):
               HashTest = "N54° {}.{:0>3}' E013° {}.{:0>3}'".format(i, i_min, j, j_min)
               HashIst = hashlib.sha384(HashTest.encode('utf-8')).hexdigest()
               if HashIst == HashSoll:
                  print("Match für Wert <{}> bei HashWert".format(HashTest))
                  print(HashIst)
                  ende = time.perf_counter()
                  print("Laufzeit {:4.2f} Sekunden".format(ende - start))
                  sys.exit()
ende = time.perf_counter()
print("Laufzeit {:4.2f} Sekunden".format(ende-start))
print ("Kein Match, mit Soll = <{}>)".format(HashSoll))
Laufzeit bei mir auf meinem Intel® Core™ i5-5200U CPU @ 2.20GHz: ca. 70 Sekunden
Python-Version: 3.5.2 (falls es für jemanden wichtig ist) auf Ubuntu 16.04.

(Anm.: äquivalenter Code in Go braucht ca. 60 Sekunden bis zum Ergebnis, als 10 Sekunden weniger)

Jetzt kann man das ganze ja parallelisieren, indem man die Koordinatenbereich aufteilt und parallel berechnen lässt:

Code: Alles auswählen

import hashlib
import time
import concurrent.futures

RANGES = (((21, 22), (28, 31)),
          ((21, 22), (32, 35)),
          ((23, 24), (28, 31)),
          ((23, 24), (32, 35)),
          ((25, 26), (28, 31)),
          ((25, 26), (32, 35)))

def workhorse(args):
    # Struktur der Zielkoords  : N54° AA.XXX' E013° BB.XXX'
    # Zu testende Wertebereiche :
    #     AA:  21 – 26
    #     BB:  28 – 35
    #     XXX: 000 - 999
    target_hash = '6b80992d3cd3c975d1b14fa87a7f91bdbd6a13ed10692463965b61dd51bc6b5fc86e5c72cbbc62537ced4f9d035740f7'
    for i in range(args[0][0], args[0][1]+1):
        for j in range (args[1][0], args[1][1]+1):
            for i_min in range (0,1000,1):
                for j_min in range (0,1000,1):
                    coords = "N54° {}.{:0>3}' E013° {}.{:0>3}'".format(i, i_min, j, j_min)
                    coords_hash = hashlib.sha384(coords.encode('utf-8')).hexdigest()
                    if coords_hash == target_hash:
                        return  coords

start = time.perf_counter()
pool = concurrent.futures.ProcessPoolExecutor(max_workers=4)
futures = [pool.submit(workhorse, ranges) for ranges in RANGES]
if concurrent.futures.wait(futures, 
                           return_when=concurrent.futures.FIRST_COMPLETED):
    found = time.perf_counter()
    print('found it, shuting down remaining workers...')
    pool.shutdown()
    print('finished')
for future in futures:
    res = future.result()
    if res:
        print(res)
finished = time.perf_counter()
print(found-start, finished-start)
Laufzeit: ca. 55 Sekunden, egal ob 2, 3 oder 4 Worker. Bei 4 Workern wird das Ergebnis nach ca 23 Sekunden gefunden, der Rest ist Warten auf das Ende der anderen Worker.
Jedenfalls ist das ca. 15 Sekunden schneller als der nicht-parallele Ansatz.

Wenn ich jetzt den ThreadPoolExecutor nutze, dauert das ganze _deutlich_ länger, nämlich bei 4 Workern ca. 115 Sekunden Und damit ca. 45 Sekunden länger als der Nicht-lineare Ansatz und doppelt so lange wie der ProcessPoolExecutor.

Frage: woran liegt das? Meine Vermutung ist, dass die Prozesse auf die verfügbaren Kerne verteilt werden, die Threads aber ggf. nur auf einem Kern laufen und sich dadurch ausbremsen?

Sonstige Kommentare sind natürlich auch willkommen, besonders wenn jemand eine Idee für eine bessere Performance hat.

Gruß, noisefloor
Benutzeravatar
kbr
User
Beiträge: 1487
Registriert: Mittwoch 15. Oktober 2008, 09:27

@noisefloor: Deine Vermutung ist richtig: innerhalb eines Python-Prozesses laufen Threads nicht parallel.
Sirius3
User
Beiträge: 17749
Registriert: Sonntag 21. Oktober 2012, 17:20

@noisefloor: das Stichwort heißt GIL. Deine Aufteilung ist seltsam. Warum teilst Du die erste und die zweite Schleife in mehrere Teile auf, wo Du doch nur die erste Schleife in 6 Teile teilen könntest.
Am besten machst Du Dir gar keine Gedanken über die Aufteilung und nutzt pool.map zusammen mit itertools.product, dann kannst Du die Schleife auch gleich abbrechen, wenn Du ein Ergebnis gefunden hast.

Code: Alles auswählen

import itertools
import hashlib
import time
import concurrent.futures

TARGET_HASH = '6b80992d3cd3c975d1b14fa87a7f91bdbd6a13ed10692463965b61dd51bc6b5fc86e5c72cbbc62537ced4f9d035740f7' 

def workhorse(ij):
    # Struktur der Zielkoords  : N54° AA.XXX' E013° BB.XXX'
    # Zu testende Wertebereiche :
    #     AA:  21 – 26
    #     BB:  28 – 35
    #     XXX: 000 - 999
    i, j = ij
    for i_min in range (0,1000,1):
        for j_min in range (0,1000,1):
            coords = "N54° {}.{:0>3}' E013° {}.{:0>3}'".format(i, i_min, j, j_min)
            coords_hash = hashlib.sha384(coords.encode('utf-8')).hexdigest()
            if coords_hash == TARGET_HASH:
                return coords
    return None
 
start = time.perf_counter()
pool = concurrent.futures.ProcessPoolExecutor()
for result in pool.map(workhorse, itertools.product(range(21,27), range(28,36))):
    if result:
        break
print(result)
finished = time.perf_counter()
print(finished-start)
Benutzeravatar
noisefloor
User
Beiträge: 3856
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,
Deine Aufteilung ist seltsam.
Sagen wir mal so: die Aufteilung war nicht "optimiert" ;-) Ich hatte die Problemstellung einfach mal zum Anlassen genommen, mich nochmal mit `concurrent.futures` zu beschäftigen, weil ich da sonst so keine Notwendigkeit zu habe.

Die Lösung mit dem `map` und der Aufteilung itertools ist in der Tat performanter - Laufzeit bei mir ca. 36 Sekunden, also 20 Sekunden weniger als mein erster `concurrent.futures` Ansatz und die Hälfte des linearen Ansatzes. Danke für den Code :-)
das Stichwort heißt GIL
Stimmt, da war ja was in der Richtung....

Was dann bei mir aber die Frage aufwirft: Macht der ThreadPoolExecutor bei CPU-lastigen Sachen wie diesem Beispiel überhaupt Sinn? IMHO nein, oder übersehe ich das was? Die gegebene Problemstellung ist ja letztendlich eine Frage der Rechenleistung. Je schneller der Computer die SHA384-Hashs berechnet, desto schneller ist das Programm fertig.

Gruß, noisefloor
BlackJack

@noisefloor: Bei CPython macht der `ThreadPoolExecutor` bei CPU-lastigen Aufgaben im Moment keinen Sinn. Das ändert sich wenn CPython vielleicht mal das GIL los wird, oder wenn man eine andere Python-Implementierung verwendet die kein GIL verwendet. Jython zum Beispiel. Da habe ich den `concurrent.futures`-Backport allerdings noch nicht ausprobiert.
Benutzeravatar
BigZ
User
Beiträge: 30
Registriert: Mittwoch 1. Juli 2015, 21:18
Wohnort: Hamburg
Kontaktdaten:

Ich erhalte mit multiprocessing, ein bisschen herum experimentieren und missachtung jeglicher Python Code Styling Empfehlungen mit folgendem Schnipsel eine durchaus annehmbare Performace steigerung.
Btw. wieso erhalte ich mit code=python kein Python Styling in der Code box? x(

Code: Alles auswählen

#! -*- coding: utf-8 -*-
import hashlib
import time
import multiprocessing
RANGES = (
    (
        (21, 22), (28, 31)
    ),
    (
        (21, 22), (32, 35)
    ),
    (
        (23, 24), (28, 31)
    ),
    (
        (23, 24), (32, 35)
    ),
    (
        (25, 26), (28, 31)
    ),
    (
        (25, 26), (32, 35)
    )
)
TARGET_HASH = '6b80992d3cd3c975d1b14fa87a7f91bdbd6a13ed10692463965b61dd51bc6b5fc86e5c72cbbc62537ced4f9d035740f7'


def workhorse(ij):
    # Struktur der Zielkoords  : N54° AA.XXX' E013° BB.XXX'
    # Zu testende Wertebereiche :
    #     AA:  21 – 26
    #     BB:  28 – 35
    #     XXX: 000 - 999
    start = time.perf_counter()

    i, j = ij
    for i_min in range (0,1000,1):
        for j_min in range (0,1000,1):
            check1 = hashlib.sha384("N54° {}.{:0>3}' E013° {}.{:0>3}'".format(i[0], i_min, j[0], j_min).encode('utf-8')).hexdigest()
            check2 = hashlib.sha384("N54° {}.{:0>3}' E013° {}.{:0>3}'".format(i[0], i_min, j[1], j_min).encode('utf-8')).hexdigest()
            check3 = hashlib.sha384("N54° {}.{:0>3}' E013° {}.{:0>3}'".format(i[1], i_min, j[0], j_min).encode('utf-8')).hexdigest()
            check4 = hashlib.sha384("N54° {}.{:0>3}' E013° {}.{:0>3}'".format(i[1], i_min, j[1], j_min).encode('utf-8')).hexdigest()
            if check1 == TARGET_HASH or \
               check2 == TARGET_HASH or \
               check3 == TARGET_HASH or \
               check4 == TARGET_HASH:
                if check1 == TARGET_HASH:
                    lo, la = i[0], j[0]
                elif check2 == TARGET_HASH:
                    lo, la = i[0], j[1]
                elif check3 == TARGET_HASH:
                    lo, la = i[1], j[0]
                else:
                    lo, la = i[1], j[1]
                print("N54° {}.{:0>3}' E013° {}.{:0>3}'".format(lo, i_min, la, j_min))
                ende = time.perf_counter()
                print("Laufzeit {:4.2f} Sekunden".format(ende-start))
    return None


if __name__ == '__main__':
    jobs = [multiprocessing.Process(target=workhorse, args=(RANGE,)).start() for RANGE in RANGES]

Edit sagt ich bin ein Noob und finde nur heraus das eins von 4 möglichen Ergebnissen das Richte ist... Bild *Zurück ans Zeichenbrett setz*

Edit² sagt läuft
Zuletzt geändert von BigZ am Samstag 3. Juni 2017, 12:45, insgesamt 2-mal geändert.
"Ist noch λ?"
"Ja, aber das ϕ ist noch ϱ"
BlackJack

@BigZ: code kann nix, ausser PHP, also so gut wie nix. Du musst codebox verwenden, das kann alles was in der Dropdownbox auswählbar ist, unter anderem also auch Python.
Benutzeravatar
BigZ
User
Beiträge: 30
Registriert: Mittwoch 1. Juli 2015, 21:18
Wohnort: Hamburg
Kontaktdaten:

Ich bin auch manchmal wirklich blind... -.-
Jetzt sehe ich sie auch D:
Danke! <3

Edit sagt:
Problem ist weniger die Hashes berechnen, sondern das viele Schleifen unperformant sind.
Wie in meinem Beispiel, siehst du das mit jedem Durchlauf der Hash 4 mal berechnet wird, dafür aber weniger Durchläufe insgesamt gebraucht werden,
was dazu führt das ich aufm meinem i3 gerade mal 30 Sekunden brauche, wo ich vorher 60 - 70 Sekunden gebraucht hab,
je nach Implementierung.
Ich weiß jetzt nicht ob concurrent einen großen Unterschied machen würde, das habe ich noch nicht ausprobiert, aber beim Bruteforcen mit Python gilt wohl lieber mehr checks pro Durchlauf als alle hinter einander :D

Edit² sagt:
Das wäre mal eine interessante Aufgabe für Stackoverflow.
Würde mich mal interessieren ob man das noch performanter machen kann Bild
"Ist noch λ?"
"Ja, aber das ϕ ist noch ϱ"
Benutzeravatar
noisefloor
User
Beiträge: 3856
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,
Das wäre mal eine interessante Aufgabe für Stackoverflow.
Kannst du gerne da Posten. Das ist zwar die Lösung für einen Rätsel-Geocache, aber um an den gezeigten Hash zu kommen muss man erst noch ein anderes Rätsel lösen. Von daher: no worries.

Und wer zu faul ist, so Rätsel zu lösen, der sucht auch nicht im Python-Forum oder SO, sondern besorgt sich aus dubiosen (Facebook-) Quellen fertig Listen mit gelösten Rätselcaches.

Übrigens: 30 Sekunden auf einem i3 ist gut - bei einem Bekannten braucht ein linearer Ansatz sehr ähnlich meinem ca. 3 Minuten.

Gruß, noisefloor
Sirius3
User
Beiträge: 17749
Registriert: Sonntag 21. Oktober 2012, 17:20

@BigZ: dass Dein neues Programm doppelt so schnell läuft, wie davor, liegt daran, dass Du nur halb so viele Fälle abprüfst. Statt eine Schleife von 28 bis 31, prüfst Du nur 28 und 31, so dass 29 und 30 fehlen. Dass die gesuchte Lösung zufällig zu den 50% geprüften Koordinaten gehört, ist Pech, denn so hat ein fehlerhaftes Programm Dich zu der Annahme verleitet, umständliches Kopieren von Code würde die Performance deutlich erhöhen.
Benutzeravatar
BigZ
User
Beiträge: 30
Registriert: Mittwoch 1. Juli 2015, 21:18
Wohnort: Hamburg
Kontaktdaten:

Ah, gut das du mich drauf aufmerksam machst, du hast vollkommen recht Bild
Ich musste natürlich mich mal wieder voreilig auf Stackoverflow blamieren mit meinem blöden Beispiel x(
Bin jetzt aber zu faul und zu hungrig mir das alles nochmal genauer anzuschauen.
/me setzt sich zurück ans Zeichenbrett und macht erstmal Mittag... *grmlgrmlgrmlhmpfgrmlärger*

Edit:
Bügel doch nochmal mein Mist vorher aus.

Code: Alles auswählen

#! -*- coding: utf-8 -*-
import hashlib
import time
import multiprocessing
RANGES = (
    (
        (21, 22), (28, 29)
    ),
    (
        (21, 22), (30, 31)
    ),
    (
        (21, 22), (32, 33)
    ),
    (
        (21, 22), (34, 35)
    ),
    (
        (23, 24), (28, 29)
    ),
    (
        (23, 24), (30, 31)
    ),
    (
        (23, 24), (32, 33)
    ),
    (
        (23, 24), (34, 35)
    ),
    (
        (25, 26), (28, 29)
    ),
    (
        (25, 26), (30, 31)
    ),
    (
        (25, 26), (32, 33)
    ),
    (
        (25, 26), (34, 35)
    ),
)
TARGET_HASH = '6b80992d3cd3c975d1b14fa87a7f91bdbd6a13ed10692463965b61dd51bc6b5fc86e5c72cbbc62537ced4f9d035740f7'


def workhorse(ij):
    # Struktur der Zielkoords  : N54° AA.XXX' E013° BB.XXX'
    # Zu testende Wertebereiche :
    #     AA:  21 – 26
    #     BB:  28 – 35
    #     XXX: 000 - 999
    start = time.perf_counter()

    i, j = ij
    for i_min in range (0,1000,1):
        for j_min in range (0,1000,1):
            check1 = hashlib.sha384("N54° {}.{:0>3}' E013° {}.{:0>3}'".format(i[0], i_min, j[0], j_min).encode('utf-8')).hexdigest()
            check2 = hashlib.sha384("N54° {}.{:0>3}' E013° {}.{:0>3}'".format(i[0], i_min, j[1], j_min).encode('utf-8')).hexdigest()
            check3 = hashlib.sha384("N54° {}.{:0>3}' E013° {}.{:0>3}'".format(i[1], i_min, j[0], j_min).encode('utf-8')).hexdigest()
            check4 = hashlib.sha384("N54° {}.{:0>3}' E013° {}.{:0>3}'".format(i[1], i_min, j[1], j_min).encode('utf-8')).hexdigest()
            if check1 == TARGET_HASH or \
               check2 == TARGET_HASH or \
               check3 == TARGET_HASH or \
               check4 == TARGET_HASH:
                if check1 == TARGET_HASH:
                    lo, la = i[0], j[0]
                elif check2 == TARGET_HASH:
                    lo, la = i[0], j[1]
                elif check3 == TARGET_HASH:
                    lo, la = i[1], j[0]
                else:
                    lo, la = i[1], j[1]
                print("N54° {}.{:0>3}' E013° {}.{:0>3}'".format(lo, i_min, la, j_min))
                ende = time.perf_counter()
                print("Laufzeit {:4.2f} Sekunden".format(ende-start))
    return None


if __name__ == '__main__':
    jobs = [multiprocessing.Process(target=workhorse, args=(RANGE,)).start() for RANGE in RANGES]
Jetzt brauchts auch nur noch 90 Sekunden Bild
"Ist noch λ?"
"Ja, aber das ϕ ist noch ϱ"
Antworten