Уведомления

Группа в Telegram: @pythonsu
  • Начало
  • » Центр помощи
  • » в результате выполнения кода создаются лишние списки, вложенные в друг друга [RSS Feed]

#1 Авг. 28, 2015 08:47:24

Isem
От:
Зарегистрирован: 2010-08-27
Сообщения: 447
Репутация: +  7  -
Профиль   Отправить e-mail  

в результате выполнения кода создаются лишние списки, вложенные в друг друга

Конечный оптимизированный вариант функций merge и mergesort:

def merge(a,b):
    a, b = iter(a), iter(b)
    try:
        vb = next(b)
        try:
            while 1:
                va = next(a)
                if va > vb: va, vb, a, b = vb, va, b, a
                yield va
        except: yield vb
        yield from b
    except: yield from a
    
def mergesort(lst):
    l = len(lst)
    if l == 2:
        ll, rr = lst
        return lst if ll < rr else (rr, ll)
    l //= 2
    return tuple(merge(mergesort(lst[:l]), mergesort(lst[l:]))) if l else lst

В предыдущем тесте (100000 натуральных чисел в обратном порядке с 5-ю повторами) получаются результаты:
для 1: 11.513048215834704, 11.374056644923208, 11.453290076851037
для 2: 2.952010323617081, 3.074638500573627, 2.982537417350983



Офлайн

#2 Авг. 28, 2015 10:29:47

py.user.next
От:
Зарегистрирован: 2010-04-29
Сообщения: 9726
Репутация: +  843  -
Профиль   Отправить e-mail  

в результате выполнения кода создаются лишние списки, вложенные в друг друга

Isem
Если вызвать mergesort2, то у меня получается
Там поправил код и результат.

Isem
Конечный оптимизированный вариант функций merge и mergesort:
Добавил оптимизированный вариант:
#!/usr/bin/env python3
 
import timeit
 
def prepare1():
 
    def merge(left, right):
        """Merge two lists in ascending order."""
        lst = []
        while left and right:
            if left[0] < right[0]:
                lst.append(left.pop(0))
            else:
                lst.append(right.pop(0))
        if left:
            lst.extend(left)
        if right:
            lst.extend(right)
        return lst
 
    def mergesort(lst):
        """Sort the list by merging O(n * log n)."""
        length = len(lst)
        if length >= 2:
            mid = int(length / 2)
            lst = merge(mergesort(lst[:mid]), mergesort(lst[mid:]))
        return lst
 
    return mergesort
 
def prepare2():
 
    def merge(a,b):
        a, b = iter(a), iter(b)
        def gen(a, b):
            vb = next(b)
            try:
                while 1:
                    va = next(a)
                    if va > vb: va, vb, a, b = vb, va, b, a
                    yield va
            except: yield vb
        yield from gen(a, b)
        yield from a
        yield from b
 
    def mergesort(lst):
        mid = len(lst) // 2
        return merge(mergesort(lst[:mid]), mergesort(lst[mid:])) if mid else lst
 
    return mergesort
 
def prepare3():
 
    def merge(a,b):
        a, b = iter(a), iter(b)
        try:
            vb = next(b)
            try:
                while 1:
                    va = next(a)
                    if va > vb: va, vb, a, b = vb, va, b, a
                    yield va
            except: yield vb
            yield from b
        except: yield from a
 
    def mergesort(lst):
        l = len(lst)
        if l == 2:
            ll, rr = lst
            return lst if ll < rr else (rr, ll)
        l //= 2
        return tuple(merge(mergesort(lst[:l]), mergesort(lst[l:]))) if l else lst
 
    return mergesort
 
mergesort1 = prepare1()
mergesort2 = prepare2()
mergesort3 = prepare3()
 
lst = list(range(100000))[::-1]
 
def f1():
    mergesort1([])
    mergesort1([3, 2, 1])
    mergesort1([4, 3, 2, 1])
    mergesort1([4, 3, 2, 1, 4, 3, 2, 1])
    mergesort1(lst)
 
