LINUX.ORG.RU

Несколько вопросов по NumPy

 ,


0

2
  1. Решил глубже познакомиться с NumPy. Сложилось впечатление, что операции над отдельными элементами даже медленнее, чем для стандартных структур Питона. Поэтому надо избегать перебирания элементов в цикле и сводить всё к операциям, обрабатывающим сразу весь массив. Это так?

  2. Проводил ли кто-либо сравнения скоростей извлечения бинарных данных средствами numpy и struct? Например, средствами numpy x = data[p:p+4].view('>u4')[0] у меня получилось почти вдвое медленнее чем x = struct.unpack('>I', data[p:p+4])[0], но килобайтные массивы numpy преобразует уже на несколько процентов быстрее. Не встречали ли вы подробный анализ? Есть ли что-либо быстрее np.ndarray.view()?

  3. Что лучше, np.memmap(filename, dtype='u1', mode='r'), np.fromfile(filename, dtype='u1') или np.frombuffer(open(filename, 'rb').read(), dtype='u1') ?

  4. Можно ли средствами numpy организовать дельта-декодирование, быстрее такого?

previous = 0
for c, d in enumerate(data):
                a = (d + previous) % 4294967296
                previous = a
                data[c] = a

Ответ: np.cumsum(data, ‘>u4’)

  1. То же по разным вариантам RLE-разжатия. Например:
src, dst = 0, 0
while src < len(data):
    if data[src] == special: 
        length = data[src+1]
        if length == 0:  # копировать special
            unpacked[dst] = special
            dst += 1
            src += 2
        else:  # length раз повторить C 
            c = data[src+2]
            unpacked[dst:dst+length] = c
            dst += length
            src += 3
    else:  # копировать следующий байт
        unpacked[dst] = data[src]
        dst += 1
        src += 1
  1. zlib и deflate. Есть ли что-либо помимо
    unpacked = np.frombuffer(zlib.decompress(packed.tobytes()), 'u1') ?
★★★★★

  1. Не могу сказать за весь питон, но очень часто подразумевается векторизация. Я однажды решил, что медленное выполнение функции это баг, и радостно зарепортил на github. Оказалось, что нужно было передавать вектор на вход, тогда все в шоколаде.
  2. Не работал с бинарными данными, у меня все в csv/tsv, там pandas иногда лучше работает, всё ещё может зависит от размера файла и пары параметров.
  3. см п.2
  4. можно, но нужно подумать
  5. тут как минимум можно вынести целую кучу операций из if/else
  6. без комментариев
ZERG ★★★★★ ()
Последнее исправление: ZERG (всего исправлений: 2)
Ответ на: комментарий от ZERG

1 … очень часто подразумевается векторизация

В смысле? Одновременные операции над всеми элементами массива? Это я и имел в виду — что операция над единственным элементом может быть не намного быстрее, чем над всеми сразу.

  1. тут как минимум можно вынести целую кучу операций из if/else

А что выносить? Единственная повторяющаяся операция — dst += 1, и то только в 2 ветках из 3.

Я думал про что-то вроде составить список special байтов и вместо последнего else копировать большие блоки, но на этом и всё.

question4 ★★★★★ ()
  1. Да, при этом цикл организуется в си - это гораздо быстрее чем цикл написанный на питоне. Но медленнее чем весь фрагмент кода переписанный на си. Если действительно нужна скорость - биндите в питон свои сишные функции.

.3. Как правило мапирование гораздо лучше чем все остальные варианты.

AntonI ★★ ()
Последнее исправление: AntonI (всего исправлений: 2)

Чтобы получить реально быстрое выполнение, проще заюзать numba. Я сам был поражён, когда выяснил, что функция, ускоренная при помощи @njit, по времени работает не намного дольше, чем она же, написанная на чистом С++ или Фортран.

P. S. В функции использовались обычные циклы for без всякой векторизации

Sahas ★★★★★ ()
Последнее исправление: Sahas (всего исправлений: 1)
  1. Если тебе нужно получить питонячую циферку, то самая дорогая операция это и есть получение питонячей циферки. struct.unpack делает это напрямую, numpy добавляет какой-то оверхед. Чтобы было быстро - нужно не делать по питонячему объекту на каждую циферку. Вот тебе и весь анализ.
ei-grad ★★★★★ ()
Ответ на: комментарий от ei-grad

Чтобы было быстро - нужно не делать по питонячему объекту на каждую циферку.

Об этом я написал в стартовом посте. Вопрос был, когда при создании массива/кортежа быстрее numpy.view, а когда struct.unpack.

question4 ★★★★★ ()
Ответ на: комментарий от anonymous

Тормоза, скорее всего из-за срезов и методов. Почему не сделать data.view(‘>u4’) или сразу не назначить данным нужный тип?

