Niedawno zacząłem grać ze Scalą i natknąłem się na następujące rzeczy. Poniżej znajdują się 4 różne sposoby na przeglądanie linii pliku, robienie pewnych rzeczy i zapisywanie wyniku do innego pliku. Niektóre z tych metod działają tak, jak bym pomyślał (choć używa się do tego dużej ilości pamięci), a niektóre jedzą pamięć bez końca.Scala Iterable Memory Leaks
Pomysł polegał na owinięciu Iterable Scala's getLines Iterator. Nie obchodzi mnie, czy czyta plik kilka razy - właśnie tego oczekuję.
Oto mój kod repro:
class FileIterable(file: java.io.File) extends Iterable[String] {
override def iterator = io.Source.fromFile(file).getLines
}
// Iterator
// Option 1: Direct iterator - holds at 100MB
def lines = io.Source.fromFile(file).getLines
// Option 2: Get iterator via method - holds at 100MB
def lines = new FileIterable(file).iterator
// Iterable
// Option 3: TraversableOnce wrapper - holds at 2GB
def lines = io.Source.fromFile(file).getLines.toIterable
// Option 4: Iterable wrapper - leaks like a sieve
def lines = new FileIterable(file)
def values = lines
.drop(1)
//.map(l => l.split("\t")).map(l => l.reduceLeft(_ + "|" + _))
//.filter(l => l.startsWith("*"))
val writer = new java.io.PrintWriter(new File("out.tsv"))
values.foreach(v => writer.println(v))
writer.close()
plik to czytanie jest ~ 10GB z liniami 1 MB.
Pierwsze dwie opcje iterują plik przy użyciu stałej ilości pamięci (~ 100MB). Tego mogłem się spodziewać. Wadą tego jest to, że iterator może być użyty tylko raz i używa konwencji Call-by-Name Scala jako pseudo-iterable. (Dla odniesienia, równoważny kod C# wykorzystuje ~ 14MB)
Trzeci sposób wywołania toIterable zdefiniowany w TraverableOnce. Ten działa, ale wykorzystuje około 2 GB do wykonania tej samej pracy. Nie mam pojęcia, dokąd zmierza pamięć, ponieważ nie może buforować całego Iterable.
Czwarty jest najbardziej alarmujący - natychmiast wykorzystuje całą dostępną pamięć i generuje wyjątek OOM. Jeszcze dziwniejsze jest to, że robi to dla wszystkich operacji, które testowałem: upuszczanie, mapowanie i filtrowanie. Patrząc na implementacje, żadna z nich nie wydaje się utrzymywać dużego stanu (choć spadek wygląda na podejrzanego - dlaczego nie liczy się tylko z przedmiotami?). Jeśli nie wykonam żadnych operacji, to działa dobrze.
Moje przypuszczenie jest takie, że gdzieś zachowuje odniesienia do każdej z przeczytanych linii, chociaż nie potrafię sobie wyobrazić, w jaki sposób. Widziałem to samo użycie pamięci podczas przekazywania Iterables w Scala. Na przykład jeśli wezmę sprawę 3 (.toIterable) i przekażę ją do metody, która zapisuje Iterable [String] do pliku, widzę ten sam wybuch.
Wszelkie pomysły?
Interesujące ... Pochodzę z C#, gdzie wszystko jest załatwione.Z ciekawości - dlaczego mieliby buforować całą sekwencję jako domyślną opcję? –
Czy to oznacza również, że kiedy przekazuję sekwencję jako parametr Iterable [T], który będzie domyślnie buforowany? Jeśli tak, to czy to nie jest celem? Odniosłem wrażenie, że dane będą buforowane tylko w pamięci, gdy jednoznacznie o to poprosię poprzez toList, toArray, itd. –
Nie jestem naprawdę kwalifikowany do komentowania projektu biblioteki kolekcji (standardowe wprowadzenie do tematem jest [tutaj] (http://www.artima.com/scalazine/articles/scala_collections_architecture.html)). Naprawdę masz problemy tylko dlatego, że próbujesz rozszerzyć Iterable, nie będziesz miał problemu z Streamem lub Iteratorem. – themel