def f2():
    list(mergesort2([]))
    list(mergesort2([3, 2, 1]))
    list(mergesort2([4, 3, 2, 1]))
    list(mergesort2([4, 3, 2, 1, 4, 3, 2, 1]))
    list(mergesort2(lst))
 
def f3():
    list(mergesort3([]))
    list(mergesort3([3, 2, 1]))
    list(mergesort3([4, 3, 2, 1]))
    list(mergesort3([4, 3, 2, 1, 4, 3, 2, 1]))
    list(mergesort3(lst))
 
def main():
    t1 = timeit.Timer('f1()', 'from __main__ import f1')
    t2 = timeit.Timer('f2()', 'from __main__ import f2')
    t3 = timeit.Timer('f3()', 'from __main__ import f3')
     
    for t in t1, t2, t3:
        print(t.repeat(3, 10))
 
if __name__ == '__main__':
    main()
[guest@localhost py]$ ./mergecmp.py 
[12.741739178999978, 12.674231455999916, 13.210156642000015]
[9.427098683000054, 9.385439743000006, 9.390970238000023]
[6.9163552539999955, 7.000873819999924, 6.991111120000028]
[guest@localhost py]$

Получилось преимущество в два раза, не более. :)
(Я там не сразу заметил, что в оптимизированном варианте ты преобразуешь в кортеж (хотя почему в кортеж? типа он быстрее списка? а почему на входе подаётся список, а на выходе кортеж? а если список пустой, то возвращается список? надо уж как-то точнее что ли), но потом проверил все случаи и даже фору твоему варианту дал, они всё равно выдают ~в два раза.)

Кстати,
>>> sorted('abcd')
['a', 'b', 'c', 'd']
>>>
Вопрос: почему встроенная sorted() возвращает список а не кортеж?



Отредактировано py.user.next (Авг. 28, 2015 10:42:13)

Офлайн

#3 Авг. 28, 2015 11:16:17

Isem
От:
Зарегистрирован: 2010-08-27
Сообщения: 447
Репутация: +  7  -
Профиль   Отправить e-mail  

в результате выполнения кода создаются лишние списки, вложенные в друг друга

py.user.next
Получилось преимущество в два раза, не более. :)
Если удвоить (утроить и т.д.) количество элементов, то время работы растет непропорционально (может быть логарифмически, может быть степенью, не могу сказать наверняка).
Много времени занимает взятие срезов в функции mergesort, хотя изначально я измерял лишь время работы merge, которое и получалось в десятки раз меньше (отсюда и диссонанс появился, так как в результате тестировали еще и mergesort).
py.user.next
(хотя почему в кортеж? типа он быстрее списка? а почему на входе подаётся список, а на выходе кортеж? а если список пустой, то возвращается список? надо уж как-то точнее что ли)
В целом кортеж немножко быстрее работает. Сказывается то, как он отводится в памяти. Для хранения списка питон создает сам объект списка плюс отдельно память для хранения объектов списка. Обект кортежа создается раз и навсегда одним куском памяти, которая хранит как сам объект кортежа, так и список объектов, которые он содержит.
На входе в merge можно подавать что угодно, лишь бы это был итератор (список, кортеж, словарь, генератор, множество). На выходе, так уж написано эта функция, выдается tuple - это работает несколько быстрее для рекурсивного алгоритма, чем выдавать генератор (особенности реализации питона).
py.user.next
Вопрос: почему встроенная sorted() возвращает список а не кортеж?
Встроенная sorted возвращает список по той простой причине, что ее оптимизированная сишная реализация органически требует динамический массив, а значит и нет никакой надобности делать его еще потом тьюплом. Зачем лишний раз заниматься реаллокацией данных в памяти?



Офлайн

#4 Авг. 28, 2015 12:52:46

