Простой вызов Python: быстрый побитовый XOR для буферов данных - PullRequest
51 голосов
/ 22 января 2010

Задача:

Выполнить побитовое XOR на двух буферах одинакового размера. Буферы должны быть типа python str, поскольку это традиционно тип буферов данных в python. Вернуть результирующее значение как str. Сделайте это как можно быстрее.

Входными данными являются две строки размером 1 мегабайт (2 ** 20 байт).

Задача состоит в том, чтобы существенно победить мой неэффективный алгоритм с использованием Python или существующих модулей Python сторонних производителей (упрощенные правила: или создать свой собственный модуль.) Маргинальные увеличения бесполезны.

from os import urandom
from numpy import frombuffer,bitwise_xor,byte

def slow_xor(aa,bb):
    a=frombuffer(aa,dtype=byte)
    b=frombuffer(bb,dtype=byte)
    c=bitwise_xor(a,b)
    r=c.tostring()
    return r

aa=urandom(2**20)
bb=urandom(2**20)

def test_it():
    for x in xrange(1000):
        slow_xor(aa,bb)

Ответы [ 11 ]

36 голосов
/ 01 февраля 2010

Первая попытка

Использование scipy.weave и SSE2 придает незначительное улучшение. Первый вызов немного медленнее, поскольку код должен быть загружен с диска и кэширован, последующие вызовы выполняются быстрее:

import numpy
import time
from os import urandom
from scipy import weave

SIZE = 2**20

def faster_slow_xor(aa,bb):
    b = numpy.fromstring(bb, dtype=numpy.uint64)
    numpy.bitwise_xor(numpy.frombuffer(aa,dtype=numpy.uint64), b, b)
    return b.tostring()

code = """
const __m128i* pa = (__m128i*)a;
const __m128i* pend = (__m128i*)(a + arr_size);
__m128i* pb = (__m128i*)b;
__m128i xmm1, xmm2;
while (pa < pend) {
  xmm1 = _mm_loadu_si128(pa); // must use unaligned access 
  xmm2 = _mm_load_si128(pb); // numpy will align at 16 byte boundaries
  _mm_store_si128(pb, _mm_xor_si128(xmm1, xmm2));
  ++pa;
  ++pb;
}
"""

def inline_xor(aa, bb):
    a = numpy.frombuffer(aa, dtype=numpy.uint64)
    b = numpy.fromstring(bb, dtype=numpy.uint64)
    arr_size = a.shape[0]
    weave.inline(code, ["a", "b", "arr_size"], headers = ['"emmintrin.h"'])
    return b.tostring()

Вторая попытка

Принимая во внимание комментарии, я повторно посетил код, чтобы выяснить, можно ли избежать копирования. Оказывается, я неправильно прочитал документацию по строковому объекту, и вот моя вторая попытка:

support = """
#define ALIGNMENT 16
static void memxor(const char* in1, const char* in2, char* out, ssize_t n) {
    const char* end = in1 + n;
    while (in1 < end) {
       *out = *in1 ^ *in2;
       ++in1; 
       ++in2;
       ++out;
    }
}
"""

code2 = """
PyObject* res = PyString_FromStringAndSize(NULL, real_size);

const ssize_t tail = (ssize_t)PyString_AS_STRING(res) % ALIGNMENT;
const ssize_t head = (ALIGNMENT - tail) % ALIGNMENT;

memxor((const char*)a, (const char*)b, PyString_AS_STRING(res), head);

const __m128i* pa = (__m128i*)((char*)a + head);
const __m128i* pend = (__m128i*)((char*)a + real_size - tail);
const __m128i* pb = (__m128i*)((char*)b + head);
__m128i xmm1, xmm2;
__m128i* pc = (__m128i*)(PyString_AS_STRING(res) + head);
while (pa < pend) {
    xmm1 = _mm_loadu_si128(pa);
    xmm2 = _mm_loadu_si128(pb);
    _mm_stream_si128(pc, _mm_xor_si128(xmm1, xmm2));
    ++pa;
    ++pb;
    ++pc;
}
memxor((const char*)pa, (const char*)pb, (char*)pc, tail);
return_val = res;
Py_DECREF(res);
"""

