Niedawno w porównaniu z przetwarzaniem prędkości []
i list()
i była zaskoczona, że []
pracuje więcej niż trzy razy szybciej niż list()
. Pobiegłem ten sam test z {}
i dict()
a wyniki były praktycznie identyczne: []
i {}
zarówno trwała około 0.128sec / milion cykli, podczas list()
i dict()
trwało około 0.428sec / milion cykli każdy.
Dlaczego to? Czy []
i {}
(a pewnie ()
i ''
też) natychmiast przechodzić z powrotem kopie jakiegoś pustego stanie dosłownym, podczas gdy ich odpowiedniki wyraźnie nazwie ( list()
, dict()
, tuple()
, str()
) w pełni go o stworzenie obiektu, czy one rzeczywiście mają elementy?
Nie mam pojęcia, jak różnią się te dwie metody, ale chciałbym się dowiedzieć. Nie mogłem znaleźć odpowiedzi w dokumentacji lub na SO, a wyszukiwanie pustych nawiasów okazało się bardziej problematyczne, niż się spodziewałem.
Otrzymałem wyniki pomiaru, dzwoniąc timeit.timeit("[]")
i timeit.timeit("list()")
, i, timeit.timeit("{}")
i timeit.timeit("dict()")
, porównując odpowiednio listy i słowniki. Korzystam z Python 2.7.9.
Niedawno odkryłem „ Dlaczego prawda jest wolniejsza niż 1? ”, Która porównuje wydajność if True
do if 1
i wydaje się, że dotyczy podobnego scenariusza dosłownie kontra globalnie; być może warto też to rozważyć.
źródło
()
i''
są wyjątkowe, ponieważ są nie tylko puste, są niezmienne i dlatego łatwo jest je wygrać; nawet nie budują nowych obiektów, po prostu ładują singleton dla pustychtuple
/str
. Technicznie szczegół implementacji, ale trudno mi sobie wyobrazić, dlaczego nie buforują pustychtuple
/str
ze względu na wydajność. Więc twoja intuicja[]
i{}
przekazywanie literału podstawowego było błędne, ale dotyczy to()
i''
.{}
szybszy niż dzwonienieset()
?Odpowiedzi:
Ponieważ
[]
i{}
są dosłowną składnią . Python może utworzyć kod bajtowy, aby utworzyć obiekty listy lub słownika:list()
idict()
są osobnymi obiektami. Ich nazwy muszą zostać rozwiązane, stos musi być zaangażowany w popychanie argumentów, ramka musi być zapisana do późniejszego pobrania i musi zostać wykonane wywołanie. To wszystko zajmuje więcej czasu.W przypadku pustego przypadku oznacza to, że masz co najmniej a
LOAD_NAME
(który musi przeszukiwać globalną przestrzeń nazw oraz__builtin__
moduł ), a następnie aCALL_FUNCTION
, który musi zachować bieżącą ramkę:Możesz sprawdzić czas nazwy osobno
timeit
:Różnica czasu występuje prawdopodobnie w wyniku kolizji słownika. Odejmij te czasy od czasów wywołania tych obiektów i porównaj wynik z czasami użycia literałów:
Zatem konieczność wywołania obiektu zajmuje dodatkowe
1.00 - 0.31 - 0.30 == 0.39
sekundy na 10 milionów wywołań.Możesz uniknąć globalnego kosztu wyszukiwania, aliasując nazwy globalne jako miejscowe (przy użyciu
timeit
konfiguracji wszystko, co wiążesz z nazwą, jest lokalne):ale nigdy nie pokonasz tego
CALL_FUNCTION
kosztu.źródło
list()
wymaga globalnego wyszukiwania i wywołania funkcji, ale[]
kompiluje się do pojedynczej instrukcji. Widzieć:źródło
Ponieważ
list
jest to funkcja do konwersji powiedzmy ciąg na obiekt listy, podczas gdy[]
jest używana do tworzenia listy poza batem. Wypróbuj to (może być dla ciebie bardziej sensowne):Podczas
Daje ci rzeczywistą listę zawierającą wszystko, co w niej umieścisz.
źródło
[]
jest szybszy niżlist()
, a nie dlaczego['wham bam']
jest szybszy niżlist('wham bam')
.[]
/list()
jest dokładnie taki sam jak['wham']
/list('wham')
ponieważ mają te same różnice zmiennych, tak1000/10
samo jak100/1
w matematyce. Teoretycznie możesz zabrać,wham bam
a fakt byłby nadal taki sam, którylist()
próbuje przekonwertować coś, wywołując nazwę funkcji, a[]
po prostu przekształca zmienną. Wywołania funkcji są różne tak, jest to tylko logiczny przegląd problemu, ponieważ na przykład mapa sieci firmy jest logiczna dla rozwiązania / problemu. Głosuj, jak chcesz.Odpowiedzi tutaj są świetne, do rzeczy i w pełni obejmują to pytanie. Zainteresowanych usunę kolejny krok w dół od kodu bajtowego. Korzystam z najnowszego repozytorium CPython; starsze wersje zachowują się pod tym względem podobnie, ale mogą występować niewielkie zmiany.
Oto podział wykonania dla każdego z nich
BUILD_LIST
dla[]
iCALL_FUNCTION
dlalist()
.BUILD_LIST
Instrukcja:Powinieneś po prostu zobaczyć horror:
Okropnie zawiłe, wiem. Oto jakie to proste:
PyList_New
(przydziela to głównie pamięć dla nowego obiektu listy),oparg
sygnalizując liczbę argumentów na stosie. Prosto do celu.if (list==NULL)
.PyList_SET_ITEM
(makra).Nic dziwnego, że jest szybki! Jest dostosowany do tworzenia nowych list, nic więcej :-)
CALL_FUNCTION
Instrukcja:Oto pierwsza rzecz, którą zobaczysz, gdy zajrzysz do obsługi kodu
CALL_FUNCTION
:Wygląda całkiem nieszkodliwie, prawda? Cóż, nie, niestety nie,
call_function
nie jest prostym facetem, który wywoła funkcję natychmiast, nie może. Zamiast tego pobiera obiekt ze stosu, pobiera wszystkie argumenty stosu, a następnie przełącza się w zależności od typu obiektu; to jest:PyCFunction_Type
? Nie, to jestlist
,list
nie jest typuPyCFunction
PyMethodType
? Nie, patrz poprzedni.PyFunctionType
? Nie, patrz poprzedni.Jesteśmy wywołanie
list
typu, argument przekazany docall_function
jestPyList_Type
. CPython musi teraz wywołać funkcję ogólną, aby obsłużyć dowolne nazwane obiekty, które można wywoływać_PyObject_FastCallKeywords
, i więcej wywołań funkcji.Ta funkcja ponownie sprawdza niektóre typy funkcji (których nie rozumiem dlaczego), a następnie, po utworzeniu słownika dla kwargs, jeśli jest to wymagane , przechodzi do wywołania
_PyObject_FastCallDict
._PyObject_FastCallDict
w końcu nas gdzieś zabiera! Po wykonaniu nawet więcej kontroli to chwytatp_call
gniazdo przeprowadzonątype
ztype
my przekazany, to znaczy, że chwytatype.tp_call
. Następnie tworzy krotkę z argumentów przekazanych za pomocą_PyStack_AsTuple
i wreszcie można wreszcie zadzwonić !tp_call
, który pasujetype.__call__
przejmuje i ostatecznie tworzy obiekt listy. Wywołuje listy,__new__
które odpowiadająPyType_GenericNew
i przydziela mu pamięć za pomocąPyType_GenericAlloc
: W rzeczywistości jest to część, w którejPyList_New
wreszcie się dogania . Wszystkie poprzednie są niezbędne do obsługi obiektów w sposób ogólny.Na koniec
type_call
wywołujelist.__init__
i inicjuje listę dowolnymi dostępnymi argumentami, a następnie wracamy do poprzedniej wersji. :-)Na koniec pamiętajcie
LOAD_NAME
, że to inny facet, który się tutaj przyczynia.Łatwo zauważyć, że gdy mamy do czynienia z naszymi danymi wejściowymi, Python zazwyczaj musi przeskakiwać przez obręcze, aby faktycznie znaleźć odpowiednią
C
funkcję do wykonania zadania. Nie ma na to ochoty natychmiastowego nazywania go, ponieważ jest dynamiczny, ktoś może maskowaćlist
( a chłopiec robi to wiele osób ) i trzeba obrać inną ścieżkę.Tu
list()
wiele traci: Eksplorujący Python musi zrobić, aby dowiedzieć się, co do cholery powinien zrobić.Z drugiej strony, dosłowna składnia oznacza dokładnie jedno; nie można go zmienić i zawsze zachowuje się w określony sposób.
Przypis: Wszystkie nazwy funkcji mogą ulec zmianie z jednej wersji na drugą. Chodzi o punkt, który najprawdopodobniej pozostanie w przyszłych wersjach, to dynamiczne wyszukiwanie spowalnia działanie.
źródło
Największym powodem jest to, że Python traktuje
list()
jak funkcję zdefiniowaną przez użytkownika, co oznacza, że możesz ją przechwycić, aliasingując coś innegolist
i zrobić coś innego (np. Użyć własnej listy w sklasyfikowanej klasie lub być może deque).Natychmiast tworzy nowe wystąpienie wbudowanej listy za pomocą
[]
.Moje wyjaśnienie ma na celu dać ci do tego intuicję.
Wyjaśnienie
[]
jest powszechnie znany jako dosłowna składnia.W gramatyce jest to określane jako „wyświetlanie listy”. Z dokumentów :
Krótko mówiąc, oznacza to, że
list
tworzony jest wbudowany obiekt typu .Nie można tego obejść - co oznacza, że Python może to zrobić tak szybko, jak to możliwe.
Z drugiej strony
list()
można przechwycić tworzenie wbudowanego programulist
za pomocą wbudowanego konstruktora listy.Załóżmy na przykład, że chcemy, aby nasze listy były tworzone głośno:
Możemy wtedy przechwycić nazwę
list
w zasięgu globalnym na poziomie modułu, a następnie, gdy tworzymylist
, tworzymy naszą listę podtypów:Podobnie możemy usunąć go z globalnej przestrzeni nazw
i umieść go we wbudowanej przestrzeni nazw:
I teraz:
I zauważ, że wyświetlenie listy tworzy listę bezwarunkowo:
Prawdopodobnie robimy to tylko tymczasowo, więc
List
cofnijmy nasze zmiany - najpierw usuń nowy obiekt z wbudowanych:Och, nie, zgubiliśmy oryginał.
Nie martw się, wciąż możemy uzyskać
list
- to rodzaj literału listy:Więc...
Jak widzieliśmy - możemy nadpisać
list
- ale nie możemy przechwycić stworzenia literalnego typu. Kiedy korzystamy,list
musimy wyszukiwać, aby sprawdzić, czy coś tam jest.Następnie musimy zadzwonić do każdej wywoływanej przez nas funkcji. Z gramatyki:
Widzimy, że robi to samo dla dowolnej nazwy, nie tylko listy:
Ponieważ
[]
na poziomie kodu bajtowego języka Python nie ma wywołania funkcji:Po prostu idzie prosto do budowania listy bez wyszukiwania lub wywołań na poziomie kodu bajtowego.
Wniosek
Wykazaliśmy, że
list
można przechwycić kod użytkownika za pomocą reguł określania zakresu, którylist()
wyszukuje wywołanie, a następnie je wywołuje.Natomiast
[]
wyświetla listę lub dosłownie, dzięki czemu unika się wyszukiwania nazwy i wywoływania funkcji.źródło
list
a kompilator python nie może być pewien, czy naprawdę zwróci pustą listę.