py.user.next
От:
Зарегистрирован: 2010-04-29
Сообщения: 9726
Репутация: +  843  -
Профиль   Отправить e-mail  

в результате выполнения кода создаются лишние списки, вложенные в друг друга

Isem
Обект кортежа создается раз и навсегда одним куском памяти, которая хранит как сам объект кортежа, так и список объектов, которые он содержит.
Кортеж - это такой же массив указателей, как и список:
>>> a, b, c = [1], [2], [3]
>>> 
>>> t = a, b, c
>>> t
([1], [2], [3])
>>> a.append(1)
>>> t
([1, 1], [2], [3])
>>>
Просто список медленнее работает, потому что в нём есть дополнительные методы для изменения. Он дольше создаётся, так как это всё надо регистрировать.

Isem
Встроенная sorted возвращает список по той простой причине, что ее оптимизированная сишная реализация органически требует динамический массив, а значит и нет никакой надобности делать его еще потом тьюплом. Зачем лишний раз заниматься реаллокацией данных в памяти?
Она (sorted) возвращает список, потому что проводит перестановки элементов.
Как точно такие же перестановки исключаются при mergesort? Какое преимущество даёт tuple?



Отредактировано py.user.next (Авг. 28, 2015 12:57:24)

Офлайн

#5 Авг. 28, 2015 13:40:23

Isem
От:
Зарегистрирован: 2010-08-27
Сообщения: 447
Репутация: +  7  -
Профиль   Отправить e-mail  

в результате выполнения кода создаются лишние списки, вложенные в друг друга

py.user.next
Кортеж - это такой же массив указателей, как и список:
Так оно и есть, разница только где этот массив находится в памяти - отдельно (в этом случае его размер можно динамически менять) или в той же структуре тьюпла (имеется в виду C-struct и в таком случае его менять нельзя).
py.user.next
Просто список медленнее работает, потому что в нём есть дополнительные методы для изменения.
Список работает не медленне, просто не вызывайте методы, которых нет в тьюпле - методы, изменяющие список. Однако, список медленнее создается и медленнее удаляется по сравнению с тьюплом. А также больше фрагментирует память.
py.user.next
Она (sorted) возвращает список, потому что проводит перестановки элементов.
В сишной реализации перестанавливать элементы в тьюпле не запрещается, потому что с точки зрения Си есть массив указателей на элементы, а не список или тьюпл. Список создается потому, что для работы алгоритма сортировки требуется массив указателей, размер которого заранее неизвестен (ведь в функцию sorted можно передать любой итератор, размер которого неизвестен, пока мы по нему не пробежим), поэтому выгоднее создать список с динамическим массивом.
py.user.next
Как точно такие же перестановки исключаются при mergesort? Какое преимущество даёт tuple?
mergesort в нашей реализации не занимается реальными перестановками, потому что каждый раз создаются новые массивы, из которых в конце собирается результирующий массив. А так как приходится создавать много-много массивов, то выгоднее всего в таком случае создавать tuple.
Операция взятия среза создает новый объект:
>>> a = (1,2,3,4)
>>> id(a) == id(a[:2])
False
>>> 
mergesot во время работы создает как минимум столько же массивов (временных), сколько элементов в списке для сортировки.

p.s. Естественно, я говорю про нашу питоновскую реализацию mergesort



Отредактировано Isem (Авг. 28, 2015 13:48:00)

Офлайн

#6 Авг. 28, 2015 13:53:18

Isem
От:
Зарегистрирован: 2010-08-27
Сообщения: 447
Репутация: +  7  -
Профиль   Отправить e-mail  

в результате выполнения кода создаются лишние списки, вложенные в друг друга

Интересно было бы написать inplace mergesort. В таком случае скорость ее работы увеличится на порядок.
И тогда она, конечно, будет работать со списком.



Офлайн

#7 Авг. 28, 2015 14:27:45

py.user.next
От:
Зарегистрирован: 2010-04-29
Сообщения: 9726
Репутация: +  843  -
Профиль   Отправить e-mail  