def inline_xor_nocopy(aa, bb):
    real_size = len(aa)
    a = numpy.frombuffer(aa, dtype=numpy.uint64)
    b = numpy.frombuffer(bb, dtype=numpy.uint64)
    return weave.inline(code2, ["a", "b", "real_size"], 
                        headers = ['"emmintrin.h"'], 
                        support_code = support)

Разница в том, что строка размещена внутри кода C. Невозможно выровнять его по 16-байтовой границе, как того требуют инструкции SSE2, поэтому невыровненные области памяти в начале и конце копируются с использованием побайтового доступа.

Входные данные в любом случае передаются с использованием числовых массивов, поскольку weave настаивает на копировании объектов Python str в std::string s. frombuffer не копирует, так что это нормально, но память не выровнена на 16 байт, поэтому нам нужно использовать _mm_loadu_si128 вместо более быстрого _mm_load_si128.

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

Задержка

Что касается времени, запись slow_xor в первом редактировании ссылалась на мою улучшенную версию (строковый бит по xor, uint64), я удалил эту путаницу. slow_xor относится к коду из оригинальных вопросов. Все сроки сделаны за 1000 прогонов.

  • slow_xor: 1,85 с (1x)
  • faster_slow_xor: 1,25 с (1,48х)
  • inline_xor: 0,95 с (1,95x)
  • inline_xor_nocopy: 0,32 с (5,78x)

Код был скомпилирован с использованием gcc 4.4.3, и я убедился, что компилятор на самом деле использует инструкции SSE.

36 голосов
/ 02 апреля 2010

Сравнение производительности: NumPy против Cython против C против Fortran против Boost.Python (pyublas)

| function               | time, usec | ratio | type         |
|------------------------+------------+-------+--------------|
| slow_xor               |       2020 |   1.0 | numpy        |
| xorf_int16             |       1570 |   1.3 | fortran      |
| xorf_int32             |       1530 |   1.3 | fortran      |
| xorf_int64             |       1420 |   1.4 | fortran      |
| faster_slow_xor        |       1360 |   1.5 | numpy        |
| inline_xor             |       1280 |   1.6 | C            |
| cython_xor             |       1290 |   1.6 | cython       |
| xorcpp_inplace (int32) |        440 |   4.6 | pyublas      |
| cython_xor_vectorised  |        325 |   6.2 | cython       |
| inline_xor_nocopy      |        172 |  11.7 | C            |
| xorcpp                 |        144 |  14.0 | boost.python |
| xorcpp_inplace         |        122 |  16.6 | boost.python |
#+TBLFM: $3=@2$2/$2;%.1f

Чтобы воспроизвести результаты, загрузите http://gist.github.com/353005 и введите make (для установки зависимостей введите: sudo apt-get install build-essential python-numpy python-scipy cython gfortran, зависимости для Boost.Python, pyublas не включены, поскольку для их работы требуется ручное вмешательство ) * +1010 *

Где:

И xor_$type_sig():

! xorf.f90.template
subroutine xor_$type_sig(a, b, n, out)
  implicit none
  integer, intent(in)             :: n
  $type, intent(in), dimension(n) :: a
  $type, intent(in), dimension(n) :: b
  $type, intent(out), dimension(n) :: out

  integer i
  forall(i=1:n) out(i) = ieor(a(i), b(i))

end subroutine xor_$type_sig

Используется из Python следующим образом:

import xorf # extension module generated from xorf.f90.template
import numpy as np

def xor_strings(a, b, type_sig='int64'):
    assert len(a) == len(b)
    a = np.frombuffer(a, dtype=np.dtype(type_sig))
    b = np.frombuffer(b, dtype=np.dtype(type_sig))
    return getattr(xorf, 'xor_'+type_sig)(a, b).tostring()

xorcpp_inplace() (Boost.Python, pyublas):

xor.cpp

