C #: File.ReadLines () vs File.ReadAllLines () - και γιατί θα πρέπει να με νοιάζει;

Πριν από μερικές εβδομάδες, εγώ και δύο από τις Ομάδες με τις οποίες συνεργάζομαι ήρθε μια συζήτηση για αποτελεσματικούς τρόπους επεξεργασίας μεγάλων αρχείων κειμένου.

Αυτό προκάλεσε κάποιες άλλες προηγούμενες συζητήσεις που είχα στο παρελθόν σχετικά με αυτό το θέμα και, ειδικότερα, σχετικά με τη χρήση της επιστροφής απόδοσης στο C # (για το οποίο μάλλον θα μιλήσω σε μια μελλοντική ανάρτηση στο blog). Έτσι, σκέφτηκα ότι θα ήταν μια καλή πρόκληση να δείξουμε πώς ο C # μπορεί να κλιμακώσει αποτελεσματικά όταν πρόκειται για επεξεργασία μεγάλων κομματιών δεδομένων.

Η πρόκληση

Έτσι, το πρόβλημα που συζητείται είναι:

  • Ας υποθέσουμε ότι υπάρχει ένα μεγάλο αρχείο CSV, π.χ. ~ 500MB για εκκινητές
  • Το πρόγραμμα πρέπει να περάσει από κάθε γραμμή του αρχείου, να το αναλύσει και να κάνει κάποιους υπολογισμούς βασισμένους σε χάρτη / περιορισμό

Και το ερώτημα σε αυτό το σημείο της συζήτησης είναι:

Ποιος είναι ο πιο αποτελεσματικός τρόπος για να γράψετε τον κώδικα που είναι σε θέση να επιτύχει αυτόν τον στόχο; Ενώ συμμορφώνεται επίσης με:
θ) ελαχιστοποίηση της χρησιμοποιούμενης μνήμης και
ii) ελαχιστοποίηση των γραμμών του κώδικα του προγράμματος (σε εύλογο βαθμό, φυσικά)

Για χάρη του επιχειρήματος, θα μπορούσαμε να χρησιμοποιήσουμε το StreamReader, αλλά αυτό θα οδηγούσε στην εγγραφή περισσότερου κώδικα που χρειαζόταν και, στην πραγματικότητα, ο C # έχει ήδη τις μεθόδους διευκόλυνσης File.ReadAllLines () και File.ReadLines (). Πρέπει λοιπόν να τα χρησιμοποιήσουμε!

Δείξε μου τον κωδικό

Για χάρη του παραδείγματος, ας εξετάσουμε ένα πρόγραμμα που:

  1. Λαμβάνει ένα αρχείο κειμένου ως είσοδο όπου κάθε γραμμή είναι ένας ακέραιος αριθμός
  2. Υπολογίζει το άθροισμα όλων των αριθμών στο αρχείο

Για χάρη αυτού του παραδείγματος, θα παραλείψουμε αρκετά μηνύματα επικύρωσης :-)

Στην C # αυτό μπορεί να επιτευχθεί με τον ακόλουθο κώδικα:

var sumOfLines = File.ReadAllLines (filePath)
    .Επιλέξτε (γραμμή => int.Parse (γραμμή))
    .Αθροισμα()

Πολύ απλό, έτσι;

Τι συμβαίνει όταν τροφοδοτούμε αυτό το πρόγραμμα με ένα μεγάλο αρχείο;

Εάν εκτελέσουμε αυτό το πρόγραμμα για να επεξεργαστούμε ένα αρχείο 100MB, αυτό είναι αυτό που παίρνουμε:

  • 2GB μνήμης RAM που καταναλώνεται για να ολοκληρωθεί αυτό το computing
  • Πολλά GC (κάθε κίτρινο στοιχείο είναι μια διαδρομή GC)
  • 18 δευτερόλεπτα για να ολοκληρωθεί η εκτέλεση
BTW, τροφοδοτώντας ένα αρχείο 500MB σε αυτόν τον κώδικα προκάλεσε το πρόγραμμα να συντριβεί με μια OutOfMemoryException Διασκέδαση, σωστά;