в результате выполнения кода создаются лишние списки, вложенные в друг друга

Isem
находится в памяти - отдельно (в этом случае его размер можно динамически менять) или в той же структуре тьюпла
Это ты смотрел или предполагаешь?

Isem
Список работает не медленне, просто не вызывайте методы, которых нет в тьюпле - методы, изменяющие список.
[guest@localhost ~]$ python3 -mtimeit -n 10000 -s "obj = [1, 2, 3]" "obj[::-1]"
10000 loops, best of 3: 0.237 usec per loop
[guest@localhost ~]$ python3 -mtimeit -n 10000 -s "obj = (1, 2, 3)" "obj[::-1]"
10000 loops, best of 3: 0.16 usec per loop
[guest@localhost ~]$

Isem
А также больше фрагментирует память.
Это откуда инфа такая?

Isem
Список создается потому, что для работы алгоритма сортировки требуется массив указателей, размер которого заранее неизвестен (ведь в функцию sorted можно передать любой итератор, размер которого неизвестен, пока мы по нему не пробежим), поэтому выгоднее создать список с динамическим массивом.
Чем же мешает кортеж в таком случае? Уж не тем ли, что в нём переставлять элементы невозможно?

Isem
А так как приходится создавать много-много массивов, то выгоднее всего в таком случае создавать tuple.
Так оно у тебя не создаётся, только конечный массив преобразуется в tuple, а так - там генераторы. А те генераторы, которые в промежутках преобразуются в tuple, делают это напрасно.

Isem
Операция взятия среза создает новый объект:
У меня там срезы, чтобы можно было списки эти менять и на исходный массив это не влияло. У тебя же там срезы - непонятно, зачем. По идее, надо сделать копию списка (итерабла) и сортировать её на месте (inplace), раз уж там генераторы.

Isem
На входе в merge можно подавать что угодно, лишь бы это был итератор (список, кортеж, словарь, генератор, множество).
Но подаются-то туда срезы, которые с генератора не сделаешь (нецелесообразно). Генератор предназначен для использования без знания его длины, тогда как правый срез должен узнавать его длину.



Отредактировано py.user.next (Авг. 28, 2015 14:30:25)

Офлайн

#8 Авг. 28, 2015 19:51:50

Isem
От:
Зарегистрирован: 2010-08-27
Сообщения: 447
Репутация: +  7  -
Профиль   Отправить e-mail  

в результате выполнения кода создаются лишние списки, вложенные в друг друга

py.user.next
Это ты смотрел или предполагаешь?
Я это знаю, так как использую С-структуры питона непосредственно в С/С++ проектах (Питон для меня - это отличный бонус).
py.user.next
$ python3 -mtimeit -n 10000 -s "obj = “ ”obj"
10000 loops, best of 3: 0.237 usec per loop
$ python3 -mtimeit -n 10000 -s “obj = (1, 2, 3)” "obj"
10000 loops, best of 3: 0.16 usec per loop
$
Здесь я вижу создание двух списков (первый пример) и двух тьюплов (второй пример). Как уже сказал, создание списка - это медленнее, чем создание тьюпла.
py.user.next
Это откуда инфа такая?
Это называется memory managment in C/C++. Список в течение времени жизни много раз отводит и освобождает память (при старте - два раза), тьюпл - только один раз и только при его создании.

