Разлика в скоростта между повторение на генератори и списъци

В следващите тривиални примери има две функции, които сортират списък от произволни числа. Първият метод предава sorted генераторен израз, вторият метод създава списък първо:

import random
l = [int(1000*random.random()) for i in xrange(10*6)]

def sort_with_generator():
    return sorted(a for a in l)

def sort_with_list():
    return sorted([a for a in l])

Сравнителният анализ с линеен профил показва, че втората опция (sort_with_list) е около два пъти по-бърза от генераторния израз.

Може ли някой да обясни какво се случва и защо първият метод е много по-бавен от втория?


person Hamish    schedule 06.09.2013    source източник
comment
добавяте ли 1 към всеки елемент в примерния списък?   -  person elyase    schedule 06.09.2013
comment
Загубен съм. Можете ли да изолирате двете и да ги сравните поотделно? Може би интерпретаторът прави някакво интелигентно кеширане на списъка или нещо странно подобно.   -  person Brian    schedule 06.09.2013
comment
Разбирането на списъци създава ЦЕЛИЯ списък в паметта наведнъж, докато генераторните изрази захранват всеки елемент от получената последователност чрез кортежа, който се предава на вашата сортирана функция. По този начин разбирането на списъка е по-бързо, но консумира повече памет. Генериращият израз е по-бавен, но паметта се запазва само за един елемент от списъка във всеки даден момент. За повече информация вижте този въпрос: stackoverflow.com/ въпроси/47789/   -  person Shashank    schedule 06.09.2013
comment
@elyase се извинява, че се промъкна по време на поставяне - не, те трябва да са същите, освен израза.   -  person Hamish    schedule 06.09.2013
comment
Предполагам, че има грешка. И двете добавят 1. codepad.org/Hek5SPZm Мисля, че @Brian може да е прав за кеширането. Ако се обърнете, последното е по-бързо. Когато списъкът включва, той прави добавяне, така че като цяло не би трябвало да е по-бързо.   -  person CppLearner    schedule 06.09.2013
comment
Ранните времена показаха, че генераторите имат значително предимство в производителността пред разбирането на списъци. Последните обаче бяха силно оптимизирани за Py2.4 и сега производителността е приблизително сравнима за малки до средни набори от данни. Тъй като обемите от данни нарастват, генераторните изрази са склонни да работят по-добре, защото не изчерпват кеш паметта и позволяват на Python да използва повторно обекти между итерациите. python.org/dev/peps/pep-0289   -  person iMom0    schedule 06.09.2013
comment
@ShashankGupta благодаря - разбирам какво се случва, но не разбирам защо разбирането на списъка е по-бързо, нито този въпрос има отговор на моя въпрос.   -  person Hamish    schedule 06.09.2013
comment
@iMom0 благодаря - въпреки че дори и с 10^5 елемента разбирането изглежда по-бързо :/   -  person Hamish    schedule 06.09.2013
comment
Въпросът може да се сведе до list(a for a in l) срещу [a for a in l]. Ето откъде идва разликата. Последният е по-бърз със същата разлика, както когато използвате sorted.   -  person flornquake    schedule 06.09.2013


Отговори (3)


Вашият първи пример е генераторен израз, който обхожда списък. Вашият втори пример е израз на списък, който итерира списък. Наистина, вторият пример е малко по-бърз.

>>> import timeit
>>> timeit("sorted(a for a in l)", setup="import random;l = [int(1000*random.random()) for i in xrange(10*6)]")
5.963912010192871
>>> timeit("sorted([a for a in l])", setup="import random;l = [int(1000*random.random()) for i in xrange(10*6)]")
5.021576881408691

Причината за това несъмнено е, че съставянето на списък се извършва наведнъж, докато итерацията върху генератор изисква извиквания на функции.

Генераторите не трябва да ускоряват малки списъци като този (имате 60 елемента в списъка, това е много малко). Основно е за пестене на памет при създаване на дълги списъци.