#include <inttypes.h>
#include <algorithm>
#include <boost/lambda/lambda.hpp>
#include <boost/python.hpp>
#include <pyublas/numpy.hpp>

namespace { 
  namespace py = boost::python;

  template<class InputIterator, class InputIterator2, class OutputIterator>
  void
  xor_(InputIterator first, InputIterator last, 
       InputIterator2 first2, OutputIterator result) {
    // `result` migth `first` but not any of the input iterators
    namespace ll = boost::lambda;
    (void)std::transform(first, last, first2, result, ll::_1 ^ ll::_2);
  }

  template<class T>
  py::str 
  xorcpp_str_inplace(const py::str& a, py::str& b) {
    const size_t alignment = std::max(sizeof(T), 16ul);
    const size_t n         = py::len(b);
    const char* ai         = py::extract<const char*>(a);
    char* bi         = py::extract<char*>(b);
    char* end        = bi + n;

    if (n < 2*alignment) 
      xor_(bi, end, ai, bi);
    else {
      assert(n >= 2*alignment);

      // applying Marek's algorithm to align
      const ptrdiff_t head = (alignment - ((size_t)bi % alignment))% alignment;
      const ptrdiff_t tail = (size_t) end % alignment;
      xor_(bi, bi + head, ai, bi);
      xor_((const T*)(bi + head), (const T*)(end - tail), 
           (const T*)(ai + head),
           (T*)(bi + head));
      if (tail > 0) xor_(end - tail, end, ai + (n - tail), end - tail);
    }
    return b;
  }

  template<class Int>
  pyublas::numpy_vector<Int> 
  xorcpp_pyublas_inplace(pyublas::numpy_vector<Int> a, 
                         pyublas::numpy_vector<Int> b) {
    xor_(b.begin(), b.end(), a.begin(), b.begin());
    return b;
  }
}

BOOST_PYTHON_MODULE(xorcpp)
{
  py::def("xorcpp_inplace", xorcpp_str_inplace<int64_t>);     // for strings
  py::def("xorcpp_inplace", xorcpp_pyublas_inplace<int32_t>); // for numpy
}

Используется из Python следующим образом:

import os
import xorcpp

a = os.urandom(2**20)
b = os.urandom(2**20)
c = xorcpp.xorcpp_inplace(a, b) # it calls xorcpp_str_inplace()
17 голосов
/ 01 февраля 2010

Вот мои результаты для Cython

slow_xor   0.456888198853
faster_xor 0.400228977203
cython_xor 0.232881069183
cython_xor_vectorised 0.171468019485

Векторизация в Cython позволяет сэкономить примерно 25% цикла for на моем компьютере, однако более половины времени тратится на создание строки Python (оператор return) - я не думаю, что лишней копии можно избежать ( юридически), так как массив может содержать нулевые байты.

Недопустимым способом было бы передать строку Python и изменить ее на месте, что удвоило бы скорость функции.

xor.py

from time import time
from os import urandom
from numpy import frombuffer,bitwise_xor,byte,uint64
import pyximport; pyximport.install()
import xor_

def slow_xor(aa,bb):
    a=frombuffer(aa,dtype=byte)
    b=frombuffer(bb,dtype=byte)
    c=bitwise_xor(a,b)
    r=c.tostring()
    return r

def faster_xor(aa,bb):
    a=frombuffer(aa,dtype=uint64)
    b=frombuffer(bb,dtype=uint64)
    c=bitwise_xor(a,b)
    r=c.tostring()
    return r

aa=urandom(2**20)
bb=urandom(2**20)

def test_it():
    t=time()
    for x in xrange(100):
        slow_xor(aa,bb)
    print "slow_xor  ",time()-t
    t=time()
    for x in xrange(100):
        faster_xor(aa,bb)
    print "faster_xor",time()-t
    t=time()
    for x in xrange(100):
        xor_.cython_xor(aa,bb)
    print "cython_xor",time()-t
    t=time()
    for x in xrange(100):
        xor_.cython_xor_vectorised(aa,bb)
    print "cython_xor_vectorised",time()-t

