Первая попытка
Использование 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.