2015-12-17 16 views
5

Sto elaborando file di testo da 60 GB o più. I file sono separati in una sezione di intestazione di lunghezza variabile e una sezione di dati. Ho tre funzioni:Clojure - elabora file di grandi dimensioni con poca memoria

  • head? un predicato per distinguere le linee di intestazione da linee di dati
  • process-header processo un'intestazione spezzata
  • process-data processo spezzata indicatori
  • Le funzioni di elaborazione asincrono accedere e modificare una database in memoria

Sono avanzato su un metodo di lettura file da un altro thread SO, che dovrebbe creare un sequenza pigra di linee. L'idea era di elaborare alcune linee con una funzione, quindi cambiare la funzione una volta e continuare l'elaborazione con la funzione successiva.

(defn lazy-file 
    [file-name] 
    (letfn [(helper [rdr] 
      (lazy-seq 
      (if-let [line (.readLine rdr)] 
       (cons line (helper rdr)) 
       (do (.close rdr) nil))))] 
    (try 
     (helper (clojure.java.io/reader file-name)) 
     (catch Exception e 
     (println "Exception while trying to open file" file-name))))) 

Io lo uso con qualcosa come

(let [lfile (lazy-file "my-file.txt")] 
    (doseq [line lfile :while head?] 
    (process-header line)) 
    (doseq [line (drop-while head? lfile)] 
    (process-data line))) 

Anche se funziona, è piuttosto inefficiente per un paio di motivi:

  • Invece di limitarsi a chiamare process-head fino a raggiungere i dati e quindi continuando con process-data, devo filtrare le righe di intestazione e elaborarle, quindi riavviare l'analisi dell'intero file e rilasciare tutte le righe di intestazione per elaborare i dati. Questo è l'esatto contrario di ciò che lazy-file intendeva fare.
  • La visualizzazione del consumo di memoria mi mostra che il programma, sebbene apparentemente pigro, si accumula per utilizzare la quantità di RAM necessaria per mantenere il file in memoria.

Quindi, qual è un modo più efficiente e idiomatico per lavorare con il mio database?

Un'idea potrebbe utilizzare un multimetodo per elaborare intestazione e dati dipendenti dal valore del predicato head?, ma suppongo che ciò avrebbe un impatto serio sulla velocità, specialmente in quanto vi è un solo caso in cui il risultato del predicato cambia da sempre fedele a sempre falso. Non l'ho ancora fatto.

Sarebbe meglio utilizzare un altro modo per creare la seq di riga e analizzarla con iterate? Questo mi lascerebbe comunque la necessità di usare: while e: drop-while, credo.

Nella mia ricerca, l'accesso al file NIO è stato menzionato un paio di volte, il che dovrebbe migliorare l'utilizzo della memoria. Non sono ancora riuscito a scoprire come usarlo in modo idiomatico in clojure.

Forse ho ancora una cattiva comprensione dell'idea generale, come deve essere trattato il file?

Come sempre, qualsiasi aiuto, idee o suggerimenti per i tut sono molto apprezzati.

risposta

0

ci sono diverse cose da considerare qui:

  1. Utilizzo della memoria

    Ci sono rapporti che Leiningen potrebbe aggiungere roba che si traduce nel mantenere i riferimenti alla testa, anche se doseq particolare non vale a il capo della sequenza che sta elaborando, cf. this SO question. Prova a verificare il tuo reclamo "utilizza la quantità di RAM necessaria per mantenere il file in memoria" senza utilizzare lein repl.

  2. linee Parsing

    Invece di usare due loop con doseq, si potrebbe anche usare un approccio loop/recur. Cosa si aspetta di essere l'analisi sarebbe un secondo argomento come questo (non testata):

    (loop [lfile (lazy-file "my-file.txt") 
          parse-header true] 
         (let [line (first lfile)] 
          (if [and parse-header (head? line)] 
           (do (process-header line) 
            (recur (rest lfile) true)) 
           (do (process-data line) 
            (recur (rest lfile) false))))) 
    

    C'è un'altra opzione qui, che sarebbe quello di incorporare le funzioni di elaborazione nella vostra funzione di lettura dei file. Quindi, invece di creare semplicemente una nuova riga e restituirla, è sufficiente elaborarla subito, in genere è possibile passare la funzione di elaborazione come argomento invece di codificarla con hard-coding.

    Il codice corrente sembra che l'elaborazione sia un effetto collaterale. Se è così, allora potresti probabilmente eliminare la pigrizia se incorpori l'elaborazione. È necessario elaborare l'intero file comunque (o così sembra) e lo si fa su una base per linea. L'approccio lazy-seq sostanzialmente allinea solo una singola riga letta con una singola chiamata di elaborazione. Il tuo bisogno di pigrizia sorge nella soluzione attuale perché separa la lettura (l'intero file, riga per riga) dall'elaborazione. Se invece si sposta l'elaborazione di una riga nella lettura, non è necessario eseguirla pigramente.

+0

Grazie per la risposta. Ieri ho scritto alcuni casi di test per fare benchmarking. Si è scoperto che ** A) ** Non è la lettura stessa che consuma tanta memoria, sembra essere il database (btw, le mie affermazioni sul consumo di memoria derivano dall'esecuzione dell'applicazione compilata) ** B) * * '' 'lazy-file''' e' '' line-seq''' si comportano in modo approssimativo, considerando la velocità e l'uso della memoria ** C) ** Sorprendentemente i metodi multimodali e un approccio loop-recurere richiedono circa il 150% del tempo necessario per aprire il file due volte e utilizzare while/drop-while – waechtertroll

+0

Mi piace il tuo modo di ricorsione durante la lettura del file. La prossima idea che proverò è che avrò il parser dell'header per controllare se la riga successiva è una linea dati (stile iteratore) e, in caso affermativo, trampolino via al parser dei dati. Se-else su ogni riga è molto lento, ma i file sono ben definiti in poche centinaia di righe di intestazione e centinaia di milioni di linee di dati, e la lettura della testa richiede meno di mezzo secondo. Non sono ancora sicuro, come combinare trampolino ed iteratore ... – waechtertroll

2

È necessario utilizzare le funzioni di libreria standard.

line-seq, with-open e doseq faranno facilmente il lavoro.

Qualcosa nella linea di:

(with-open [rdr (clojure.java.io/reader file-path)] 
    (doseq [line (line-seq rdr)] 
    (if (head? line) 
     (process-header line) 
     (process-data line)))) 
+0

Grazie per il tuo suggerimento. Il metodo '' 'lazy-file''' che sto usando è stato implementato quando ho iniziato ad imparare il clojure, riposto in un modulo io e usato da lì. L'effetto netto di esso è veramente lo stesso di usare '' 'line-seq'''. – waechtertroll

+0

Un'altra informazione di lato, l'approccio if-else per linea si è dimostrato molto più lento (fattore 1.5) rispetto al modo in cui stavo prendendo. Significativamente perché il tempo di esecuzione qui è misurato in ore ;-) – waechtertroll

+0

Capisco la tua argomentazione su 'lazy-file', ma occupandomi di aprire e chiudere il file rende questa funzione più difficile da testare. – kawas44

Problemi correlati