2017-10-03 5 views
8

J'ai un objet "bytes" et un masque "int", je veux faire un xor sur tous les octets avec mon masque. Je répète cette action sur de gros objets "bytes" (~ 4096 Ko).Python - "xor" chaque octet dans "octets" de la manière la plus efficace

C'est le code que j'ai qui fait le travail bien, seulement il est très consommateur d'UC et ralentit mon script:

# 'data' is bytes and 'mask' is int 
bmask = struct.pack('!I', mask) # converting the "int" mask to "bytes" of 4 bytes 
a = bytes(b^m for b, m in zip(data, itertools.cycle(bmask))) 

Le mieux que je pouvais trouver est ce qui est environ 20 fois plus rapide:

# 'data' is bytes and 'mask' is int 
# reversing the bytes of the mask 
bmask = struct.pack("<I", mask) 
mask = struct.unpack(">I", bmask)[0] 

# converting from bytes to array of "int"s 
arr = array.array("I", data) 

# looping over the "int"s 
for i in range(len(arr)): 
    arr[i] ^= mask 

# must return bytes 
a = bytes(arr) 

Mes questions sont les suivantes:

  1. Est-il possible de le faire (CPU-Wize) plus efficace?
  2. Existe-t-il un moyen plus "propre" de faire cela (sans nuire aux performances)?

P.S. si c'est important, j'utilise Python 3.5

+0

Qu'est-ce 'data'? Est-ce une liste ou des octets ou un itérateur ou ..? –

+0

Si c'est un goulot d'étranglement, il serait logique d'écrire une fonction C appelée de Python –

+0

"données" est des octets, je vais mettre à jour la question –

Répondre

3

Je ne pense pas que vous puissiez obtenir beaucoup plus rapidement que votre algorithme, en utilisant du Python pur. (Mais la réponse de Fabio Veronese montre que ce n'est pas vrai). Vous pouvez raser un peu de temps en faisant la boucle dans une liste de compréhension, mais ensuite cette liste doit être reconvertie en un tableau, et le tableau doit être converti en octets, de sorte qu'il utilise plus de RAM pour un bénéfice négligeable .

Cependant, vous pouvez faire ce beaucoup plus rapidement en utilisant Numpy. Voici une courte démo.

from time import perf_counter 
from random import randrange, seed 
import array 
import numpy as np 

seed(42) 

def timed(func): 
    ''' Timing decorator ''' 
    def wrapped(*args): 
     start = perf_counter() 
     result = func(*args) 
     stop = perf_counter() 
     print('{}: {:.6f} seconds'.format(func.__name__, stop - start)) 
     return result 
    wrapped.__name__ = func.__name__ 
    wrapped.__doc__ = func.__doc__ 
    return wrapped 

@timed 
def do_mask_arr1(data, mask): 
    arr = array.array("I", data) 
    # looping over the "int"s 
    for i in range(len(arr)): 
     arr[i] ^= mask 
    return arr.tobytes() 

@timed 
def do_mask_arr2(data, mask): 
    arr = array.array("I", data) 
    return array.array("I", [u^mask for u in arr]).tobytes() 

@timed 
def do_mask_numpy(data, mask): 
    return (np.fromstring(data, dtype=np.uint32)^mask).tobytes() 

@timed 
def make_data(datasize): 
    ''' Make some random bytes ''' 
    return bytes(randrange(256) for _ in range(datasize)) 

datasize = 100000 
mask = 0x12345678 
data = make_data(datasize) 

d1 = do_mask_arr1(data, mask) 
d2 = do_mask_arr2(data, mask) 
print(d1 == d2) 

d3 = do_mask_numpy(data, mask) 
print(d1 == d3) 

sortie typique

make_data: 0.751557 seconds 
do_mask_arr1: 0.026865 seconds 
do_mask_arr2: 0.025110 seconds 
True 
do_mask_numpy: 0.000438 seconds 
True 

testé en utilisant Python 3.6.0 sur un vieux noyau unique 32 bits machine à 2GHz fonctionnant sous Linux.

Je viens de faire un tour avec datasize = 4000000 et do_mask_numpy a pris 0.0422 secondes.

+0

En fait, il est possible d'obtenir un meilleur résultat en utilisant Python, de toute façon numpy reste le le plus rapide. –

2

Une alternative si vous ne voulez pas utiliser numpy. L'avantage vient de faire une seule comparaison, tout en augmentant la taille du masque au besoin (en fonction de la donnée).

@timed 
def do_mask_int(data, mask): 
    intdata = int.from_bytes(data, byteorder='little', signed=False) 
    strmask = format(mask,'0x') 
    strmask = strmask * ((intdata.bit_length() + 31) // 32) 
    n = intdata^int(strmask, 16) 
    return n.to_bytes(((n.bit_length() + 7) // 8), 'little') or b'\0' 

résultats sont comme suit:

make_data: 8.288754 seconds 
do_mask_arr1: 0.258530 seconds 
do_mask_arr2: 0.253095 seconds 
True 
do_mask_numpy: 0.010309 seconds 
True 
do_mask_int: 0.060408 seconds 
True 

crédite encore à numpy pour être plus rapide, mais peut-être on ne veut pas l'inclure dans un environnement de production.

:] Meilleur

+0

Très intelligent!Vous pourriez être en mesure de le rendre plus rapide en remplaçant ces divisions par des changements: 'a // 32' ->' a >> 5', 'a // 8' ->' a >> 3' –

+0

pas vraiment beaucoup 'make_data: 8.696361 secondes do_mask_arr1: 0.260604 secondes do_mask_arr2: 0.207433 secondes Vrai do_mask_numpy: 0.006003 secondes vrai do_mask_int: 0.050470 secondes tRUE' –