Насколько велик оверхэд от срезов, если они не тасуют байты, а просто выделяют невыровненный кусок из цепочки?

Применить view(‘>u4’) ко всему массиву нельзя, так как там перемешаны данные произвольной длины: во-первых, нет гарантии, что длина data кратна 4 байтам, во-вторых, адрес 4-байтного числа в массиве может быть не кратен 4. view() не позволяет задавать начало и длину. Поэтому я пробовал np.frombuffer(), который позволяет, но отличий в скорости не заметил.

np.cumsum(data, dtype=‘u4’)

Похоже, оно. Спасибо! (Удалённое сообщение писал в состоянии переутомления на 60-м километре велопоездки.) Только не ‘u4’, а ‘>u4’, эти данные вдобавок ко всем радостям ещё и big-endian :)

Как в markdown пропустить пункт в нумерованном списке?

Я для этого переключаюсь на Lorcode.

question4 ★★★★★ ()
Последнее исправление: question4 (всего исправлений: 1)

По п.1 - в питоне дорогой вызов функции. Когда ты работаешь с матрицей, ты вызываешь обработчик один раз, и дальше обработка идёт в сишных кишках numpy. Если ты делаешь цикл - ты вызываешь обработчик n раз, что дорого. По той же причине часто тормозят лямбды, классы с кастомным getattribute и прочее

upcFrost ★★★★★ ()
Ответ на: комментарий от question4

Нзч.

Вообще питон в плане производительности довольно забавный язык. Канонiчный пример - оператор +=. В зависимости от класса он может быть как быстрее чем a = a + b, так и медленнее, потому что он сначала проверит определён ли этот метод для класса, и если нет - воспользуется обычным сложением. Но проверка наличия метода, как можно догадаться, не бесплатная.

Есть еще отдельные темы про list comprehension, map и цикл, которые зависят от имплементации next и других аналогичных методов.

Короче много чёрной магии, которую можно отловить только профайлером, и все это сильно зависит от типа переменной с которой работаешь

upcFrost ★★★★★ ()
Ответ на: комментарий от question4

Насколько велик оверхэд от срезов, если они не тасуют байты, а просто выделяют невыровненный кусок из цепочки?

Да в питоне почти любое действие связано с дичайшим оверхедом, если сравнивать с сишечкой. Делаешь срез — создаётся новый объект; при вызове методов объекта начинают резолвится имена по всей его иерархии типов; в цикле никаких оптимизаций нет и доходит даже до такого, что

unpack = struct.unpack
for chunk in data:
    unpack(...)

может работать на десяток-другой процентов быстрее, чем вызов по struct.unpack(...). Возможно, что в новых версиях что-то поправили или оптимизировали, но до 3.7 было так.

Если у тебя чёткая регулярная структура, то лучше, наверное, действительно будет заюзать struct, но сразу на всём массиве данных, без лишних срезов и присваиваний.

А вообще, для быстрого и чистого тасования байтиков лучше голой сишечки пока ничего не придумали. Попробуй, кстати, на сишке написать фильтр своих данных и импортируй его модулем в питон, это не сложно, хотя почитать немножко надо; фильтрованные данные потом можно через np.frombuffer() «открывать».

anonymous ()
Ответ на: комментарий от question4

писал в состоянии переутомления на 60-м километре велопоездки

Не такое уж и значительное расстояние для переутомления, если, конечно ты ехал не в всё время в гору и не против ветра :) Держи каденс повыше, пей водичку/горячий чай и кушай конфетки по дороге. Правильно питание — очень важно, особенно если ездишь долго и быстро, расход жидкости и энергии идёт большой и если в организме чего-то будет не хватать, то можно не только переутомиться, но даже до расстройств и травм докататься.

anonymous ()
Ответ на: комментарий от question4
src, dst = 0, 0
while src < len(data):
    if data[src] == special: 
        length = data[src + 1]
        if length == 0:  
            unpacked[dst] = data[src]
        else:  
            unpacked[dst:dst + length] = data[src + 2]
            dst += length - 1
            src += 1
        src += 1
    else:  
        unpacked[dst] = data[src]
    dst += 1
    src += 1

Это то, что за пару минут в голову пришло. На самом деле можно и всю эту мишуру с if/else сделать более компактной, но не факт, что будет быстрее, зато точно будет не таким читабельным. Но без наглядного примера содержания data и unpacked сложно сказать.

ZERG ★★★★★ ()
Ответ на: комментарий от anonymous

при вызове методов объекта начинают резолвится имена по всей его иерархии типов;

Уж не поэтому ли вызовы numpy для отдельных чисел заметно дольше более простого struct?