person Lennart Regebro    schedule 06.09.2013
comment
Обърнете двете и ми кажете дали виждате, че генераторът е по-бърз. Освен това предположих, че прави a+1 - person CppLearner; 06.09.2013
comment
В случая двете са изолирани, защото всяка има отделна инициализация на l. Съмнявам се, че ще наблюдаваме същото явление. - person Brian; 06.09.2013
comment
Всъщност кодът ми трябваше да инициализира 10**6 елемента =D. Изглежда, че стават рентабилни някъде между 10**5 и 10**5. Все още не съм сигурен, че разбирам защо. - person Hamish; 06.09.2013
comment
@Brian: А? Ако вие двамата се опитвате да кажете, че ако преместя генерирането на произволни числа директно в извикването sorted(), това ще промени резултата: Опитах, само за да се уверя, преди да публикувам отговора. Както подозирах, не стана. - person Lennart Regebro; 06.09.2013
comment
Не не това. Предполагахме, че две последователни итерации върху един списък може да въведат разлика в производителността поради някакъв вид кеширане или други подобни. Колкото повече мисля за това, толкова по-малко вероятно го намирам - person Brian; 06.09.2013
comment
@Brian: Точно така, няма такива ефекти. - person Lennart Regebro; 06.09.2013
comment
@Hamish: Не съм сигурен, но предполагам, че разпределението на паметта на списъка започва да изяжда повече време, когато стане по-голям. - person Lennart Regebro; 06.09.2013
comment
Подкрепено, въпреки че би било чудесно, ако някой може да каже категорично. - person Hamish; 06.09.2013

Ако погледнете източника за sorted, всяка последователност, която подадете, първо се копира в нов списък.

newlist = PySequence_List(seq);

generator --> list изглежда по-бавно от list --> list.

>>> timeit.timeit('x = list(l)', setup = 'l = xrange(1000)')
16.656711101531982
>>> timeit.timeit('x = list(l)', setup = 'l = range(1000)')
4.525658845901489

Що се отнася до това защо трябва да се направи копие, помислете как работи сортирането. Сортировките не са линейни алгоритми. Ние се движим през данните няколко пъти, понякога преминавайки през данните в двете посоки. Генераторът е предназначен за създаване на последователност, през която итерираме веднъж и само веднъж, от началото до някъде след него. Списък позволява произволен достъп.

От друга страна, създаването на списък от генератор ще означава само един списък в паметта, докато създаването на копие на списък ще означава два списъка в паметта. Добър старомоден компромис пространство-време.

Python използва Timsort, хибрид на сортиране чрез сливане и сортиране чрез вмъкване.

person Brian    schedule 06.09.2013
comment
Не, генераторът --› list не е по-бавен от list --› list. Възможно е обаче да е по-бавно, отколкото първо да генерирате списъка и след това да го копирате в списък. Така че +1 все пак. - person Lennart Regebro; 06.09.2013

Списъчните изрази, първо, зареждат данни в памет. След това извършване на всякакви операции с получения списък. Нека времето за разпределение е T2 (за втори случай). Изразите на генератора не разпределят време наведнъж, но променят стойността на итератора за време t1[i]. Сумата от всички t1[i] ще бъде T1. T1T2.

Но когато извикате sorted(), в първия случай времето T1 се добавя с времето за разпределение на паметта на всяка двойка в сравнение със сортирането (tx1[i]). В резултат T1 се добавя със сумата от всички tx1[i].

Следователно, T2T1 + sum(tx1[i])

person Vladimir Chub    schedule 06.09.2013
comment
sorted не разпределя памет за всяка сравнена двойка, така че това няма смисъл. За огромни списъци, които биха изисквали огромно количество памет. Възможно е сортирането да е по-малко ефективно с генератори, но това не е причината. - person Lennart Regebro; 06.09.2013
comment
И така, и как тогава обяснявате, че изразите на генератора не се съхраняват в паметта на всички предишни стойности при повторение? КАК го сортират тогава? - person Vladimir Chub; 06.09.2013
comment
Очевидно съхранява стойностите, които сортира, да. двойки, не. Тъй като в този случай няма ключ или cmp функция, това, което съхранява, е списъкът, който сортира. - person Lennart Regebro; 06.09.2013