wtorek, 16 września 2014

Python: najszybsza metoda łączenia stringów

Zawsze istnieje więcej niż jeden sposób wykonania jakiegoś zadania. Nie dziwota, że w języku Python stringi (ciągi znaków) można złączyć na więcej niż jeden sposób. Do głowy przychodzą mi co najmniej trzy i zastanawiam się, która z metod jest najszybsza. Dzięki temu, że już wiem jak mierzyć czas wykonania skryptu, pozwolę sobie przeprowadzić parę eksperymentów i opisać ich wynik.

Od razu ostrzegam, że sprawdzenia robiłem na szybko i nie wykonywałem wielu powtórzeń dla tej samej metody. Jestem pewien, że nie sprawdziłem wszystkich możliwych sposobów, a jedynie cztery w dwóch wariantach każdy. Jeśli ktoś zna inny ze sposobów, proszę o zamieszczenie go w komentarzu, a z przyjemnością sprawdzę, jak wypada w porównaniu z poniższymi.

Do wykonania pomiarów użyłem modułu timeit i jego funkcji timeit. Dzięki temu wykonywany był pomiar wielu powtórzeń, co pozwala na uwydatnienie różnic między czasami wykonań. Każda z metod została wykonana w dwóch wariantach:
z użyciem ciągów znaków
z użyciem zmiennych, do których uprzednio przypisane były te same ciągi znaków co w wariacie pierwszym

Pierwsza z metod jaką skojarzyłem, to złączenie przez operator dodawania. W IDLE łatwo sprawdzić, że to działa:
>>> "foo" + "bar"
'foobar'
Można ewentualnie bardziej zaszaleć:
>>> a, b = ("foo", "bar")
>>> a + b
'foobar'
W obu przypadkach wynik jest taki sam. Jak jednak wygląda czas wykonania poniższego?
>>> timeit.timeit('"foo" + "bar"')
0.0317072855809933
>>> timeit.timeit('a + b', 'a, b = ("foo", "bar")')
0.17695442069203082
Jak widać wykonanie bezpośrednio na ciągach znaków jest blisko 6x szybsze. W dokumentacji nie ma słowa o tym, czy parametr setup jest liczony do pomiaru. Na tyle na ile zrozumiałem kod modułu, to jest on wykonywany tylko raz przed wykonaniem pomiaru, Tym bardziej dziwi mnie wynik i tak duża jego różnica.
Druga metoda to użycie funkcji format. Jako że staram się uczyć Python 3, a testować głównie na wersji 3.4, format jest mi dość znany i bliski. W IDLE  wygląda to tak:
>>> "{0}{1}".format("foo", "bar")
'foobar'
Wariant drugi to:
>>> a, b = ("foo", "bar")
>>> "{0}{1}".format(a, b)
'foobar'
Test dla tego rozwiazania:
>>> timeit.timeit('"{0}{1}".format("foo","bar")')
0.6440918694441109
>>> timeit.timeit('"{0}{1}".format(a,b)', 'a, b = ("foo", "bar")')
0.5819630410125995
Jak łatwo zauważyć w tym przypadku użycie zmiennych jest szybsze niż użycie ciągów znaków bezpośrednio. Różnica nie jest wielka, ale zauważalna.
Trzecia metoda to łączenie elementów krotek. Do tego służy metoda join. Przykładowo w IDLE:
>>> "".join(("foo", "bar"))
'foobar'
Wariant drugi:
>>> a, b = ("foo", "bar")
>>> "".join((a, b))
'foobar'
Test dla tej metody:
>>> timeit.timeit('"".join(("foo", "bar"))')
0.2564672791386897
>>> timeit.timeit('"".join((a, b))', 'a, b = ("foo", "bar")')
0.2673977912679675
Jak widać ta metoda jest znacznie szybsza niż użycie format. Różnica pomiędzy wariantami jest zaś bardzo nieduża.
Czwarta metoda to w zasadzie drobna modyfikacja trzeciej. Po sprawdzeniu poprzedniej, zacząłem się zastanawiać czy lista będzie szybsza od krotki. Dlatego też ponownie użyta została metoda join, ale łączeniu poddana została lista:
>>> "".join(["foo", "bar"])
'foobar'
Wariant drugi:
>>> a, b = ("foo", "bar")
>>> "".join([a, b])
'foobar'
Test dla tej metody:
>>> timeit.timeit('"".join(["foo", "bar"])')
0.43746682858508734
>>> timeit.timeit('"".join([a, b])', 'a, b = ("foo", "bar")')
0.3809328838217425
Jak widać zastosowanie listy jest gorszym pomysłem niż zastosowanie krotki. W sumie nie jest to dziwne, bo lista jest tworzona w każdym z miliona wywołań. Domyślam się, że stworzenie rozszerzalnej listy jest znacznie bardziej kosztowne niż stałej krotki. Nie spodziewam się, aby sam dostęp do elementów miał różny koszt czasowy.
Wspomniałem już, że pracuję na Python 3.4 i z tego powodu korzystam z metody format. W Python 2 mamy do dyspozycji operator %, który robi mniej wiecej to samo, ale ciąg z formatem jest nieco inny. Nie używam praktycznie tego operatora, ale choć odradzany, jest on ciągle wspierany w Python 3. Zgodnie z wcześniejszymi przykładami w IDLE użycie go wygląda tak:
>>> "%s%s" % ("foo", "bar")
'foobar'
Wariant drugi:
>>> a, b = ("foo", "bar")
>>> "%s%s" % (a, b)
'foobar'
Sam test dla tej metody:
>>> timeit.timeit('"%s%s" % ("foo", "bar")')
0.031486765752106294
>>> timeit.timeit('"%s%s" % (a, b)', 'a, b = ("foo", "bar")')
0.40198046513705776
Uważam za dość interesujące, że dla ciągów znaków stary operator jest tak szybki jak operator dodawania. Spodziewałem się, że wynik tego testu będzie bardzo zbliżony do wyników dla metody pierwszej. Co ciekawe użycie zmiennych powoduje spowolnienie ponad 12x. Bardzo ciekaw jestem skąd może to wynikać.

Wnioski

Ktoś mi podpowiedział jakiś czas temu, że metoda format jest szybsza niż operator sumy (+) przy łączeniu zmiennych zawierających ciągi znaków. Nie poddałem tego wtedy w wątpliwość, ale teraz wiem, że tak nie jest. Zwykłe sumowanie zmiennych jest jednak najszybsze (przy porównaniu stosowania zmiennych w metodach). Trzeba jednak pamiętać, że timeit wykonuje aż milion (1000000) powtórzeń przy pomiarze czasu wykonania. Oznacza to, że dla pojedynczych czy setek powtórzeń operacji konkatenacji, różnice są tak nieduże, że nie ma większego sensu zawracać sobie nimi głowy.

Brak komentarzy:

Prześlij komentarz