unpack = struct.unpack может работать на десяток-другой процентов быстрее, чем вызов по struct.unpack(…).

Спасибо, о таком фокусе забыл. Но пока в такие оптимизации не суюсь. Для начала — вещи, ускоряющие в разы, вроде замены всех list на numpy.array.

Если у тебя чёткая регулярная структура

Нет, часть обрабатываемых файлов состоит из записей магическое_число—длина—«длина»_байт_данных. Поэтому нужны срезы или какие-то способы задавать offset-ы. Поэтому и спрашиваю об альтернативах view.

question4 ★★★★★ ()
Ответ на: комментарий от question4

Понятно, но не совсем.

По пункту 3, думаю, быстрее будет open(filename, 'rb').read().

А длина всегда кратна четырём? А если не всегда, то что и в каком месте этому мешает? Нельзя ли отправить весь этот кусок, возможно обрезанный навроде data[:-(len(data)%4) or None], в np.frombuffer()?

‘u1’

Если ты не используешь математику с отдельными байтами, то тогда вообще нет смысла заморачиваться с NumPy. То есть, прочить и декодировать байты из файла (пункт 6) будет быстрее так: unpacked = zlib.decompress(open(filename, 'rb').read()).

anonymous ()
  1. А если так? :
parts = data.split(special)
unpacked_list = []
for part in parts:
    length = part[0]
    if length == 0:
        c = special
        d = part[1:]
    else:
        c = part[1] * length
        d = part[2:]
    unpacked_list.append(c + d)
unpacked = b''.join(unpacked_list)

Тип у data и specialbytes.

На NumPy это не переложить, наверное. Но так хотя бы не по байтику будет копироваться, а кусками. Да и хотя бы читать это проще. Для меня так уж точно.

anonymous ()
Ответ на: комментарий от anonymous

Спасибо, уже нашёл.

Проблема только отслеживать все такие случаи и не забывать свопить.

И некрасиво :)

И заодно вопрос: что должен делать astype()? Только менять возвращаемый dtype?

question4 ★★★★★ ()
Ответ на: комментарий от question4

Ну да, это просто каст в другой тип, больше ничего не делает.

И некрасиво :)

А шо поделаешь? Примени инкапсуляцию — и будет красиво :)

while True:
    try:
        from pond import fish
    except:
        continue
anonymous ()
Ответ на: комментарий от anonymous

А длина всегда кратна четырём?

Нет.

Нельзя ли отправить весь этот кусок, возможно обрезанный навроде data[:-(len(data)%4) or None], в np.frombuffer()?

Можно и так. Но что быстрее: многократно дёргать np.frombuffer(), или сделать его (или np.memmap()) один раз и дёргать .view()? Мне кажется, что второе.

‘u1’

Если ты не используешь математику с отдельными байтами, то тогда вообще нет смысла заморачиваться с NumPy.

После импорта массивы надо будет сравнивать. Поэтому NumPy. А раз он, всё равно, нужен, возникает соблазн использовать его и для импорта. И прирост скорости даже от одного cumsum многократный.

question4 ★★★★★ ()
Ответ на: комментарий от question4

Что тут постить? Зачем дрочить на производительность в, прости господи, БИДОНЕ?

Это олигофрения.

Еще раз - в бидоне полюбому будет медленно, так медленно, что аж жопу сводить будет. Поэтому никакого онлайн процессинга итп в нем не делают. Делают игрушки в песочнице дата-даунтистов, и простенький веб на нем, всё.

«Оптимизировать» питоновский код это нонсенс вообще. Потому что в результате получается такая параша, что не читать невозможно, ни дебажить(а и так с этими двумя вещами в нем крайне туго), а тормозит в итоге точно так же.

lovesan ★☆ ()
Ответ на: комментарий от lovesan

никакого онлайн процессинга итп в нем не делают.

Мой работодатель им торгует :) NumPy + h5py.

«Оптимизировать» питоновский код это нонсенс вообще.

Так и запишем, Лавсан не умеет оптимизировать. Просто припёрся побухтеть, лишний раз сунуть всем в нос свою бесполезность.

question4 ★★★★★ ()
Ответ на: комментарий от question4

Мой работодатель им торгует :) NumPy + h5py.

Мало ли наркоманов. inb4 есть такое правило «если я не слышал про вашу компанию, у вас нет никаких проблем с производительностью и масштабируемостью»

Так и запишем, Лавсан не умеет оптимизировать. Просто припёрся побухтеть, лишний раз сунуть всем в нос свою бесполезность.

он настолько херово спроектирован и реализован, особенно CPython, что это просто не имеет смысла. Всем твоим «оптимизациям» грошь цена, это просто сферическое дрочево в вакууме.

lovesan ★☆ ()