Самый быстрый побитовый xor между двумя многобайтовыми переменными двоичных данных

Каков самый быстрый способ реализовать следующую логику:

def xor(data, key):
    l = len(key)

    buff = ""
    for i in range(0, len(data)):
        buff += chr(ord(data[i]) ^ ord(key[i % l]))
    return buff

В моем случае key — это 20-байтовый дайджест sha1, а data — это двоичные данные длиной от 20 байт до нескольких (1, 2, 3) мегабайт.

ОБНОВЛЕНИЕ:

Хорошо, ребята. Вот реализация в 3,5 раза быстрее, которая разбивает данные и ключ на куски по 4, 2 или 1 байт (в моем случае большую часть времени это целое число длиной 4 байта):

def xor(data, key):
    index = len(data) % 4
    size = (4, 1, 2, 1)[index]
    type = ('L', 'B', 'H', 'B')[index]
    key_len = len(key)/size
    data_len = len(data)/size
    key_fmt = "<" + str(key_len) + type;
    data_fmt = "<" + str(data_len) + type;

    key_list = struct.unpack(key_fmt, key)
    data_list = struct.unpack(data_fmt, data)

    result = []
    for i in range(data_len):
        result.append (key_list[i % key_len] ^ data_list[i])

    return struct.pack(data_fmt, *result)

Использует много памяти, но в моем случае это не имеет большого значения.

Есть идеи, как увеличить скорость еще в несколько раз? :-)

ПОСЛЕДНЕЕ ОБНОВЛЕНИЕ:

Хорошо, хорошо... numpy сделал свою работу. Это просто молниеносно:

def xor(data, key):
    import numpy, math

    # key multiplication in order to match the data length
    key = (key*int(math.ceil(float(len(data))/float(len(key)))))[:len(data)]

    # Select the type size in bytes       
    for i in (8,4,2,1):
        if not len(data) % i: break

    if i == 8: dt = numpy.dtype('<Q8');
    elif i == 4: dt = numpy.dtype('<L4');
    elif i == 2: dt = numpy.dtype('<H2');
    else: dt = numpy.dtype('B');

    return numpy.bitwise_xor(numpy.fromstring(key, dtype=dt), numpy.fromstring(data, dtype=dt)).tostring()

Первоначальная реализация требовала 8 минут 50 секунд для обработки гигабайта, вторая - около 2 минут 30 секунд, а последняя всего.... 0 минут 10 секунд.

Спасибо всем, кто поделился идеями и кодом. Вы отличные ребята!


person Nikolai Gorchilov    schedule 20.04.2011    source источник
comment
Самый быстрый? Что ж, путь с наименьшими накладными расходами по скорости выполнения — это расширение C (или Cython, для слабонервных).   -  person    schedule 20.04.2011
comment
Создать словарь key:ord(key) + val:ord(val) (из set(key) | set(data)), чтобы сохранить много вызовов ord? Затем использовать понимание списка вместо конкатенации строк?   -  person TryPyPy    schedule 20.04.2011
comment
Здесь довольно подробно рассматривается эта проблема: stackoverflow.com/questions/2119761/   -  person Scott Griffiths    schedule 21.04.2011
comment
Тот, который я только что опубликовал, занимает около 42% времени вашего текущего самого быстрого при xoring строке 16M и значительно меньше памяти. Он также не зависит от numpy или встроенной сборки.   -  person Omnifarious    schedule 21.04.2011
comment
Кроме того, я понятия не имею, что вы собираетесь с этим делать, но это совершенно ужасный метод шифрования.   -  person Omnifarious    schedule 21.04.2011
comment
Я добавил новую версию своего кода, которая намного короче и понятнее. И да, это действительно намного быстрее, чем версия, которая у вас сейчас самая быстрая. Я знаю, это не кажется ужасно интуитивным.   -  person Omnifarious    schedule 21.04.2011
comment
Re: Ваше последнее (и предыдущее) обновление -- обычно вы можете ускорить их, всегда обрабатывая как можно больше данных, используя наибольший размер фрагмента (4 или 8 байтов) и обрабатывая любой остаток с помощью соответствующего меньшего размера (1 или 2 байта). Последнее, будучи такой небольшой суммой, возможно, не стоит оптимизировать.   -  person martineau    schedule 23.04.2011
comment
@ Николай Горчилов Я попробовал ваш код, но получил ошибку типа данных относительно «Q8», он недавно изменился? я гуглил и ничего не нашел   -  person Amir Afianian    schedule 31.12.2015
comment
Ваш алгоритм заполнения для расширения ключа, вероятно, добавляет 9 секунд к вашим вычислениям. Я бы оптимизировал приведенный выше код следующим образом: key = np.pad(key, (0, len(data) - len(key)), 'wrap')   -  person Nadeem Douba    schedule 08.11.2019


