2010-10-07 7 views
7

I napotkał następujący kod w JAXMag Scala specjalnym wydaniu:Zastosowanie leniwe val do buforowania reprezentację ciąg

package com.weiglewilczek.gameoflife 

case class Cell(x: Int, y: Int) { 
    override def toString = position 
    private lazy val position = "(%s, %s)".format(x, y) 
} 

Czy użycie lazy val w powyższym kodzie zapewniają znacznie większą wydajność niż następującego kodu?

package com.weiglewilczek.gameoflife 

case class Cell(x: Int, y: Int) { 
    override def toString = "(%s, %s)".format(x, y) 
} 

A może jest to tylko przypadek niepotrzebnej optymalizacji?

+0

Leniwy powinien wypchnąć obliczenie wartości, dopóki nie zostanie wywołane. Jednak dobre pytanie. Czy tworzy obiekt do przechowywania funkcji oceny? – wheaties

+2

Po prostu wiesz, możesz uzyskać ten sam format ciągu, nazywając '(x, y) .toString' –

Odpowiedz

0

Klasy przypadków są, z definicji, niezmienne. Każda wartość zwracana przez toString również będzie niezmienna. Dlatego sensowne jest zasadniczo "buforowanie" tej wartości przez użycie leniwego val. Z drugiej strony, dostarczona implementacja toString ma niewiele więcej niż domyślny toString dostarczony przez wszystkie klasy przypadków. Nie zdziwiłbym się, gdyby klasa walizek toString użyła leniwego vala pod spodem.

+3

Niezmienny z definicji? scala> case class Foo (var x: Int) zdefiniowana klasa Foo –

+0

Nie wiedziałam, że modyfikator 'var' może być użyty na elementach danych klasy case. Powinienem był powiedzieć: "klasa przypadków podana w pytaniu jest niezmienna z definicji" chociaż wciąż myślę, że to, co tam zrobiliście, to czyste zło. –

1

W pierwszym urywku position zostanie obliczony tylko raz, na żądanie, zostanie wywołana metoda [when | if] toString. W drugim fragmencie korpus zostanie ponownie oceniony przy każdym wywołaniu metody. Biorąc pod uwagę, że x i y nie można zmienić, jest to bezsensowne i należy zapisać wartość toString.

0

Wygląda mi to na mikrooptymalizację. JVM jest w stanie zadbać o takie przypadki.

+6

Nie ma absolutnie nic w JVM, która zapamiętuje wyniki metody, tak jak poza zasięgiem lokalnym (tj. Jeśli wywołasz .toString dwukrotnie w tej samej metodzie, JVM ma szansę na buforowanie prostych zwrotów metod.) Trwałe buforowanie takich wyników jest poza zakresem, co jest dobre, ponieważ (w tym przypadku) automatyczne zapamiętywanie spowodowałoby znaczny wzrost śladu pamięci –

19

Jedną z rzeczy, o których należy pamiętać w przypadku leniwych valsów, jest to, że chociaż są one obliczane tylko raz, każdy dostęp do nich jest chroniony przez podwójnie sprawdzane opakowanie zamykające. Jest to konieczne, aby zapobiec próbom zainicjowania wartości dwóch różnych wątków w tym samym czasie dzięki zabawnym wynikom. Teraz podwójne sprawdzanie blokowania jest dość wydajne (teraz, gdy faktycznie działa w JVM) i nie wymaga w większości przypadków przechwytywania blokady, ale jest więcej narzut niż prosty dostęp do wartości.

Dodatkowo (i nieco oczywista), poprzez buforowanie reprezentacji łańcuchowej obiektu, wyraźnie zamieniamy cykle procesora na możliwie duże zwiększenie wykorzystania pamięci. Łańcuchy w wersji "def" mogą być zbędne, podczas gdy te w wersji "lazy val" nie będą.

Wreszcie, jak to zwykle bywa w przypadku pytań dotyczących wydajności, hipotezy oparte na teorii nie mają prawie żadnego znaczenia bez analizy porównawczej opartej na faktach. Nigdy się nie dowiesz bez profilowania, więc możesz spróbować i zobaczyć.

13

toString można bezpośrednio zmienić za pomocą lazy val.

scala> case class Cell(x: Int, y: Int) { 
    | override lazy val toString = {println("here"); "(%s, %s)".format(x, y)} 
    | } 
defined class Cell 

scala> {val c = Cell(1, 2); (c.toString, c.toString)} 
here 
res0: (String, String) = ((1, 2),(1, 2)) 

Uwaga że def nie może przesłonić val - można dokonać jedynie członkowie bardziej stabilny w klasie sub.

+4

+1, nie wiedział, że "def" może zostać przesłonięte przez 'lazy val '. :) – missingfaktor