Przyśpieszenie pętli przy dostepie do słownika w każdej iteracji

Przyśpieszenie pętli przy dostepie do słownika w każdej iteracji
AN
  • Rejestracja:prawie 11 lat
  • Ostatnio:około 12 godzin
  • Postów:973
0

Ma ktoś jakieś pomysły jak przyśpieszyć podany kod wkluczając następujące możliwości:

  1. list comprehension
  2. pre alokacja listy
  3. deque
  4. pisanie tego w innym języku - chodzi mi tutaj o 100% Pythona

Przy założeniach, że liczba elementów w słowniku jest dynamiczna (podawana przez użytkownika) wiec optymalizacja typu
a_list = data["a"] i potem odwoływanie się do a_list (żeby unikać dostępu przez słownik) również nie jest możliwa (chyba, że jest sposób, żeby to zrobić dynamicznie)

Kolejne założenie to, że dane są też dynamiczne, a "string" i 123 jest dla uproszczenia, w rzeczywistości to są różne dane

Kopiuj
import time
start_time = time.perf_counter()

data = {
    "a": [],
    "b": [],
}
for i in range(10_000_000):
    data["a"].append("string")
    data["b"].append(123)

print("It took: ", time.perf_counter() - start_time)

Zdalna praca dla Senior Python Developerów --> PW
edytowany 2x, ostatnio: anonimowy
Patryk27
Moderator
  • Rejestracja:ponad 17 lat
  • Ostatnio:ponad rok
  • Lokalizacja:Wrocław
  • Postów:13042
3

W podanym przez Ciebie przykładzie zdaje się, że większość czasu powinnno zejść nie na dostęp do słownika, a na uruchomienie .append() - skąd zatem pomysł aby optymalizować akurat dostęp do słownika?

(tzn. moje przypuszczenie jest takie, że nawet gdyby dostęp do słownika zajmował 0 ns, nie wpłynęłoby to znacząco na ogólny czas działania Twojego programu, bo większość czasu spędza on na .append())


AN
  • Rejestracja:prawie 11 lat
  • Ostatnio:około 12 godzin
  • Postów:973
0

Co tu jest bottleneckiem to już nie ma znaczenia tak naprawdę. Bo chodzi mi po prostu o przyśpieszenie tego, a dostęp do słownika pozwala na przyśpieszenie tego o około 10-20%. Tutaj szukam każdej możliwej optymalizacji bo jest to core aplikacji i podobna pętla jest uruchamiana biliony razy.
Można nawet zrobić a_append = data["a"].append i to też przyśpiesza, problem mam taki, że te dane są dynamiczne, więc nie wiem ile zmiennych zrobić za wczasu


Zdalna praca dla Senior Python Developerów --> PW
YA
  • Rejestracja:prawie 10 lat
  • Ostatnio:około 14 godzin
  • Postów:2367
6

Co tu jest bottleneckiem to już nie ma znaczenia tak naprawdę.

No raczej ma znaczenie, bo trzeba ten bottleneck usunąć. A tak stawiasz zadanie "Przyspieszyć kody, który mam, ale nie pokażę go, pokaże coś innego co myślę, że jest takie samo".
Ciężko zgadywać jaki problem ten kod ma rozwiązywać.

W pythonie przerabiałem temat wczytywania małych danych (góra kilkaset mega) na różne sposoby:

  • Pandas, Numpy, Dask, ...
  • ładowanie bezpośrednio do bazy in memory (testowałem sqallite3, duckdb, monetdb, ... )
  • cythonizacja
  • wydzielenie funkcjonalności do biblioteki w C przykrytej pythonem

Generalnie python był zbyt wolny na moje potrzeby, a powodem bottlenecku była dynamiczna natura pythona i to w jaki sposób obsługuje typy danych, w szczególności sprawdzanie typów i konwersja danych zachodząca na styku pythona i warstw pośrednich (czy to konwersja do wywołania biblioteki C, czy wstawianie danych do bazy in-memory).