py.user.next
Чем же мешает кортеж в таком случае? Уж не тем ли, что в нём переставлять элементы невозможно?
Дело не в перестановке элементов в кортеже, а в изменении его размера (количества элементов).
py.user.next
Так оно у тебя не создаётся, только конечный массив преобразуется в tuple, а так - там генераторы. А те генераторы, которые в промежутках преобразуются в tuple, делают это напрасно.
Они это делают не напрасно. Можно и без них, но с ними код в данном конкретном случае работает быстрее, чем с генераторами. Особенности реализации питона.
py.user.next
У меня там срезы, чтобы можно было списки эти менять и на исходный массив это не влияло. У тебя же там срезы - непонятно, зачем. По идее, надо сделать копию списка (итерабла) и сортировать её на месте (inplace), раз уж там генераторы.
Срез создает новый список (или тьюпл, если срез берется от тьюпла), сами списки ты не меняешь. Поставь на вход тьюпл и все прекрасно сработает, хотя тьюплы, как мы все знаем, менять нельзя (immutable object). Например срез
[1,2,3,4][:]
создает копию списка.
Для inplace mergesort функция merge тоже должна быть inplace, то есть выглядеть совершенно иначе.

py.user.next
Но подаются-то туда срезы, которые с генератора не сделаешь (нецелесообразно). Генератор предназначен для использования без знания его длины, тогда как правый срез должен узнавать его длину.
Срезы не являются самостоятельными объектами, поэтому их нельзя куда-то подать. Срез - это операция, которая создает новый список (или тьюпл), который затем уже передается или что там с ним делается.



Отредактировано Isem (Авг. 28, 2015 19:53:58)

Офлайн

#9 Авг. 29, 2015 13:32:48

py.user.next
От:
Зарегистрирован: 2010-04-29
Сообщения: 9726
Репутация: +  843  -
Профиль   Отправить e-mail  

в результате выполнения кода создаются лишние списки, вложенные в друг друга

Isem
Я это знаю, так как использую С-структуры питона непосредственно в С/С++ проектах
И откуда следует, что объекты списка и кортежа хранятся по-разному?
Насколько помню (лень смотреть) они там хранятся в виде прицепленного к структуре массива. Для элементов кортежа память выделяется один раз, для элементов списка - выделяется в первый, а потом перевыделяется.

Isem
Здесь я вижу создание двух списков (первый пример) и двух тьюплов (второй пример). Как уже сказал, создание списка - это медленнее, чем создание тьюпла.
Операция-то одна и та же - взятие среза.

Isem
Это называется memory managment in C/C++. Список в течение времени жизни много раз отводит и освобождает память (при старте - два раза), тьюпл - только один раз и только при его создании.
Ахаха, а причём тут фрагментация? Зря ты склеил у себя в сознании C и C++, это разные языки, а склеивают их обычно те, кто C не знает (теоретизировал после C++ в стиле “гиме зе мер бите… а, ну это я знаю”).

Isem
Дело не в перестановке элементов в кортеже, а в изменении его размера (количества элементов).
Если подаётся кортеж, для сортировки его элементов нужно их переставлять, а так как порядок элементов кортежа можно определить один раз, то делается список, чтобы там можно было обмены совершать по мере сортировки.

Isem
Можно и без них, но с ними код в данном конкретном случае работает быстрее, чем с генераторами.
Как-то у тебя странно: ты хотел использовать итераторы, но так как они криво пошли, ты переключился на кортежи. Но ведь кортежи жрут память, и практически то же количество, что и списки.

Isem
Поставь на вход тьюпл и все прекрасно сработает
У меня там списки не просто так, а для возможности выполнения pop(). И по ним ещё и условие проверяется, пуст/не пуст.

Isem
Для inplace mergesort функция merge тоже должна быть inplace
В общем, у тебя какая-то комбинация получилась запутанная. С одной строны ты хотел применить итераторы, но оказалось, что там надо копировать части. И в итоге ты их использовал хотя бы где-то. А если всё делать на итераторах, то оно как-то не получается ;)
А всё почему? Потому что в итераторе ты не можешь менять порядок элементов.

Isem
Срезы не являются самостоятельными объектами, поэтому их нельзя куда-то подать.
>>> [1, 2, 3, 4][slice(1, 3)]
[2, 3]
>>>



Отредактировано py.user.next (Авг. 29, 2015 13:36:06)

Офлайн

#10 Авг. 29, 2015 19:25:43

