J'ai plusieurs fichiers séparés par des tabulations de 3 Go. Il y a 20 millions de lignes dans chaque fichier. Toutes les lignes doivent être traitées indépendamment, aucune relation entre deux lignes. Ma question est de savoir ce qui sera plus rapide A. Lire ligne par ligne en utilisant:
with open() as infile:
for line in infile:
Ou B. Lire le fichier en mémoire par morceaux et le traiter, disons 250 Mo à la fois?
Le traitement n'est pas très compliqué, je saisis juste la valeur dans la colonne 1 pour List1
, colonne2 à List2
etc. Peut-être besoin d'ajouter des valeurs de colonne ensemble.
J'utilise python 2.7 sur une boîte Linux qui a 30 Go de mémoire. ASCII Text.
Une façon d'accélérer les choses en parallèle? En ce moment, j'utilise l'ancienne méthode et le processus est très lent. L'utilisation d'un module CSVReader
va-t-elle aider? Je n'ai pas à le faire en python, tout autre langage ou idée d'utilisation de base de données est le bienvenu.
Il semble que votre code soit lié aux E/S. Cela signifie que le multitraitement ne va pas vous aider - si vous passez 90% de votre temps à lire à partir du disque, avoir 7 processus supplémentaires en attente à la prochaine lecture ne va rien aider.
Et, tout en utilisant un module de lecture CSV (que ce soit le csv
de stdlib ou quelque chose comme NumPy ou Pandas) peut être une bonne idée pour la simplicité, il est peu probable de faire beaucoup de différence dans les performances.
Pourtant, cela vaut la peine de vérifier que vous êtes vraiment lié aux E/S, au lieu de simplement deviner. Exécutez votre programme et voyez si votre utilisation du processeur est proche de 0% ou proche de 100% ou d'un cœur. Faites ce que Amadan a suggéré dans un commentaire, et exécutez votre programme avec juste pass
pour le traitement et voyez si cela coupe 5% du temps ou 70%. Vous pouvez même essayer de comparer avec une boucle sur os.open
Et os.read(1024*1024)
ou quelque chose et voir si c'est plus rapide.
Étant donné que vous utilisez Python 2.x, Python s'appuie sur la bibliothèque C stdio pour deviner la quantité de mémoire tampon à la fois, il peut donc être utile de forcer pour tamponner plus. La façon la plus simple de le faire est d'utiliser readlines(bufsize)
pour certains gros bufsize
. (Vous pouvez essayer différents nombres et les mesurer pour voir où se trouve le pic. D'après mon expérience , généralement, tout ce qui est de 64 Ko à 8 Mo est à peu près le même, mais en fonction de votre système, cela peut être différent, surtout si vous lisez, par exemple, un système de fichiers réseau avec un débit élevé mais une latence horrible qui submerge le débit contre la latence du disque physique réel et de la mise en cache du système d'exploitation.)
Ainsi, par exemple:
bufsize = 65536
with open(path) as infile:
while True:
lines = infile.readlines(bufsize)
if not lines:
break
for line in lines:
process(line)
En attendant, en supposant que vous êtes sur un système 64 bits, vous pouvez essayer d'utiliser mmap
au lieu de lire le fichier en premier lieu. Ce n'est certainement pas garanti pour être meilleur, mais il peut être mieux, selon votre système. Par exemple:
with open(path) as infile:
m = mmap.mmap(infile, 0, access=mmap.ACCESS_READ)
A Python mmap
est en quelque sorte un objet étrange - il agit comme un str
et comme un file
en même temps, donc vous peut, par exemple, itérer manuellement l'analyse des sauts de ligne, ou vous pouvez appeler readline
dessus comme s'il s'agissait d'un fichier. Les deux prendront plus de traitement de Python que d'itérer la fichier sous forme de lignes ou faisant un batch readlines
(car une boucle qui serait en C est maintenant en Python pur… bien que peut-être vous pouvez contourner cela avec re
, ou avec une simple extension Cython?) … Mais l'avantage d'E/S du système d'exploitation sachant ce que vous faites avec le mappage peut inonder l'inconvénient du processeur.
Malheureusement, Python n'expose pas l'appel madvise
que vous utiliseriez pour modifier les choses dans le but d'optimiser cela en C (par exemple, en définissant explicitement MADV_SEQUENTIAL
au lieu de faire deviner le noyau ou de forcer des pages énormes transparentes) - mais vous pouvez réellement ctypes
la fonction à partir de libc
.
Je sais que cette question est ancienne; mais je voulais faire une chose similaire, j'ai créé un cadre simple qui vous aide à lire et à traiter un gros fichier en parallèle. Laissant ce que j'ai essayé comme réponse.
Ceci est le code, je donne un exemple à la fin
def chunkify_file(fname, size=1024*1024*1000, skiplines=-1):
"""
function to divide a large text file into chunks each having size ~= size so that the chunks are line aligned
Params :
fname : path to the file to be chunked
size : size of each chink is ~> this
skiplines : number of lines in the begining to skip, -1 means don't skip any lines
Returns :
start and end position of chunks in Bytes
"""
chunks = []
fileEnd = os.path.getsize(fname)
with open(fname, "rb") as f:
if(skiplines > 0):
for i in range(skiplines):
f.readline()
chunkEnd = f.tell()
count = 0
while True:
chunkStart = chunkEnd
f.seek(f.tell() + size, os.SEEK_SET)
f.readline() # make this chunk line aligned
chunkEnd = f.tell()
chunks.append((chunkStart, chunkEnd - chunkStart, fname))
count+=1
if chunkEnd > fileEnd:
break
return chunks
def parallel_apply_line_by_line_chunk(chunk_data):
"""
function to apply a function to each line in a chunk
Params :
chunk_data : the data for this chunk
Returns :
list of the non-None results for this chunk
"""
chunk_start, chunk_size, file_path, func_apply = chunk_data[:4]
func_args = chunk_data[4:]
t1 = time.time()
chunk_res = []
with open(file_path, "rb") as f:
f.seek(chunk_start)
cont = f.read(chunk_size).decode(encoding='utf-8')
lines = cont.splitlines()
for i,line in enumerate(lines):
ret = func_apply(line, *func_args)
if(ret != None):
chunk_res.append(ret)
return chunk_res
def parallel_apply_line_by_line(input_file_path, chunk_size_factor, num_procs, skiplines, func_apply, func_args, fout=None):
"""
function to apply a supplied function line by line in parallel
Params :
input_file_path : path to input file
chunk_size_factor : size of 1 chunk in MB
num_procs : number of parallel processes to spawn, max used is num of available cores - 1
skiplines : number of top lines to skip while processing
func_apply : a function which expects a line and outputs None for lines we don't want processed
func_args : arguments to function func_apply
fout : do we want to output the processed lines to a file
Returns :
list of the non-None results obtained be processing each line
"""
num_parallel = min(num_procs, psutil.cpu_count()) - 1
jobs = chunkify_file(input_file_path, 1024 * 1024 * chunk_size_factor, skiplines)
jobs = [list(x) + [func_apply] + func_args for x in jobs]
print("Starting the parallel pool for {} jobs ".format(len(jobs)))
lines_counter = 0
pool = mp.Pool(num_parallel, maxtasksperchild=1000) # maxtaskperchild - if not supplied some weird happend and memory blows as the processes keep on lingering
outputs = []
for i in range(0, len(jobs), num_parallel):
print("Chunk start = ", i)
t1 = time.time()
chunk_outputs = pool.map(parallel_apply_line_by_line_chunk, jobs[i : i + num_parallel])
for i, subl in enumerate(chunk_outputs):
for x in subl:
if(fout != None):
print(x, file=fout)
else:
outputs.append(x)
lines_counter += 1
del(chunk_outputs)
gc.collect()
print("All Done in time ", time.time() - t1)
print("Total lines we have = {}".format(lines_counter))
pool.close()
pool.terminate()
return outputs
Disons par exemple, j'ai un fichier dans lequel je veux compter le nombre de mots dans chaque ligne, alors le traitement de chaque ligne ressemblerait à
def count_words_line(line):
return len(line.strip().split())
puis appelez la fonction comme:
parallel_apply_line_by_line(input_file_path, 100, 8, 0, count_words_line, [], fout=None)
En utilisant cela, j'obtiens une vitesse de ~ 8 fois par rapport à Vanilla lecture ligne par ligne sur un fichier échantillon de taille ~ 20 Go dans laquelle je fais un traitement modérément compliqué sur chaque ligne.