Chcesz pisać kod szybko -> python jest dobrym wyborem.
Chcesz żeby działało szybko -> python nie jest dobrym wyborem.

AN
  • Rejestracja:prawie 11 lat
  • Ostatnio:około 12 godzin
  • Postów:973
0

No dobrze, nie musisz o tym pisać bo to jest oczywiste. Wiem jak rozwiązuje się problemy z wydajnością i wszystko co mogłem już zrobiłem. To jest już ostateczny kod, do przyśpieszenia bo reszta już zoptymalizowana.

Python jest nie do wymiany z tego powodu, że te dane potem są wykorzystywane w Pythonie i ładowanie ich zajęło by więcej czasu niż ta pętla.

Zadałem konkretne pytanie o konkretny mały fragment kodu i nie potrzebuję oczywistości w stylu "gdzie jest bottleneck", "python nie jest szybki" itd. Owszem można to napisać komuś początkującemu ale nie komuś kto wyraźnie zaznaczył, które sposoby odpadają


Zdalna praca dla Senior Python Developerów --> PW
AN
  • Rejestracja:prawie 11 lat
  • Ostatnio:około 12 godzin
  • Postów:973
0

Znalazłem jedną optymalizację o 10% (dla przypadku z tematu, dla realnych danych może się różnić):
Dzięki nie odwoływaniu się do methody "append" za pomocą data["a"].append() tylko data["a_append"]() zyskujemy czas. I o tego typu zabawy mi chodzi w temacie

Kopiuj
import time
start_time = time.perf_counter()

data = {
    "a": [],
    "b": [],

}
data["a_append"] = data["a"].append
data["b_append"] = data["b"].append

for _ in range(10_000_000):
    data["a_append"]("string")
    data["b_append"](123)

print("It took: ", time.perf_counter() - start_time)

Zdalna praca dla Senior Python Developerów --> PW
YA
  • Rejestracja:prawie 10 lat
  • Ostatnio:około 14 godzin
  • Postów:2367
3
anonimowy napisał(a):

No dobrze, nie musisz o tym pisać bo to jest oczywiste. Wiem jak rozwiązuje się problemy z wydajnością i wszystko co mogłem już zrobiłem. To jest już ostateczny kod, do przyśpieszenia bo reszta już zoptymalizowana.

Dzielę się tylko swoimi doświadczeniami. Skoro wyczerpałeś możliwości na poziomie kodu, to pozostaje zmiana architektury, wszak rozwiązania nie kończą się na kodzie.
Nie obraź się, ale używanie time.perf_counter() pokazuje, że skupiasz się na czasie wykonania bloku kodu, co jest (moim zdaniem) bardzo słabym podejściem, przy takich mikro optymalizacjach.

Skoro ten problem z wydajnością występuje, to naturlane pytanie: O ile % chciałbyś poprawić (jakie są kryteria "sukcesu przyspieszania pętli")?

Python jest nie do wymiany z tego powodu, że te dane potem są wykorzystywane w Pythonie i ładowanie ich zajęło by więcej czasu niż ta pętla.

No ale co stoi na przeszkodzie by wydzielić tę krytyczną funkcjonalność do osobnego komponentu, z którym mógłbyś interfejsować z poziomu pythona?

Zadałem konkretne pytanie o konkretny mały fragment kodu i nie potrzebuję oczywistości w stylu "gdzie jest bottleneck", "python nie jest szybki" itd. Owszem można to napisać komuś początkującemu ale nie komuś kto wyraźnie zaznaczył, które sposoby odpadają

Jeśli jest to realny problem wydajnościowy, to odrzucanie x10 w innej technologii na rzecz +10% w pythonie(piję to do zachwytów wydajnościowych przy przejściu z 3.10 na 3.11) jest mało racjonalne z perspektywy technologii. Można też spróbować odpalić na Graalu: https://www.graalvm.org/22.2/reference-manual/python i zobaczyć czy będzie jakiś "wystarczający" uzysk.

