быстрые байты XORing в python 3

Мне нужно xor 2 байта объектов. Я использую этот код:

def bxor(b1, b2): # use xor for bytes
    result = b""
    for b1, b2 in zip(b1, b2):
        result += bytes([b1 ^ b2])
    return result

Он отлично работает, когда объекты байтов малы, но если я исправляю большие объекты (несколько МБ), это занимает очень много времени (несколько часов). Как я могу сделать это быстрее?


person anton-tsyganenko    schedule 26.04.2014    source источник
comment
Это может помочь.   -  person devnull    schedule 26.04.2014


Ответы (4)


При выполнении XOR объектов bytes с одним миллионом элементов в каждом этот цикл создает примерно один миллион временных объектов bytes и копирует каждый байт в среднем примерно 500 тысяч раз из одного временного bytes в другой. Обратите внимание, что точно такая же проблема существует для строк (и во многих других языках). Строковое решение состоит в том, чтобы создать список частей строки и использовать ''.join в конце для их эффективного объединения. Вы можете сделать то же самое с байтами:

def bxor(b1, b2): # use xor for bytes
    parts = []
    for b1, b2 in zip(b1, b2):
        parts.append(bytes([b1 ^ b2]))
    return b''.join(parts)

В качестве альтернативы вы можете использовать bytearray, который является изменяемым и, следовательно, может избежать проблемы. Это также позволяет вам не выделять новый объект bytes на каждой итерации, вы можете просто добавить байт/int.

def bxor(b1, b2): # use xor for bytes
    result = bytearray()
    for b1, b2 in zip(b1, b2):
        result.append(b1 ^ b2)
    return result

В качестве альтернативы вы можете return bytes(result), если вам нужен/нужен объект bytes.

person Community    schedule 26.04.2014
comment
b''.join() не быстрее; в моем времени это на самом деле немного медленнее .. - person Martijn Pieters; 26.04.2014
comment
Я засекал 70 байт, а также добавил тест на 7000 байт. bytes.join() немного улучшается с увеличением количества байтов. - person Martijn Pieters; 26.04.2014
comment
bytes(map(operator.xor, b1, b2)) выполняется примерно вдвое быстрее в тесте с миллионом байтов. - person Eryk Sun; 26.04.2014

Добавление этого в другой ответ, потому что это один:

Если вам нужно что-то более быстрое, чем указанные «ручные» методы, всегда есть Numpy:

import numpy

def bxor_numpy(b1, b2):
    n_b1 = numpy.fromstring(b1, dtype='uint8')
    n_b2 = numpy.fromstring(b2, dtype='uint8')

    return (n_b1 ^ n_b2).tostring()

и это быстро:

first_random = urandom(100000)
second_random = urandom(100000)

min(Timer(partial(bxor_inplace, first_random, second_random)).repeat(10, 100))
#>>> 1.5381054869794752
min(Timer(partial(bxor_append, first_random, second_random)).repeat(10, 100))
#>>> 1.5624085619929247
min(Timer(partial(bxor_numpy, first_random, second_random)).repeat(10, 100))
#>>> 0.009930026979418471

Так что это в 150 раз быстрее, чем лучшие альтернативы, размещенные здесь.

person Veedrac    schedule 26.04.2014

Использование bytearray уже намного быстрее:

def bxor(b1, b2):
    result = bytearray(b1)
    for i, b in enumerate(b2):
        result[i] ^= b
    return bytes(result)

Быстрое timeit сравнение:

>>> import timeit
>>> b1, b2 = b'abcdefg' * 10, b'aaaaaaa' * 10
>>> timeit.timeit('it(b1, b2)', 'from __main__ import b1, b2, bxor as it', number=10000)
0.9230150280000089
>>> timeit.timeit('it(b1, b2)', 'from __main__ import b1, b2, bxor_ba as it', number=10000)
0.16270576599890774

Это позволяет избежать создания новых объектов bytes для всех конкатенаций.

Метод b''.join(), предложенный delnan, ненамного лучше оригинальной версии:

>>> timeit.timeit('it(b1, b2)', 'from __main__ import b1, b2, bxor_join as it', number=10000)
0.9936718749995634

И повторный запуск со строками байтов в 100 раз больше:

>>> b1, b2 = b'abcdefg' * 1000, b'aaaaaaa' * 1000
>>> timeit.timeit('it(b1, b2)', 'from __main__ import b1, b2, bxor as it', number=1000)
11.032563796999966
>>> timeit.timeit('it(b1, b2)', 'from __main__ import b1, b2, bxor_join as it', number=1000)
9.242204494001271
>>> timeit.timeit('it(b1, b2)', 'from __main__ import b1, b2, bxor_ba as it', number=1000)
1.762020197998936

