Существует ли более быстрый способ преобразования произвольного большого целого числа в последовательность байтов с прямым порядком байтов? - PullRequest
8 голосов
/ 05 декабря 2010

У меня есть этот код Python для этого:

from struct import pack as _pack

def packl(lnum, pad = 1):
    if lnum < 0:
        raise RangeError("Cannot use packl to convert a negative integer "
                         "to a string.")
    count = 0
    l = []
    while lnum > 0:
        l.append(lnum & 0xffffffffffffffffL)
        count += 1
        lnum >>= 64
    if count <= 0:
        return '\0' * pad
    elif pad >= 8:
        lens = 8 * count % pad
        pad = ((lens != 0) and (pad - lens)) or 0
        l.append('>' + 'x' * pad + 'Q' * count)
        l.reverse()
        return _pack(*l)
    else:
        l.append('>' + 'Q' * count)
        l.reverse()
        s = _pack(*l).lstrip('\0')
        lens = len(s)
        if (lens % pad) != 0:
            return '\0' * (pad - lens % pad) + s
        else:
            return s

Для преобразования 2**9700 - 1 в строку байтов на моем компьютере требуется примерно 174 мксек.Если я захочу использовать специфичный для Python 2.7 и Python 3.x метод bit_length, я могу сократить его до 159 usecs, предварительно выделив массив l в качестве точного правильного размера в самом начале и используя *Синтаксис 1007 * вместо l.append.

Могу ли я что-нибудь сделать, чтобы сделать это быстрее?Это будет использоваться для преобразования больших простых чисел, используемых в криптографии, а также некоторых (но не многих) меньших чисел.

Редактировать

В настоящее время это самый быстрый вариант вВ Python <3.2 принятый ответ занимает примерно половину времени в любом направлении: </p>

def packl(lnum, padmultiple=1):
    """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 = binascii.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(binascii.hexlify(bytestr), 16) if len(bytestr) > 0 else 0

В Python 3.2 класс int имеет функции to_bytes и from_bytes, которые могут выполнить это намного быстрее, чем приведенный выше метод.

Ответы [ 4 ]

10 голосов
/ 05 декабря 2010

Вот решение, вызывающее API-интерфейс Python / C через ctypes.В настоящее время он использует NumPy, но если NumPy не является опцией, это можно сделать просто с помощью ctypes.

import numpy
import ctypes
PyLong_AsByteArray = ctypes.pythonapi._PyLong_AsByteArray
PyLong_AsByteArray.argtypes = [ctypes.py_object,
                               numpy.ctypeslib.ndpointer(numpy.uint8),
                               ctypes.c_size_t,
                               ctypes.c_int,
                               ctypes.c_int]

def packl_ctypes_numpy(lnum):
    a = numpy.zeros(lnum.bit_length()//8 + 1, dtype=numpy.uint8)
    PyLong_AsByteArray(lnum, a, a.size, 0, 1)
    return a

На моей машине это в 15 раз быстрее, чем ваш подход.

Редактировать: Вот тот же код, использующий только ctypes и возвращающий строку вместо массива NumPy:

import ctypes
PyLong_AsByteArray = ctypes.pythonapi._PyLong_AsByteArray
PyLong_AsByteArray.argtypes = [ctypes.py_object,
                               ctypes.c_char_p,
                               ctypes.c_size_t,
                               ctypes.c_int,
                               ctypes.c_int]

def packl_ctypes(lnum):
    a = ctypes.create_string_buffer(lnum.bit_length()//8 + 1)
    PyLong_AsByteArray(lnum, a, len(a), 0, 1)
    return a.raw

Это еще в два раза быстрее, что в сумме дает скоростьна моей машине 30.

5 голосов
/ 22 июня 2011

Для полноты и для будущих читателей этого вопроса:

Начиная с Python 3.2, есть функции int.from_bytes() и int.to_bytes(), которые выполняют преобразованиемежду bytes и int объектами в порядке выбора байтов.

3 голосов
/ 11 февраля 2011

Просто хотел опубликовать продолжение ответа Свена (который прекрасно работает).Операция напротив - при переходе от произвольно длинных байтов к объекту Python Integer требуется следующее (поскольку я не могу найти функцию API PyLong_FromByteArray () C):

import binascii

def unpack_bytes(stringbytes):
    #binascii.hexlify will be obsolete in python3 soon
    #They will add a .tohex() method to bytes class
    #Issue 3532 bugs.python.org
    return int(binascii.hexlify(stringbytes), 16)
3 голосов
/ 05 декабря 2010

Полагаю, вы действительно должны просто использовать numpy, который, я уверен, что-то для этого встроен. Также может быть быстрее взломать модуль array. Но я все равно попробую.

IMX, создание генератора и использование осмысления списка и / или встроенного суммирования происходит быстрее, чем цикл, который добавляет к списку, потому что добавление может быть выполнено внутри. О, и «полоса» на большой струне должна быть дорогой.

Кроме того, некоторые стилевые моменты: особые случаи недостаточно особенные; и вы, похоже, не получили памятку о новой конструкции x if y else z. :) Хотя нам все равно это не нужно. ;)

from struct import pack as _pack


Q_size = 64
Q_bitmask = (1L << Q_size) - 1L


def quads_gen(a_long):
    while a_long:
        yield a_long & Q_bitmask
        a_long >>= Q_size


def pack_long_big_endian(a_long, pad = 1):
    if lnum < 0:
        raise RangeError("Cannot use packl to convert a negative integer "
                         "to a string.")
    qs = list(reversed(quads_gen(a_long)))
    # Pack the first one separately so we can lstrip nicely.
    first = _pack('>Q', qs[0]).lstrip('\x00')
    rest = _pack('>%sQ' % len(qs) - 1, *qs[1:])
    count = len(first) + len(rest)
    # A little math trick that depends on Python's behaviour of modulus
    # for negative numbers - but it's well-defined and documented
    return '\x00' * (-count % pad) + first + rest
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...