Chętnie bym się dowiedział ile tak naprawdę można uzyskać z tego Graala przy realnej aplikacji w pythonie :)

lion137
  • Rejestracja:około 8 lat
  • Ostatnio:4 minuty
  • Postów:4884
1

AN
  • Rejestracja:prawie 11 lat
  • Ostatnio:około 12 godzin
  • Postów:973
0
yarel napisał(a):
anonimowy napisał(a):

No ale co stoi na przeszkodzie by wydzielić tę krytyczną funkcjonalność do osobnego komponentu, z którym mógłbyś interfejsować z poziomu pythona?

To, że musiałbyś całość przenieść do innego języka a całość jest większa niż ta pętla. Jeśli natomiast chciałbyś przenieść samą pętlę to narzut na przekazanie danych jest większy niż oszczędność czasu na pętli.

To jest bardziej zadanie na mikro optymalizacje niż na zmianę architektury/języka itd.


Zdalna praca dla Senior Python Developerów --> PW
DarekRepos
  • Rejestracja:ponad 2 lata
  • Ostatnio:21 dni
  • Postów:49
2

Włozenie kodu do funkcji przyspieszyło go jeszcze o około 5 %

Kopiuj
import time

start_time = time.perf_counter()

data = {
    "a": [],
    "b": [],
}

def doit(list):
    data["a_append"] = data["a"].append
    data["b_append"] = data["b"].append
    for i in list:
        data["a_append"]("string")
        data["b_append"](123)

list = range(10_000_000)

doit(list)

print("It took: ", time.perf_counter() - start_time)

AN
Rzeczywiście, dzięki!
TheWypierdzisty
TheWypierdzisty
  • Rejestracja:prawie 2 lata
  • Ostatnio:prawie 2 lata
  • Postów:43
0

Można jeszcze trochę przyspieszyć stosując pre alokację tablic i jest jeszcze jax, który pozwala na jit kompilację, tyle że pierwsze uruchomienie jest znacznie wolniejsze, chyba że jest z cachowany kod już.

I ta optymalizacja często jest w C++ wykorzystywana, bo tam też ciągłe realokowanie nowego bufora niepotrzebnie inicjuje konstruktory i destruktory, przerzuca pamięci, bo w poprzednim buforze zabrakło miejsca.

Kopiuj
import time
start_time = time.perf_counter()

data = {
    "a": [None]*10_000_000,
    "b": [None]*10_000_000,
}

def doit(list):
    for i in list:
        data["a"][i] = "string"
        data["b"][i] = 123

list = range(10_000_000)
doit(list)

print("It took: ", time.perf_counter() - start_time)

/ed
Ewentualnie można spróbować wygenerować nową tablicę i ją połączyć z aktualną.

Kopiuj
def concatenate(l):
    data["a"] += ["string" for _ in l]
    data["b"] += [123 for _ in l]

l = range(10_000_000)

concatenate(l)
edytowany 1x, ostatnio: TheWypierdzisty
AN
Tak jak pisałem w pierwszym poście pre alokacja odpada
TheWypierdzisty
TheWypierdzisty
@anonimowy: widzę, ze nie przeczytałem zbyt dokładnie więc edytuje :L
BG
  • Rejestracja:prawie 6 lat
  • Ostatnio:4 dni
  • Postów:287
0
anonimowy napisał(a):

Ma ktoś jakieś pomysły jak przyśpieszyć podany kod wkluczając następujące możliwości:

  1. list comprehension
  2. pre alokacja listy
  3. deque
  4. pisanie tego w innym języku - chodzi mi tutaj o 100% Pythona

Tak z ciekawości - dlaczego chcesz to wykluczyć ?

