51 votes

Défi Python simple: XOR bit à bit le plus rapide sur les tampons de données

Défi:

Effectuer un XOR au niveau du bit sur l'égalité des deux tampons de taille. Les tampons seront nécessaires pour être le python str type puisque c'est traditionnellement le type de tampons de données en python. De retour de la valeur résultante en tant que str. Le faire aussi vite que possible.

Les entrées sont deux 1 mégaoctet (2**20 octets) les chaînes de caractères.

Le défi est de sensiblement battre mon inefficace de l'algorithme à l'aide de python ou de tiers existants modules python (a assoupli les règles: ou créer votre propre module.) Des augmentations marginales sont inutiles.

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)

36voto

J.F. Sebastian Points 102961

Comparaison des performances: numpy vs Cython vs C ou Fortran vs coup de pouce.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

Reproduire les résultats, télécharger http://gist.github.com/353005 et tapez make (pour installer les dépendances, tapez: sudo apt-get install build-essential python-numpy python-scipy cython gfortran, dépendances Boost.Python, pyublas ne sont pas inclus en raison de qu'ils nécessitent une intervention manuelle pour le travail)

Où:

Et xor_$type_sig() sont:

! 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

Il est utilisé à partir de Python comme suit:

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
}

Il est utilisé à partir de Python comme suit:

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()

36voto

Torsten Marek Points 27554

Essayez D'Abord

À l'aide de scipy.weave et SSE2 intrinsèques donne une amélioration marginale. La première invocation est un peu plus lent étant donné que le code doit être chargé à partir du disque et de la mise en cache, à la suite d'invocations sont plus rapides:

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()

Deuxième Essai

En prenant en compte les commentaires, j'ai revisité le code pour savoir si la copie pourrait être évité. S'avère que j'ai lu la documentation de l'objet string de mal, alors, voici mon deuxième essai:

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)

La différence est que la chaîne est allouée à l'intérieur de la C code. Il est impossible d'avoir aligné à 16 octets-frontière comme requis par les instructions SSE2, donc la non alignés régions de la mémoire au début et à la fin sont copiés à l'aide de byte-sage d'accès.

Les données d'entrée est remis à l'aide de tableaux numpy de toute façon, parce qu' weave insiste sur la copie Python str objets d' std::strings. frombuffer ne copie pas, donc c'est très bien, mais la mémoire n'est pas aligné à 16 octets, donc nous avons besoin d'utiliser _mm_loadu_si128 , au lieu de la plus rapide, _mm_load_si128.

Au lieu d'utiliser _mm_store_si128, nous utilisons _mm_stream_si128, ce qui fera en sorte que toutes les écritures sont diffusées en direct à la mémoire principale dès que possible---de cette façon, la sortie de la matrice de ne pas utiliser de précieuses lignes de cache.

Timings

Comme pour les timings, l' slow_xor entrée dans la première édition visée à ma version améliorée (inline xor au niveau du bit, en uint64), j'ai supprimé cette confusion. slow_xor fait référence au code de l'origine des questions. Tous les horaires sont fait pour 1000 pistes.

  • slow_xor: 1.85 s (1x)
  • faster_slow_xor: 1.25 s (1.48 x)
  • inline_xor: 0.95 s (1,95 x)
  • inline_xor_nocopy: 0.32 s (5.78 x)

Le code a été compilé avec gcc 4.4.3 et j'ai vérifié que le compilateur utilise le jeu d'instructions SSE.

17voto

gnibbler Points 103484

Voici mes résultats pour cython

slow_xor   0.456888198853
faster_xor 0.400228977203
cython_xor 0.232881069183
cython_xor_vectorised 0.171468019485

Vectorising en cython rasages environ 25% de rabais pour la boucle sur mon ordinateur, Cependant, plus de la moitié du temps est consacré à la construction de la chaîne python ( return déclaration) - je ne pense pas que la copie supplémentaire peut être évité (légalement) le tableau peut contenir des octets nuls.

La manière illégale, serait de passer une chaîne Python et muter en place et permettrait de doubler la vitesse de la fonction.

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_.custode

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]

10voto

Alex Martelli Points 330805

Une accélération facile consiste à utiliser un plus gros "morceau":

 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
 

avec uint64 également importé de numpy de cours. I timeit ceci à 4 millisecondes, vs 6 millisecondes pour la version byte .

7voto

Joshua Points 1685

Votre problème n'est pas la vitesse de NumPy du xOr méthode, mais plutôt avec l'ensemble de la mise en mémoire tampon/conversions de types de données. Personnellement, je soupçonne que le point de ce post peut ont vraiment été pour se vanter de Python, parce que ce que vous faites ici, c'est le traitement de TROIS GIGAOCTETS de données dans les délais, à égalité avec les langages interprétés, qui sont intrinsèquement plus rapide.

Le code ci-dessous montre que, même à mon humble ordinateur Python peut xOr "aa" (1 MO) et "bb" (1 MO) en "c" (1 MO) d'un millier de fois (total de 3 go) en moins de deux secondes. Sérieusement, combien plus l'amélioration voulez-vous? En particulier à partir d'un langage interprété! 80% du temps a été passé en appelant "frombuffer" et "tostring". La réelle xOr-ing est réalisé dans les autres 20% du temps. À 3 GO en 2 secondes, vous serait difficile à améliorer que sensiblement la même en n'utilisant que memcpy dans c.

Dans le cas où c'était une vraie question, et pas seulement secrète se vanter de Python, la réponse est pour le code, de façon à minimiser le nombre, le montant et la fréquence de vos conversions de types tels que "frombuffer" et "tostring". Le réel qui utilise xOr est rapide comme l'éclair déjà.

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

De toute façon, le "test_it2" ci-dessus accomplit exactement le même montant de xOr-ing "test_it", mais à 1/5 du temps. 5x amélioration de la vitesse devrait être considéré comme "important", non?

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X