Ответы (7)


Не испытано

Не знаю, быстрее ли

предположим, что len(mystring) кратно 4

def xor(hash,mystring):
    s = struct.Struct("<L")

    v1 = memoryview(hash)

    tab1 = []
    for i in range(5):
        tab1.append(s.unpack_from(v1,i*4)

    v2 = memoryview(mystring)
    tab2=[]
    for i in range(len(mystring)/4):
        tab2.append(s.unpack_from(v1,i*4))
    tab3 = []
    try:
        for i in range(len(mystring)/20):
            for j in range(5):
               tab3.append(s.pack(tab1[j]^tab2[5*i+j]))
    expect IndexError:
        pass
    return "".join(tab3)
person Xavier Combelle    schedule 20.04.2011
comment
20-30% увеличение скорости. Хороший результат, но мне нужно гораздо больше :) - person Nikolai Gorchilov; 21.04.2011

Если len(data) большое, вы можете увидеть значительное улучшение по сравнению с xrange. Фактически, вы можете полностью заменить функцию диапазона на enumerate. Вы также можете использовать список вместо добавления к строке.

def xor(data, key):
    l = len(key)
    buff = []
    for idx, val in enumerate(data):
        buff.append(chr(ord(val) ^ ord(key[idx % l]))
    return ''.join(buff)

Я не замерял время, но навскидку я ожидаю, что это будет немного быстрее для больших объемов данных. Убедитесь, что вы измеряете каждое изменение.

Если профилирование предполагает, что вызов ord() на самом деле требует времени, вы можете запустить его для всех значений в key заранее, чтобы сохранить вызов в цикле.

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

person nmichaels    schedule 20.04.2011
comment
Никакой ощутимой выгоды с вашей реализацией. Но предварительное вычисление ord() для ключа ускоряет его на 10% - person Nikolai Gorchilov; 21.04.2011
comment
@Nikolai: Ну, вряд ли это того стоит. Как насчет понимания списка? ''.join([chr(ord(val) ^ ord_key[idx % 1] for idx, val in enumerate(data)]) - person nmichaels; 21.04.2011

Этот код должен работать в Python 2.6+, включая Py3k.

from binascii import hexlify as _hexlify
from binascii import unhexlify as _unhexlify


def packl(lnum, padmultiple=0):
    """Packs the lnum (which must be convertable to a long) into a
    byte string 0 padded to a multiple of padmultiple bytes in size. 0
    means no padding whatsoever, so that packing 0 result in an empty
    string.  The resulting byte string is the big-endian two's
    complement representation of the passed in long."""

    if lnum == 0:
        return b'\0' * padmultiple
    elif lnum < 0:
        raise ValueError("Can only convert non-negative numbers.")
    s = hex(lnum)[2:]
    s = s.rstrip('L')
    if len(s) & 1:
        s = '0' + s
    s = _unhexlify(s)
    if (padmultiple != 1) and (padmultiple != 0):
        filled_so_far = len(s) % padmultiple
        if filled_so_far != 0:
            s = b'\0' * (padmultiple - filled_so_far) + s
    return s

def unpackl(bytestr):
    """Treats a byte string as a sequence of base 256 digits
    representing an unsigned integer in big-endian format and converts
    that representation into a Python integer."""

    return int(_hexlify(bytestr), 16) if len(bytestr) > 0 else 0

def xor(data, key):
    dlen = len(data)
    klen = len(key)
    if dlen > klen:
        key = key * ((dlen + klen - 1) // klen)
    key = key[:dlen]
    result = packl(unpackl(data) ^ unpackl(key))
    if len(result) < dlen:
         result = b'\0' * (dlen - len(result)) + result
    return result

Это также будет работать в Python 2.7 и 3.x. Преимущество этого метода в том, что он намного проще, чем предыдущий, и в то же время делает то же самое примерно за то же время:

from binascii import hexlify as _hexlify
from binascii import unhexlify as _unhexlify

def xor(data, key):
    dlen = len(data)
    klen = len(key)
    if dlen > klen:
        key = key * ((dlen + klen - 1) // klen)
    key = key[:dlen]
    data = int(_hexlify(data), 16)
    key = int(_hexlify(key), 16)
    result = (data ^ key) | (1 << (dlen * 8 + 7))
    # Python 2.6/2.7 only lines (comment out in Python 3.x)
    result = memoryview(hex(result))
    result = (result[4:-1] if result[-1] == 'L' else result[4:])
    # Python 3.x line
    #result = memoryview(hex(result).encode('ascii'))[4:]
    result = _unhexlify(result)
    return result
person Omnifarious    schedule 21.04.2011
comment
Похоже, что memoryview доступен только в python 2.7+. у меня 2.6.5 - person Nikolai Gorchilov; 21.04.2011
comment
@Николай Горчилов: Ой, ой. :-) Вы можете заменить memoryview на buffer в Python 2.6. В качестве альтернативы первая версия, в которой есть packl и unpackl, не использует memoryview. - person Omnifarious; 21.04.2011

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

во-первых, простой алгоритм xor:

def xor(a,b,_xor8k=lambda a,b:struct.pack("!1000Q",*map(operator.xor,
                    struct.unpack("!1000Q",a),
                    struct.unpack("!1000Q",b)))
        ):
    if len(a)<=8000:
        s="!%iQ%iB"%divmod(len(a),8)
        return struct.pack(s,*map(operator.xor,
            struct.unpack(s,a),
            struct.unpack(s,b)))
    a=bytearray(a)
    for i in range(8000,len(a),8000):
        a[i-8000:i]=_xor8k(
            a[i-8000:i],
            b[i-8000:i])
    a[i:]=xor(a[i:],b[i:])
    return str(a)

во-вторых, алгоритм обертывания xor:

def xor_wrap(data,key,_struct8k=struct.Struct("!1000Q")):
    l=len(key)
    if len(data)>=8000:
        keyrpt=key*((7999+2*l)//l)#this buffer is accessed with whatever offset is required for a given 8k block
        #this expression should create at most 1 more copy of the key than is needed
        data=bytearray(data)
        offset=-8000#initial offset, set to zero on first loop iteration
        modulo=0#offset used to access the repeated key
        for offset in range(0,len(data)-7999,8000):
            _struct8k.pack_into(data,offset,*map(operator.xor,
                _struct8k.unpack_from(data,offset),
                _struct8k.unpack_from(keyrpt,modulo)))
            modulo+=8000;modulo%=l
        offset+=8000
    else:offset=0;keyrpt=key*(len(data)//l+1)#simple calculation guaranteed to be enough
    rest=len(data)-offset
    srest=struct.Struct("!%iQ%iB"%divmod(len(data)-offset,8))
    srest.pack_into(data,offset,*map(operator.xor,
        srest.unpack_from(data,offset),
        srest.unpack_from(keyrpt,modulo)))
    return data
person Richard Thiessen    schedule 05.05.2017

Вот версия, которая использует только встроенные и стандартные модули Python, что кажется очень быстрым, хотя я не сравнивал ее с вашей версией numpy. Как указано, он использует несколько оптимизированных функций преобразования из Python Cryptography Toolkit.

# Part of the Python Cryptography Toolkit
# found here:
# http://www.google.com/codesearch/p?hl=en#Y_gnTlD6ECg/trunk/src/gdata/Crypto/Util/number.py&q=lang:python%20%22def%20long_to_bytes%22&sa=N&cd=1&ct=rc

# Improved conversion functions contributed by Barry Warsaw, after
# careful benchmarking

import struct

def long_to_bytes(n, blocksize=0):
    """long_to_bytes(n:long, blocksize:int) : string
    Convert a long integer to a byte string.

    If optional blocksize is given and greater than zero, pad the front of the
    byte string with binary zeros so that the length is a multiple of
    blocksize.
    """
    # after much testing, this algorithm was deemed to be the fastest
    s = ''
    n = long(n)
    pack = struct.pack
    while n > 0:
        s = pack('>I', n & 0xffffffffL) + s
        n = n >> 32
    # strip off leading zeros
    for i in range(len(s)):
        if s[i] != '\000':
            break
    else:
        # only happens when n == 0
        s = '\000'
        i = 0
    s = s[i:]
    # add back some pad bytes.  this could be done more efficiently w.r.t. the
    # de-padding being done above, but sigh...
    if blocksize > 0 and len(s) % blocksize:
        s = (blocksize - len(s) % blocksize) * '\000' + s
    return s

def bytes_to_long(s):
    """bytes_to_long(string) : long
    Convert a byte string to a long integer.

    This is (essentially) the inverse of long_to_bytes().
    """
    acc = 0L
    unpack = struct.unpack
    length = len(s)
    if length % 4:
        extra = (4 - length % 4)
        s = '\000' * extra + s
        length = length + extra
    for i in range(0, length, 4):
        acc = (acc << 32) + unpack('>I', s[i:i+4])[0]
    return acc


# original code in SO question
def xor_orig(data, key):
    l = len(key)

    buff = ""
    for i in range(0, len(data)):
        buff += chr(ord(data[i]) ^ ord(key[i % l]))
    return buff

# faster pure python version
def xor_new(data, key):
    import math

    # key multiplication in order to match the data length
    key = (key*int( math.ceil(float(len(data))/float(len(key)))))[:len(data)]

    # convert key and data to long integers
    key_as_long = bytes_to_long(key)
    data_as_long = bytes_to_long(data)

    # xor the numbers together and convert the result back to a byte string
    return long_to_bytes(data_as_long ^ key_as_long)

if __name__=='__main__':
    import random
    import sha

    TEST_DATA_LEN = 100000

    data = ''.join(chr(random.randint(0, 255)) for i in xrange(TEST_DATA_LEN))
    key = sha.new(data).digest()

    assert xor_new(data, key) == xor_orig(data, key)
    print 'done'
person martineau    schedule 26.04.2011

Следуя моему комментарию в начальном посте, вы можете довольно быстро обрабатывать большие файлы, если будете придерживаться numpy для заполнения клавиш и побитового XOR, например:

import numpy as np

# ...

def xor(key, data):

    data = np.fromstring(data, dtype=np.byte)
    key = np.fromstring(key, dtype=np.byte)

    # Pad the key to match the data length
    key = np.pad(key, (0, len(data) - len(key)), 'wrap')

    return np.bitwise_xor(key, data)

person Nadeem Douba    schedule 08.11.2019

То, что у вас есть, уже настолько быстро, насколько это возможно в Python.

Если вам действительно нужно это быстрее, реализуйте это на C.

person orlp    schedule 20.04.2011
comment
Я так не думаю, я думаю, что было бы быстрее, если бы xoring был на длинном числе длиной 20 байт. - person Xavier Combelle; 20.04.2011
comment
К сожалению, я очень некомпетентен в C :( - person Nikolai Gorchilov; 20.04.2011
comment
@xavier: как это реализовать? - person Nikolai Gorchilov; 20.04.2011