Zobacz pozostałe 7 komentarzy
BG
To może nie potrzebujesz słownika list tylko na przykład listę tupli? DataFrame zbuduje się z tego tak samo dobrze a sama lista powinna zbudować się szybciej.
AN
Nie rozumiem, jak to sobie wyobrażasz? Do tupli nic nie dodam więc musiałbym znać jej zawartość przed pętlą
BG
[('str',123), ('str', 123), ...].
TheWypierdzisty
TheWypierdzisty
@anonimowy: jest jeszcze jeden sposób na badanie wewnętrznych struktur i ich optymalizację, można gdb z symbolami pythona użyć i przeanalizować daną funkcję, zobaczyć jakie funkcje CPythona są wywoływane w naszym kodzie pythona i znaleźć jakieś lepsze, które ominie jakieś nieszczęsne błędy dla konkretnego przypadku.
AN
@Bartłomiej Golenko: To rozwiązanie będzie wolniejsze bo tworzysz więcej obiektów (milion tupli, które zawierają milion str i milion int) vs (kilka list, które zawierają milion str i milion int) i potem jeszcze dostęp do tupli
S7
  • Rejestracja:prawie 5 lat
  • Ostatnio:6 dni
  • Postów:354
0

Użyj https://numba.pydata.org/ mało inwazyjne a przyspiesza kod


Competitive Google searcher
AN
Przecież w numbie nie możesz korzystać ze wszystkiego co oferuje Python więc to odpada jeśli funkcja będzie nie trywialna
YA
  • Rejestracja:prawie 10 lat
  • Ostatnio:około 14 godzin
  • Postów:2367
1

Z ciekawości zerknałem co oferuje Graal dla pythona i tak na oko x5 szybciej, bez żadnej modyfikacji kodu.

screenshot-20230522150201.png

AN
Tylko, że nie przekażesz tej listy do pythona bez strat czasu
YA
Nie rozumiem o jakim przekazywaniu listy w kontekście Graal'a mówisz. Możesz rozwinąć?
AN
Chodzi mi o to, żę na Graalu nie odpalę wszystkiego raczej? Czy odpalę np. jakiś webowy framework np. Django? Albo celery itd
YA
Nie wiem czy Django, celery i inne są/będą wspierane, to już sam musiałbyś przetestować i ocenić czy ta technologia wpisuje Ci się w obecny obrazek.
DarekRepos
  • Rejestracja:ponad 2 lata
  • Ostatnio:21 dni
  • Postów:49
1

Tak z ciekawości pobawiłem się jeszcze w zagadnienia optymalizacyjne i wyszło mi cos takiego. Nie wiem czy to pomoze w przyspieszeniu realnego kodu i da sie je zastosować, ale ten przykładowy jest troche przyspieszony. Wyniki sa dość ciekawe podam je ponizej. Zapisywalem kazdy kod w odzdzielnym pliku odpalałem wszystkie za pomoca hyperfine. (Ostrzegam nazwy plików sa przypadkowe ^^)
benchmark.png

A o to kody poszczególnych plików zeby mozna bylo sobie samemu porównać

Kopiuj
#file.py
def task():
    data = {
        "a": [],
        "b": [],
    }
    a_append = data["a"].append
    b_append = data["b"].append
    li = range(10_000_000)

    for _ in li:
        s1 = "text"
        s2 = 123
        a_append(s1)
        b_append(s2)

    return data


p=task()

Kopiuj
#cext.py
def doit(lst, data):
    s1 = "string"
    s2 = 123

    data["a_append"] = data["a"].append
    data["b_append"] = data["b"].append
    for i in lst:
        data["a_append"](s1)
        data["b_append"](s2)
    return data


def task():
    data = {
        "a": [],
        "b": [],
    }
    li = range(10_000_000)

    return doit(li, data)


task()

Kopiuj
#panda.py
data = {
    "a": [],
    "b": [],
}

def doit(list):
    data["a_append"] = data["a"].append
    data["b_append"] = data["b"].append
    for i in list:
        data["a_append"]("string")
        data["b_append"](123)

list = range(10_000_000)