if __name__=="__main__":
    test_it()

xor_.pyx

cdef char c[1048576]
def cython_xor(char *a,char *b):
    cdef int i
    for i in range(1048576):
        c[i]=a[i]^b[i]
    return c[:1048576]

def cython_xor_vectorised(char *a,char *b):
    cdef int i
    for i in range(131094):
        (<unsigned long long *>c)[i]=(<unsigned long long *>a)[i]^(<unsigned long long *>b)[i]
    return c[:1048576]
10 голосов
/ 22 января 2010

Простое ускорение - использовать больший «кусок»:

def faster_xor(aa,bb):
    a=frombuffer(aa,dtype=uint64)
    b=frombuffer(bb,dtype=uint64)
    c=bitwise_xor(a,b)
    r=c.tostring()
    return r

с uint64 также импортируется из numpy, конечно. Я timeit это за 4 миллисекунды против 6 миллисекунд для byte версии.

7 голосов
/ 01 февраля 2010

Ваша проблема не в скорости метода xOr NumPy, а скорее во всех преобразованиях буферизации / типов данных. Лично я подозреваю, что смысл этого поста в том, чтобы действительно похвастаться Python, потому что здесь вы обрабатываете три гигабайта данных в таймфреймах наравне с неинтерпретированными языками, которые по своей природе быстрее.

Приведенный ниже код показывает, что даже на моем скромном компьютере Python может xOr «aa» (1 МБ) и «bb» (1 МБ) превратиться в «c» (1 МБ) тысячу раз (всего 3 ГБ) за две секунды . Серьезно, сколько еще улучшений ты хочешь? Особенно из устного перевода! 80% времени было потрачено на вызовы «frombuffer» и «tostring». Фактический xOr-ing завершен в другие 20% времени. При 3 ГБ за 2 секунды вам будет трудно улучшить это по существу , даже просто используя memcpy в c.

В случае, если это был настоящий вопрос, а не просто скрытое хвастовство в отношении Python, ответ заключается в кодировании, чтобы минимизировать количество, количество и частоту преобразований типов, таких как «frombuffer» и «tostring». Фактически xOr'ing уже молниеносен.

from os import urandom
from numpy import frombuffer,bitwise_xor,byte,uint64

def slow_xor(aa,bb):
    a=frombuffer(aa,dtype=byte)
    b=frombuffer(bb,dtype=byte)
    c=bitwise_xor(a,b)
    r=c.tostring()
    return r

bb=urandom(2**20)
aa=urandom(2**20)

def test_it():
    for x in xrange(1000):
    slow_xor(aa,bb)

def test_it2():
    a=frombuffer(aa,dtype=uint64)
    b=frombuffer(bb,dtype=uint64)
    for x in xrange(1000):
        c=bitwise_xor(a,b);
    r=c.tostring()    

test_it()
print 'Slow Complete.'
#6 seconds
test_it2()
print 'Fast Complete.'
#under 2 seconds

В любом случае, «test_it2» выше выполняет столько же xOr-ing, как «test_it», но в 1/5 времени. 5-кратное улучшение скорости должно квалифицироваться как «существенное», нет?

4 голосов
/ 06 мая 2014

Python3 имеет int.from_bytes и int.to_bytes, таким образом:

x = int.from_bytes(b"a" * (1024*1024), "big")
y = int.from_bytes(b"b" * (1024*1024), "big")
(x ^ y).to_bytes(1024*1024, "big")

Это быстрее, чем IO, довольно сложно проверить, насколько быстро это выглядит, на моей машине выглядит как 0,018 .. 0,020 с . Странно, но "little" -андийское преобразование немного быстрее.

CPython 2.x имеет базовую функцию _PyLong_FromByteArray, он не экспортируется, но доступен через ctypes:

In [1]: import ctypes
In [2]: p = ctypes.CDLL(None)
In [3]: p["_PyLong_FromByteArray"]
Out[3]: <_FuncPtr object at 0x2cc6e20>

