Просмотр памяти Python медленнее, чем ожидалось - PullRequest
0 голосов
/ 20 апреля 2019

Учитывая, что memoryview интерфейс Python для буферизованного протокола может помочь уменьшить необходимость делать временные копии данных, я решил провести его быстрое тестирование на основе этого ответа на этот вопрос .

import time

expressions = ['list(b[i:i+1000])',
               'list(b[i:])',
               'b[i:]'
              ]

size = 1000000
x = b'x'*size
mv = memoryview(x)
for e in expressions:
    print(f"Expression: {e}")
    for b in (x, mv):
        l = len(b)
        start = time.time()
        for i in range(0, l, 1000):
            eval(e)
        end = time.time()
        print(f"Size: {size}, {type(b).__name__}, time: {end-start}")

Результат:

$ python c:\temp\test_memoryview.py
Expression: list(b[i:i+1000])
Size: 1000000, bytes, time: 0.021999597549438477
Size: 1000000, memoryview, time: 0.03600668907165527
Expression: list(b[i:])
Size: 1000000, bytes, time: 5.3010172843933105
Size: 1000000, memoryview, time: 11.202003479003906
Expression: b[i:]
Size: 1000000, bytes, time: 0.2990117073059082
Size: 1000000, memoryview, time: 0.006985902786254883

Первые два результата кажутся довольно удивительным результатом.Я понимаю, что список вызовов будет включать в себя копию данных, но я подумал, что при разрезании представления памяти вместо базового байтового массива вы сохраняете промежуточную копию.

1 Ответ

1 голос
/ 20 апреля 2019

Вы не можете думать о Python, как о C или C ++. Затраты на постоянные множители дополнительной копии намного ниже, чем издержки на постоянные множители, связанные с поддержкой всех динамических функций Python, особенно без JIT в CPython. Вы не можете предполагать, что сохранение одной копии действительно поможет, если учесть другие вещи, которые нужно изменить, чтобы избежать этой копии.

В этом случае почти вся работа заключается в преобразовании списка. Копия, которую вы сохраняете, не имеет смысла. Сравните время для b[i:] и list(b[i:]), и вы увидите, что срезы составляют всего несколько процентов времени выполнения, даже когда срез выполняет копирование.

Копия, которую вы сохраняете, не имеет значения, потому что это просто memcpy. Напротив, для преобразования списка необходимо создать итератор по строке байтов или просмотру памяти, повторно вызывать слот tp_iternext итератора, получать int объекты, соответствующие необработанным байтам памяти и т. Д., Что намного дороже. Это даже дороже для вида памяти, потому что объекты вида памяти должны поддерживать многомерные формы и небайтовые типы данных, а также потому, что реализация вида памяти не имеет выделенной реализации __iter__, поэтому она проходит через стандартную итеративную резервную итерацию , который медленнее.

Вы можете сэкономить некоторое время, используя метод tolist вида памяти вместо вызова list. Это пропускает кучу накладных расходов итерационного протокола и позволяет выполнять некоторые проверки только один раз, а не один раз для каждого элемента. В моих тестах это почти так же быстро, как вызов list для строки байтов.

...