【问题标题】:Fastest bitwise xor between two multibyte binary data variables两个多字节二进制数据变量之间的最快按位异或
【发布时间】:2011-08-09 17:35:32
【问题描述】:

以下逻辑最快的实现方式是什么:

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 秒。

感谢任何贡献想法和代码的人。你们是好人!

【问题讨论】:

  • “最快”?好吧,运行时速度开销最低的方式是 C(或 Cython,用于心脏弱者)扩展。
  • 创建一个key:ord(key) + val:ord(val)(来自set(key) | set(data))的字典来保存许多ord调用?然后使用列表推导而不是字符串连接?
  • 这里有相当详细的看这个问题:stackoverflow.com/questions/2119761/…
  • 当异或一个 16M 的字符串时,我刚刚发布的那个占用了你当前最快的 42% 的时间,并且显着减少了内存。它也不依赖于 numpy 或内联汇编。
  • 另外,我不知道你打算用这个做什么,但这是一种完全可怕的加密方法。

标签: python bitwise-operators multibyte


【解决方案1】:

你所拥有的已经和 Python 一样快了。

如果你真的需要它更快,用 C 实现它。

【讨论】:

  • 我不这么认为,我认为如果 xoring 的长度为 20 字节长,它可能会更快
  • 不幸的是我在 C 方面非常不合格:(
  • @xavier:如何实现这个?
【解决方案2】:

如果len(data) 很大,您可能会看到xrange 的显着改进。实际上,您可以将 range 函数完全替换为 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 循环转换为普通的旧列表理解,但这会对可读性产生负面影响。无论如何,尝试一下,看看它是否更快。

【讨论】:

  • 您的实施没有明显的收获。但是提前计算 ord() 的 key 可以加快 10%
  • @Nikolai:嗯,这似乎不值得。列表理解怎么样? ''.join([chr(ord(val) ^ ord_key[idx % 1] for idx, val in enumerate(data)])
【解决方案3】:

未测试

不知道是不是更快

假设 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)

【讨论】:

  • 速度提高 20-30%。不错的结果,但我需要更多:)
【解决方案4】:

此代码应在 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

【讨论】:

  • 似乎 memoryview 仅在 python 2.7+ 中可用。我的是 2.6.5
  • @Nikolai Gorchilov:哦,哎呀。 :-) 您可以在 Python 2.6 中将 memoryview 替换为 buffer。或者,具有packlunpackl 的第一个版本不使用memoryview
【解决方案5】:

这是一个仅使用 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'

【讨论】:

    【解决方案6】:

    免责声明:正如其他发帖者所说,这是一种非常糟糕的文件加密方式。 This article 演示了如何简单地逆转这种混淆。

    首先,一个简单的异或算法:

    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)
    

    其次是包装异或算法:

    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
    

    【讨论】:

      【解决方案7】:

      根据我在最初帖子中的评论,如果您坚持使用numpy 进行键盘填充和按位异或,则可以相当快地处理大文件,如下所示:

      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)
      
      

      【讨论】:

        猜你喜欢
        • 2014-06-19
        • 1970-01-01
        • 2021-11-04
        • 2012-09-11
        • 1970-01-01
        • 1970-01-01
        • 2020-12-03
        • 2014-10-05
        • 1970-01-01
        相关资源
        最近更新 更多