Детали Python 2 оставлены читателю в качестве упражнения.

4 голосов
/ 03 февраля 2010

Самый быстрый побитовый XOR - это «^». Я могу набрать это намного быстрее, чем "bitwise_xor"; -)

1 голос
/ 03 апреля 2010

Насколько сильно вам нужен ответ в виде строки? Обратите внимание, что метод c.tostring() должен копировать данные в c в новую строку, поскольку строки Python являются неизменяемыми (а c является изменяемым). Python 2.6 и 3.1 имеют тип bytearray, который действует как str (bytes в Python 3.x), за исключением того, что он изменчив.

Другая оптимизация - использование параметра out для bitwise_xor, чтобы указать, где хранить результат.

На моей машине я получаю

slow_xor (int8): 5.293521 (100.0%)
outparam_xor (int8): 4.378633 (82.7%)
slow_xor (uint64): 2.192234 (41.4%)
outparam_xor (uint64): 1.087392 (20.5%)

с кодом в конце этого поста. В частности, обратите внимание, что метод, использующий предварительно выделенный буфер, в два раза быстрее, чем создание нового объекта (при работе с 4-байтовыми (uint64) фрагментами). Это согласуется с тем, что более медленный метод выполняет две операции на блок (xor + copy) с более быстрым 1 (просто xor).

Кроме того, FWIW, a ^ b эквивалентно bitwise_xor(a,b), а a ^= b эквивалентно bitwise_xor(a, b, a).

Итак, 5-кратное ускорение без записи каких-либо внешних модулей:)

from time import time
from os import urandom
from numpy import frombuffer,bitwise_xor,byte,uint64

def slow_xor(aa, bb, ignore, dtype=byte):
    a=frombuffer(aa, dtype=dtype)
    b=frombuffer(bb, dtype=dtype)
    c=bitwise_xor(a, b)
    r=c.tostring()
    return r

def outparam_xor(aa, bb, out, dtype=byte):
    a=frombuffer(aa, dtype=dtype)
    b=frombuffer(bb, dtype=dtype)
    c=frombuffer(out, dtype=dtype)
    assert c.flags.writeable
    return bitwise_xor(a, b, c)

aa=urandom(2**20)
bb=urandom(2**20)
cc=bytearray(2**20)

def time_routine(routine, dtype, base=None, ntimes = 1000):
    t = time()
    for x in xrange(ntimes):
        routine(aa, bb, cc, dtype=dtype)
    et = time() - t
    if base is None:
        base = et
    print "%s (%s): %f (%.1f%%)" % (routine.__name__, dtype.__name__, et,
        (et/base)*100)
    return et

def test_it(ntimes = 1000):
    base = time_routine(slow_xor, byte, ntimes=ntimes)
    time_routine(outparam_xor, byte, base, ntimes=ntimes)
    time_routine(slow_xor, uint64, base, ntimes=ntimes)
    time_routine(outparam_xor, uint64, base, ntimes=ntimes)
1 голос
/ 24 января 2010

Если вы хотите быстро выполнять операции с типами данных массива, попробуйте Cython (cython.org). Если вы дадите ему правильные объявления, он сможет компилироваться в чистый код на языке c.

0 голосов
/ 30 января 2010

Самый быстрый способ (по скорости) будет делать то, что Макс. S рекомендуется. Реализуйте это в C.

Вспомогательный код для этой задачи должен быть довольно простым для написания. Это всего лишь одна функция в модуле, создающая новую строку и выполняющая xor. Это все. Когда вы реализовали один такой модуль, просто взять код в качестве шаблона. Или вы даже берете модуль, реализованный у кого-то другого, который реализует простой модуль расширения для Python, и просто выбрасываете все, что не нужно для вашей задачи.

По-настоящему сложная часть заключается в правильном выполнении RefCounter-Stuff. Но однажды осознав, как это работает, он становится управляемым - в том числе и потому, что стоящая перед нами задача действительно проста (выделите немного памяти и верните ее - параметры не нужно трогать (Ref-wise)).

...