doit(list)
Kopiuj
#run.py
def task():
    data = {
        "a": [],
        "b": [],
    }
    li = range(10_000_000)

    a_list = data["a"]
    b_list = data["b"]
    a_append = a_list.append
    b_append = b_list.append

    for _ in li:
        s1 = "string"
        s2 = 123
        a_append(s1)
        b_append(s2)

    return data

task()

Jesli klucze i wartosci w słowniku byly by dymiczne to byloby takie cos znowu:

Kopiuj
#wtime.py
def task():
    data = {
        "a": [],
        "b": [],
        # Additional dynamic keys
        "c": [],
        "d": [],
    }
    li = range(10_000_000)

    keys = ["a", "b", "c", "d"]

    for key in keys:
        lst = data[key]
        append_method = lst.append

        for _ in li:
            value = "string" # some dynamic value , can come from any function
            append_method(value)

    return data

task()

Czasy wychodza dosyć ciekawe np wychodzi na to że najszybciej python operuje na zmiennych lokalnych.

Kliknij, aby dodać treść...

Pomoc 1.18.8

Typografia

Edytor obsługuje składnie Markdown, w której pojedynczy akcent *kursywa* oraz _kursywa_ to pochylenie. Z kolei podwójny akcent **pogrubienie** oraz __pogrubienie__ to pogrubienie. Dodanie znaczników ~~strike~~ to przekreślenie.

Możesz dodać formatowanie komendami , , oraz .

Ponieważ dekoracja podkreślenia jest przeznaczona na linki, markdown nie zawiera specjalnej składni dla podkreślenia. Dlatego by dodać podkreślenie, użyj <u>underline</u>.

Komendy formatujące reagują na skróty klawiszowe: Ctrl+B, Ctrl+I, Ctrl+U oraz Ctrl+S.

Linki

By dodać link w edytorze użyj komendy lub użyj składni [title](link). URL umieszczony w linku lub nawet URL umieszczony bezpośrednio w tekście będzie aktywny i klikalny.

Jeżeli chcesz, możesz samodzielnie dodać link: <a href="link">title</a>.

Wewnętrzne odnośniki

Możesz umieścić odnośnik do wewnętrznej podstrony, używając następującej składni: [[Delphi/Kompendium]] lub [[Delphi/Kompendium|kliknij, aby przejść do kompendium]]. Odnośniki mogą prowadzić do Forum 4programmers.net lub np. do Kompendium.

Wspomnienia użytkowników

By wspomnieć użytkownika forum, wpisz w formularzu znak @. Zobaczysz okienko samouzupełniające nazwy użytkowników. Samouzupełnienie dobierze odpowiedni format wspomnienia, zależnie od tego czy w nazwie użytkownika znajduje się spacja.

Znaczniki HTML

Dozwolone jest używanie niektórych znaczników HTML: <a>, <b>, <i>, <kbd>, <del>, <strong>, <dfn>, <pre>, <blockquote>, <hr/>, <sub>, <sup> oraz <img/>.

Skróty klawiszowe

Dodaj kombinację klawiszy komendą notacji klawiszy lub skrótem klawiszowym Alt+K.

Reprezentuj kombinacje klawiszowe używając taga <kbd>. Oddziel od siebie klawisze znakiem plus, np <kbd>Alt+Tab</kbd>.

Indeks górny oraz dolny

Przykład: wpisując H<sub>2</sub>O i m<sup>2</sup> otrzymasz: H2O i m2.

Składnia Tex

By precyzyjnie wyrazić działanie matematyczne, użyj składni Tex.

<tex>arcctg(x) = argtan(\frac{1}{x}) = arcsin(\frac{1}{\sqrt{1+x^2}})</tex>

Kod źródłowy

Krótkie fragmenty kodu

Wszelkie jednolinijkowe instrukcje języka programowania powinny być zawarte pomiędzy obróconymi apostrofami: `kod instrukcji` lub ``console.log(`string`);``.

Kod wielolinijkowy