Ας δοκιμάσουμε τώρα το αρχείο File.ReadLines ()

Ας αλλάξουμε τον κώδικα για να χρησιμοποιήσετε το File.ReadLines () αντί για το File.ReadAllLines () και δείτε πώς πηγαίνει:

var sumOfLines = File.ReadLines (filePath)
    .Επιλέξτε (γραμμή => int.Parse (γραμμή))
    .Αθροισμα()

Όταν το τρέχουμε, παίρνουμε τώρα:

  • 12MB μνήμης RAM που καταναλώθηκαν, αντί για 2GB (!!)
  • Μόνο 1 εκτέλεση GC
  • 10 δευτερόλεπτα για να ολοκληρωθεί, αντί για 18

Γιατί συμβαίνει αυτό?

TL? DR η διαφορά κλειδιού είναι ότι το File.ReadAllLines () δημιουργεί μια συμβολοσειρά [] που περιέχει κάθε γραμμή του αρχείου, απαιτώντας αρκετή μνήμη για να φορτώσει ολόκληρο το αρχείο. ως αντίθετο στο File.ReadLines () που τροφοδοτεί το πρόγραμμα κάθε γραμμή κάθε φορά, απαιτώντας μόνο τη μνήμη να φορτώσει μία γραμμή.

Με περισσότερες λεπτομέρειες:

Το αρχείο File.ReadAllLines () διαβάζει ολόκληρο το αρχείο ταυτόχρονα και επιστρέφει μια συμβολοσειρά [] όπου κάθε στοιχείο του πίνακα αντιστοιχεί σε μια γραμμή του αρχείου. Αυτό σημαίνει ότι το πρόγραμμα χρειάζεται τόσο περισσότερη μνήμη όσο το μέγεθος του αρχείου για να φορτώσει τα περιεχόμενα από το αρχείο. Επιπλέον, η απαραίτητη μνήμη για την ανάλυση όλων των στοιχειοσειρών string στο int και στη συνέχεια να υπολογίσει το Sum ()

Από την άλλη πλευρά, το File.ReadLines () δημιουργεί έναν απαριθμητή στο αρχείο, διαβάζοντάς το γραμμικά με γραμμή (στην πραγματικότητα χρησιμοποιώντας το StreamReader.ReadLine ()). Αυτό σημαίνει ότι κάθε γραμμή διαβάζεται, μετατρέπεται και προστίθεται στο μερικό άθροισμα σε λειτουργία γραμμής-γραμμής.

συμπέρασμα

Αυτό το θέμα μπορεί να φαίνεται σαν λεπτομέρεια εφαρμογής χαμηλού επιπέδου, αλλά στην πραγματικότητα είναι πολύ σημαντικό επειδή καθορίζει τον τρόπο με τον οποίο ένα πρόγραμμα θα κλιμακωθεί όταν τροφοδοτείται με ένα μεγάλο σύνολο δεδομένων.

Είναι σημαντικό για τους προγραμματιστές λογισμικού να είναι σε θέση να προβλέψουν τέτοιου είδους καταστάσεις, γιατί κανείς δεν ξέρει ποτέ αν κάποιος πρόκειται να προσφέρει μια μεγάλη εισροή που δεν είχε προβλεφθεί στο στάδιο της ανάπτυξης.

Επίσης, η LINQ είναι αρκετά ευέλικτη ώστε να μπορεί να χειριστεί αυτά τα δύο σενάρια απρόσκοπτα και να παρέχει εξαιρετική απόδοση όταν χρησιμοποιείται με κώδικα που παρέχει "ροή" αξιών.

Αυτό σημαίνει ότι όλα δεν χρειάζεται να είναι μια λίστα ή μια T [] που σημαίνει ότι ολόκληρο το σύνολο δεδομένων φορτώνεται στη μνήμη. Με τη χρήση του IEnumerable κάνουμε τον κώδικα γενικής χρήσης να χρησιμοποιείται με μεθόδους που παρέχουν ολόκληρο το σύνολο δεδομένων στη μνήμη ή που παρέχουν τιμές στη λειτουργία "ροής".