бързо 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

Работи добре, когато байтовите обекти са малки, но ако xor големи обекти (няколко MB) отнема много време (няколко часа). Как мога да го направя по-бързо?


person anton-tsyganenko    schedule 26.04.2014    source източник
comment
Това може да помогне.   -  person devnull    schedule 26.04.2014


Отговори (4)


Когато XORing 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 обект на всяка итерация, можете просто да добавите byte/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

Времената на Martijn Pieters са малко по-различни от моите:

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 Аз също ги третирам като едно и също число и следователно (както каза Marijn) няма причина да предпочитам една версия на байтов масив пред друга въз основа на скоростта. - person ; 26.04.2014