чтобы показать, что bytes.join() быстрее, чем повторная конкатенация.

Последний прогон на 7 миллионов байт, повторенный 10 раз, только с версией bytearray, у меня кончилось терпение с другими версиями:

>>> b1, b2 = b'abcdefg' * 1000000, b'aaaaaaa' * 1000000
>>> timeit.timeit('it(b1, b2)', 'from __main__ import b1, b2, bxor_ba as it', number=10)
16.18445999799951
person Martijn Pieters    schedule 26.04.2014
comment
Это не просто в 9 раз быстрее, разрыв увеличивается довольно быстро по мере того, как струны становятся длиннее. Но обнадеживает то, что асимптотически более быстрый вариант также быстрее для небольших входных данных. - person ; 26.04.2014

Тайминги Мартина Питерса немного отличаются от моих:

def bxor_add(b1, b2): # use xor for bytes
    result = b""
    for b1, b2 in zip(b1, b2):
        result += bytes([b1 ^ b2])
    return result

def bxor_inplace(b1, b2):
    result = bytearray(b1)
    for i, b in enumerate(b2):
        result[i] ^= b
    return bytes(result)

def bxor_join(b1, b2): # use xor for bytes
    parts = []
    for b1, b2 in zip(b1, b2):
        parts.append(bytes([b1 ^ b2]))
    return b''.join(parts)

def bxor_append(b1, b2): # use xor for bytes
    result = bytearray()
    for b1, b2 in zip(b1, b2):
        result.append(b1 ^ b2)
    return bytes(result)

#>>> 

from os import urandom
from timeit import Timer
from functools import partial

first_random = urandom(200000)
second_random = urandom(200000)

Timer(partial(bxor_add, first_random, second_random)).timeit(1)
#>>> 1.3261873809969984
Timer(partial(bxor_inplace, first_random, second_random)).timeit(1)
#>>> 0.03055390200461261
Timer(partial(bxor_join, first_random, second_random)).timeit(1)
#>>> 0.15852201101370156
Timer(partial(bxor_append, first_random, second_random)).timeit(1)
#>>> 0.030534288001945242

first_random = urandom(10000000)
second_random = urandom(10000000)

Timer(partial(bxor_inplace, first_random, second_random)).timeit(1)
#>>> 1.5432947289955337
Timer(partial(bxor_join, first_random, second_random)).timeit(1)
#>>> 7.90503858300508
Timer(partial(bxor_append, first_random, second_random)).timeit(1)
#>>> 1.5145326450001448

Я бы выбрал версию append для ясности и скорости.


Для пояснения, я не думаю, что метод append значительно быстрее, чем версия inplace; Я просто думаю, что это немного проще.

Тем не менее, поскольку было запрошено:

first_random = urandom(100000)
second_random = urandom(100000)

min(Timer(partial(bxor_inplace, first_random, second_random)).repeat(10, 100))
#>>> 1.5381054869794752
min(Timer(partial(bxor_append, first_random, second_random)).repeat(10, 100))
#>>> 1.5196998479950707
person Veedrac    schedule 26.04.2014
comment
.timeit(1) немногим лучше, чем возиться с time.time, вам нужно больше итераций, чтобы выровнять шум. - person ; 26.04.2014
comment
Обычно это было бы правдой, но шум, вероятно, будет крошечным (в логарифмическом масштабе) с размером операций. На самом деле timeit(N) ничего делать не собирается; вы хотите .repeat. - person Veedrac; 26.04.2014
comment
Я не сомневаюсь в значении 7,9 секунды против 1,5 секунды, но я сомневаюсь в значении 1,515 секунды против 1,543 секунды (я бы сделал с версией append для [...] скорости). И да, вы бы хотели repeat(). - person ; 26.04.2014
comment
Оуи, но я просто отношусь к ним как к одному числу. Если 0,03 секунды важны для вас из-за масштаба ваших операций, вам лучше переписать это в Numpy. - person Veedrac; 26.04.2014
comment
bytearray.append() полностью сопоставим с 10 запусками с 7 миллионами байтов; не имеет значения, какой из них вы выберете, если вы используете bytearray. - person Martijn Pieters; 26.04.2014
comment
@Veedrac Я также рассматриваю их как одно и то же число, и, следовательно, (как сказал Марин) нет причин предпочитать одну версию bytearray другой на основе скорости. - person ; 26.04.2014