Isem
От:
Зарегистрирован: 2010-08-27
Сообщения: 447
Репутация: +  7  -
Профиль   Отправить e-mail  

в результате выполнения кода создаются лишние списки, вложенные в друг друга

py.user.next
И откуда следует, что объекты списка и кортежа хранятся по-разному?
Бля буду кишки в таз :)
py.user.next
Насколько помню (лень смотреть) они там хранятся в виде прицепленного к структуре массива.
Мне, например, не лень не только смотреть, но и делать. Все остальное сказал все правильно, но не сказал суть: где хранится прицепленный к структуре массив? (подскзка: у тьюплов и списков он хранится в разных местах).
py.user.next
Операция-то одна и та же - взятие среза.
В действительности (это неявно), взятие среза у списка - это одна операция, которая создает новый список, а взятие среза у тьюпла - это другая операция, которая создает новый тьюпл. Абстрактно операция одна, а фактически - две разные операции, делающие разные вещи. Причем, занимающие разное время (у списка - больше, у тьюпла - меньше). Ничего, что я начинаю разжевывать и повторяться?
py.user.next
C и C++, это разные языки, а склеивают их обычно те, кто C не знает (теоретизировал после C++ в стиле “гиме зе мер бите… а, ну это я знаю”).
Ключевое слово в этой фразе: теоретизировал. Считаем, что ты говоришь за себя о себе, а не о других. Некультурно говорить о других, ничего о них не зная. Совет. И да, без обид, ничего личного.
py.user.next
Если подаётся кортеж, для сортировки его элементов нужно их переставлять, а так как порядок элементов кортежа можно определить один раз, то делается список, чтобы там можно было обмены совершать по мере сортировки
Все правильно, но речь шла совсем о другом.
py.user.next
Как-то у тебя странно: ты хотел использовать итераторы, но так как они криво пошли, ты переключился на кортежи. Но ведь кортежи жрут память, и практически то же количество, что и списки.
Итератолры пошли великолепно (особенно по сравнению с вызовом pop первого элемента в цикле), потому что скорость работы merge увеличилась в десятки раз. Можно ее отдельно потестировать и убедиться в этом, чтобы все остальные посмотрели. Мы же (то есть ты), стал тестировать mergesort (не inplace), на основе такой же merge (то есть не inplace). У Кнута, кстати, подробно рассмотрены десятки алгоритмов сортировки, рекомендую.
py.user.next
У меня там списки не просто так, а для возможности выполнения pop(). И по ним ещё и условие проверяется, пуст/не пуст.
Вот эот pop и делает алгоритм merge ужасно неэффективным.
py.user.next
В общем, у тебя какая-то комбинация получилась запутанная. С одной строны ты хотел применить итераторы, но оказалось, что там надо копировать части. И в итоге ты их использовал хотя бы где-то. А если всё делать на итераторах, то оно как-то не получается ;)
А всё почему? Потому что в итераторе ты не можешь менять порядок элементов.
Предлагаю распутать эту ситуацию, предложив людям два способа merge и mergesort: итерабельный и inplace. С итерабельным вариант готов, осталось все то же самое сделать inplace и довести их до совершенства. Кто начнет? Готов? А бессмысленную полемику предлагаю отставить.
py.user.next
slice(1, 3)
slice позволяет использовать итераторы. Но результат тот же: срез от списка - создает новый список, срез от тьюпла - создает новый тьюпл, срез от итератора создает новый итератор. В твоем примере просто создается новый список.
Чтобы написать оптимальный код, надо представлять что происходит на уровне команд процессора (CPU то есть). Теория и практика (Бог) в помощь.




Отредактировано Isem (Авг. 29, 2015 19:50:00)

Офлайн

  • Начало
  • » Центр помощи
  • » в результате выполнения кода создаются лишние списки, вложенные в друг друга[RSS Feed]

Board footer

Модераторировать

Powered by DjangoBB

Lo-Fi Version