Dodaj fragment kodu komendą . Fragmenty kodu zajmujące całą lub więcej linijek powinny być umieszczone w wielolinijkowym fragmencie kodu. Znaczniki ``` lub ~~~ umożliwiają kolorowanie różnych języków programowania. Możemy nadać nazwę języka programowania używając auto-uzupełnienia, kod został pokolorowany używając konkretnych ustawień kolorowania składni:

```javascript
document.write('Hello World');
```

Możesz zaznaczyć również już wklejony kod w edytorze, i użyć komendy  by zamienić go w kod. Użyj kombinacji Ctrl+`, by dodać fragment kodu bez oznaczników języka.

Tabelki

Dodaj przykładową tabelkę używając komendy . Przykładowa tabelka składa się z dwóch kolumn, nagłówka i jednego wiersza.

Wygeneruj tabelkę na podstawie szablonu. Oddziel komórki separatorem ; lub |, a następnie zaznacz szablonu.

nazwisko;dziedzina;odkrycie
Pitagoras;mathematics;Pythagorean Theorem
Albert Einstein;physics;General Relativity
Marie Curie, Pierre Curie;chemistry;Radium, Polonium

Użyj komendy by zamienić zaznaczony szablon na tabelkę Markdown.

Lista uporządkowana i nieuporządkowana

Możliwe jest tworzenie listy numerowanych oraz wypunktowanych. Wystarczy, że pierwszym znakiem linii będzie * lub - dla listy nieuporządkowanej oraz 1. dla listy uporządkowanej.

Użyj komendy by dodać listę uporządkowaną.

1. Lista numerowana
2. Lista numerowana

Użyj komendy by dodać listę nieuporządkowaną.

* Lista wypunktowana
* Lista wypunktowana
** Lista wypunktowana (drugi poziom)

Składnia Markdown

Edytor obsługuje składnię Markdown, która składa się ze znaków specjalnych. Dostępne komendy, jak formatowanie , dodanie tabelki lub fragmentu kodu są w pewnym sensie świadome otaczającej jej składni, i postarają się unikać uszkodzenia jej.

Dla przykładu, używając tylko dostępnych komend, nie możemy dodać formatowania pogrubienia do kodu wielolinijkowego, albo dodać listy do tabelki - mogłoby to doprowadzić do uszkodzenia składni.

W pewnych odosobnionych przypadkach brak nowej linii przed elementami markdown również mógłby uszkodzić składnie, dlatego edytor dodaje brakujące nowe linie. Dla przykładu, dodanie formatowania pochylenia zaraz po tabelce, mogłoby zostać błędne zinterpretowane, więc edytor doda oddzielającą nową linię pomiędzy tabelką, a pochyleniem.

Skróty klawiszowe

Skróty formatujące, kiedy w edytorze znajduje się pojedynczy kursor, wstawiają sformatowany tekst przykładowy. Jeśli w edytorze znajduje się zaznaczenie (słowo, linijka, paragraf), wtedy zaznaczenie zostaje sformatowane.

  • Ctrl+B - dodaj pogrubienie lub pogrub zaznaczenie
  • Ctrl+I - dodaj pochylenie lub pochyl zaznaczenie
  • Ctrl+U - dodaj podkreślenie lub podkreśl zaznaczenie
  • Ctrl+S - dodaj przekreślenie lub przekreśl zaznaczenie

Notacja Klawiszy

  • Alt+K - dodaj notację klawiszy

Fragment kodu bez oznacznika

  • Alt+C - dodaj pusty fragment kodu

Skróty operujące na kodzie i linijkach:

  • Alt+L - zaznaczenie całej linii
  • Alt+, Alt+ - przeniesienie linijki w której znajduje się kursor w górę/dół.
  • Tab/⌘+] - dodaj wcięcie (wcięcie w prawo)
  • Shit+Tab/⌘+[ - usunięcie wcięcia (wycięcie w lewo)

Dodawanie postów:

  • Ctrl+Enter - dodaj post
  • ⌘+Enter - dodaj post (MacOS)