# Numerik in Scala (Performance)



## 0x7F800000 (4. Sep 2011)

> - Wrapper-Objekte werden nur dann generiert, wenn sie auch notwendig sind. Wenn ich z.B. ein [c]Int[/c] zu den Elementen eines [c]Array[Int][/c] addiere, wird nichts gewrappt.
> - moderne JVMs optimieren Wrapper-Zugriffe mittlerweile weg
> - implizite Umwandlungen werden schon zur Compilezeit aufgelöst, ein Aufruf einer [c]Numeric[/c]-Methode ist genauso schnell wie etwa der Aufruf einer [c]java.lang.Math[/c]-Methode


Echt? Ok, dann wäre ich endlos dankbar zu erfahren, was ich denn hier falsch mache:

```
object InfixWrapperTest{
  
  trait AdditiveMonoid[A]{
    def zero: A
    def add(x: A, y: A): A	
  
    class Ops(x: A){
	  def +(y: A) = add(x, y)
	}
  }
  
  implicit object DoubleIsAdditive extends AdditiveMonoid[Double]{
    def zero = 0d
	def add(x: Double, y: Double) = x + y
  }
  
  implicit def wrapToOps[T](x: T)(implicit monoid: AdditiveMonoid[T]): AdditiveMonoid[T]#Ops = new monoid.Ops(x)
  
  def measuringTime[A](f: => A) = {
    val t = System.currentTimeMillis
	val result = f
	(result, System.currentTimeMillis - t)
  }
  
  def sumGeneric[X](a: Array[X])(implicit monoid: AdditiveMonoid[X]) = {
    var result = monoid.zero
	var i = 0
	while (i < a.length){ 
	  result += a(i)
	  i += 1
	}
	result
  }
  
  def sumDirect(a: Array[Double]) = {
    var result = 0d
	var i = 0
	while (i < a.length){ 
	  result += a(i)
	  i += 1
	}
	result
  }
  
  def main(args: Array[String]){
    
	val arr = new Array[Double](1000000)
	for (i <- 0 until arr.size) arr(i) = math.sin(i*i + i +137)
	
	val warmup = 200
	val runs = 1000
	
	var genericTime = 0L
	var directTime = 0L
	
	println("visueller Check der Korrektheit der Resultate:")
	println(measuringTime{ sumDirect(arr) })
	println(measuringTime{ sumGeneric(arr) })
	
	println("Messung der Laufzeit")
	for (i <- 1 to (warmup + runs)){
	  val g = measuringTime{ sumGeneric(arr) }._2
	  val d = measuringTime{ sumDirect(arr) }._2
	  if( i > warmup){
	    genericTime += g
		directTime += d
	  }
	  println(i + "/" + (warmup + runs))
	}
	println("generisch: " + (genericTime.toDouble / runs) + "ms")
	println("direkt: " + (directTime.toDouble / runs) + "ms")
  }
}
```
Ergebnis:

```
generisch: 82.383ms
direkt: 5.298ms
```
Das ist alles unter scala 2.9.1 für jvm 1.6.irgendwas kompiliert, mit dem original scalac-compiler. Wo kommt der Faktor 16 her, wenn das alles so toll wegoptimiert wird? Wenn man fehlendes fma miteinrechnet, und das ganze mit C++ vergleicht, dann ist das Faktor 32. Wenn man das also so schreiben würde, dann bräuchte man für Scala einen mini-cluster mit 32 Rechnern, um gegen denselben C++ - Code auf einem Studi-Laptop anzukommen.

_Edit (Illuvatar): Abgetrennt aus diesem Post_


----------



## escalate (4. Sep 2011)

Habe mich noch nicht so wirklich mit Optimierungen in Scala beschäftigt, aber ich habe es mal ausprobiert (Scala 2.9.0.1, sun-jdk 1.6.0_26):

Orginal:

generisch: 8.95ms
direkt: 1.734ms

einzige Änderung @specialized

```
25: def sumGeneric[@specialized(Double) X](a: Array[X])(implicit monoid: AdditiveMonoid[X]) = {
```

generisch: 1.776ms
direkt: 1.635ms

Das sieht doch schon wesentlich freundlicher aus. Das Wrapper-Objekt sieht man aber im Bytecode noch.


----------



## 0x7F800000 (4. Sep 2011)

escalate hat gesagt.:


> Orginal:
> 
> generisch: 8.95ms
> direkt: 1.734ms
> ...


Huh? Was geht los... :autsch: Moment mal^^


----------



## 0x7F800000 (4. Sep 2011)

[Liebe Mods! könnte bitte jemand den Thread absägen und irgendwo in Scala-Unterforum reinschmeißen?]

Also:
Ohne @specialized: generische Version ist um faktor 16 langsamer
Mit @specialized: um faktor 10 langsamer (ehrlich gesagt wundert es mich ziemlich, warum es überhaupt schneller wird?)

Dass die generische Version bei dir plötzlich genauso schnell läuft, wie die spezielle, ist für mich ehrlich gesagt ein ziemlicher Shock: ich habe mich eigentlich schon damit abgefunden, dass Scala dieses blöde infix-rumgewrappe nicht vernünftig inlinen kann :shock:

Was mich jetzt brennend interessiert ist, was du denn damit angestellt hast, bzw was ich denn schon wieder verpasst habe? ???:L

Hab hier scalac compiler version 2.9.0.1 und jdk1.7.0-b147, habe versucht das ganze mit verschiedenen Optionen wie -optimise zu kompilieren oder mit -server zu starten: bringt alles nichts, Faktor 10 bleibt. Wie kommt es zu solch einem gigantischen Unterschied? Wenn dieser Unterschied weg wäre, dann wäre alles nochmal *wesentlich* geiler... Es würde schlagartig meine Einstufung von Numeric von _gruselig_ auf _genial_ ändern.

PS: ehrlich gesagt sieht das ganze zu gut aus, um wahr zu sein: als ob man beim herumexperimentieren dieselbe methode zweimal gemessen hätte... :bahnhof:


----------



## JavaCoda (4. Sep 2011)

0x7F800000 hat gesagt.:


> implicit def wrapToOps[T](x: T)(implicit monoid: AdditiveMonoid[T]): AdditiveMonoid[T]#Ops = new monoid.Ops(x)


<ironie>
        Wow, Scala ist wahrlich ein Meilenstein für die Verständlichkeit und Nachvollziehbarkeit der Fachkonzepte aus dem Code
</ironie>
Mal ehrlich: Man muss angesichts solcher Spielereien und dem abwechselnden Zuckerschock, den man durch die Syntax kriegt, und den Wutanfällen, die einem die miserable IDE-Unterstützung bescheren, schon harter Fanboy sein, oder den ganzen Tag mit rosaroten Kontaktlinsen durch die Gegend taumeln, um zu glauben, dass so ein Bastelkram je den Status "nette aber unnütze Spielerei" verlassen und "Enterprise-ready" sein werden.


----------



## 0x7F800000 (4. Sep 2011)

JavaCoda hat gesagt.:


> <ironie>
> Wow, Scala ist wahrlich ein Meilenstein für die Verständlichkeit und Nachvollziehbarkeit der Fachkonzepte aus dem Code
> </ironie>


Das ist ein kurzer und bestens nachvollziehbarer Einzeiler, vorausgesetzt man kennt die Syntax und entwirft gelegentlich irgendwelche Scala-Bibliotheken. Dies ist definitiv die Library-Designer Sicht. Der Benutzer sieht am Ende nur das hier: 

```
val z = x + y
```
Bei deinen Enterprise-Anwendungen müssen die Benutzer ja auch nicht im Backend rumwühlen, sondern kriegen eine bunte Benutzeroberfläche... 



> Mal ehrlich: Man muss angesichts solcher Spielereien und dem abwechselnden Zuckerschock, den man durch die Syntax kriegt, und den Wutanfällen, die einem die miserable IDE-Unterstützung bescheren, schon harter Fanboy sein, oder den ganzen Tag mit rosaroten Kontaktlinsen durch die Gegend taumeln, um zu glauben, dass so ein Bastelkram je den Status "nette aber unnütze Spielerei" verlassen und "Enterprise-ready" sein werden.


Mit "Enterprise-ready" kenn ich mich nicht aus, interessiert mich nicht sonderlich. Ich will einfach ein Werkzeug haben, mit dem man vernünftig rechnen kann. Java ist um mehrere Größenordnungen besser als die meisten verstümmelten C-Parodien (sogenannte "mathematische Programmiersprachen"), aber man kann damit leider auch nicht vernünftig rechnen. Wenn Du nächstes mal auf irgendeinen Experimentalphysiker o.ä. triffst, dann kannst Du gerne versuchen ihm zu erläutern, wie "enterprise-ready" der ganze spitze Klammersalat und völlig.unleserliche(Formeln) sind, ich versuche es in der Zwischenzeit mit Scala, wenn niemand was dagegen hat


----------



## escalate (4. Sep 2011)

0x7F800000 hat gesagt.:


> Was mich jetzt brennend interessiert ist, was du denn damit angestellt hast, bzw was ich denn schon wieder verpasst habe? ???:L


Gute Frage, eigentlich gar nichts. Habe es einfach ohne irgendwelche Optimierungen ausgeführt. 
Mit Scala 2.9.1 und JDK 1.7.0 bekomme ich die gleichen Ergebnisse. Ich habe es auch mal mit meiner Timing-Funktion getestet, wieder gleiche Zeiten.

Der Bytecode hier wird für die Spezialisierung auf double ausgeworfen:

```
public double sumGeneric$mDc$sp(double[], InfixWrapperTest$AdditiveMonoid);
  Code:
   0:   aload_2
   1:   invokeinterface #66,  1; //InterfaceMethod InfixWrapperTest$AdditiveMonoid.zero:()Ljava/lang/Object;
   6:   invokestatic    #203; //Method scala/runtime/BoxesRunTime.unboxToDouble:(Ljava/lang/Object;)D
   9:   dstore_3
   10:  iconst_0
   11:  istore  5
   13:  iload   5
   15:  aload_1
   16:  arraylength
   17:  if_icmpge       52
   20:  aload_0
   21:  dload_3
   22:  invokestatic    #181; //Method scala/runtime/BoxesRunTime.boxToDouble:(D)Ljava/lang/Double;
   25:  aload_2
   26:  invokevirtual   #77; //Method wrapToOps:(Ljava/lang/Object;LInfixWrapperTest$AdditiveMonoid;)LInfixWrapperTest$AdditiveMonoid$Ops;
   29:  aload_1
   30:  iload   5
   32:  daload
   33:  invokestatic    #181; //Method scala/runtime/BoxesRunTime.boxToDouble:(D)Ljava/lang/Double;
   36:  invokevirtual   #85; //Method InfixWrapperTest$AdditiveMonoid$Ops.$plus:(Ljava/lang/Object;)Ljava/lang/Object;
   39:  invokestatic    #203; //Method scala/runtime/BoxesRunTime.unboxToDouble:(Ljava/lang/Object;)D
   42:  dstore_3
   43:  iload   5
   45:  iconst_1
   46:  iadd
   47:  istore  5
   49:  goto    13
   52:  dload_3
   53:  dreturn
```

Die nicht-generische Variante:


```
public double sumDirect(double[]);
  Code:
   0:   dconst_0
   1:   dstore_2
   2:   iconst_0
   3:   istore  4
   5:   iload   4
   7:   aload_1
   8:   arraylength
   9:   if_icmpge       28
   12:  dload_2
   13:  aload_1
   14:  iload   4
   16:  daload
   17:  dadd
   18:  dstore_2
   19:  iload   4
   21:  iconst_1
   22:  iadd
   23:  istore  4
   25:  goto    5
   28:  dload_2
   29:  dreturn
```

Den Unterschied sieht man, der Wrapper ist schon noch da. Nächste Frage ist dann, was die JVM daraus macht


----------



## 0x7F800000 (4. Sep 2011)

escalate hat gesagt.:


> Gute Frage, eigentlich gar nichts. Habe es einfach ohne irgendwelche Optimierungen ausgeführt.
> Mit Scala 2.9.1 und JDK 1.7.0 bekomme ich die gleichen Ergebnisse.


Also der stinknormale compiler direkt von scala-lang.org und stinknormale jdk1.7.0 von der Oracle seite? (nicht irgendeine leicht divergente OpenJDK Version oder so?) :reflect: Vom Betriebssystem und der schnelligkeit des Rechners kann es nicht abhängen? Kann das irgendwie sein, dass scala beim launchen die falsche jvm-version benutzt oder dass irgendwelche für die Optimierung verantwortlichen Systemvariablen falsch gesetzt sind? ???:L

Der decompilierte bytecode sieht bei mir jeweils 1:1 exakt genauso aus, Zeichen für Zeichen. Was könnte denn noch die JVM dran hindern, vernünftig zu funktionieren? An zu schwacher Hardware kann's ja wohl nicht liegen, so Aufwendig kann das Bisschen Inlining ja nicht sein? :bahnhof:


----------



## escalate (4. Sep 2011)

0x7F800000 hat gesagt.:


> Also der stinknormale compiler direkt von scala-lang.org und stinknormale jdk1.7.0 von der Oracle seite?


Ja.



> (nicht irgendeine leicht divergente OpenJDK Version oder so?)


Ich habe mit denen hier getestet, immer das Gleiche...

```
Java(TM) SE Runtime Environment (build 1.6.0_26-b03)
Java HotSpot(TM) 64-Bit Server VM (build 20.1-b02, mixed mode)
```


```
OpenJDK Runtime Environment (IcedTea6 1.10.3) (Gentoo build 1.6.0_22-b22)
OpenJDK 64-Bit Server VM (build 20.0-b11, mixed mode)
```


```
Java(TM) SE Runtime Environment (build 1.7.0-b147)
Java HotSpot(TM) 64-Bit Server VM (build 21.0-b17, mixed mode)
```



> Vom Betriebssystem und der schnelligkeit des Rechners kann es nicht abhängen?


Das wäre ja seltsam  64bit?



> Kann das irgendwie sein, dass scala beim launchen die falsche jvm-version benutzt oder dass irgendwelche für die Optimierung verantwortlichen Systemvariablen falsch gesetzt sind? ???:L



Die Version von Java habe ich mir so geben lassen:

```
scala -J-version
```


----------



## 0x7F800000 (4. Sep 2011)

escalate hat gesagt.:


> Ich habe mit denen hier getestet, immer das Gleiche...
> 
> ```
> Java(TM) SE Runtime Environment (build 1.6.0_26-b03)
> ...


Ja, immer das gleiche: 64-Bit Server VM...

Bei mir ist's 32-bit Client VM (kleiner laptop :autsch

```
java version "1.7.0"
Java(TM) SE Runtime Environment (build 1.7.0-b147)
Java HotSpot(TM) Client VM (build 21.0-b17, mixed mode, sharing)
```
Aber launchen in -server (-Jserver? -J-server? wohin mit dem minus?) mode bringt auch nicht die erwünschte "agressive Optimierung" :noe: (außerdem bezieht sich dieses "mixed" doch eh darauf, dass es wie Client startet und wie Server optimiert, oder wie war das nochmal?)


----------



## 0x7F800000 (4. Sep 2011)

> The 32-bit JRE (any version) doesn't even have the Server VM.


Das wird's wohl sein? -.-
Da muss ich wohl der JVM für die erleuchtenden Fehlermeldungen danken: "-server" wird nämlich völlig stillschweigend akzeptiert... :autsch:


----------



## mvitz (4. Sep 2011)

Falls es euch hilft, hier mal meine Werte:


```
generisch: 76.163ms
direkt: 5.038ms
```

Versionen:
java -version 
	
	
	
	





```
java version "1.6.0_26"
Java(TM) SE Runtime Environment (build 1.6.0_26-b03)
Java HotSpot(TM) Client VM (build 20.1-b02, mixed mode, sharing)
```

scala -J-version 
	
	
	
	





```
Welcome to Scala version 2.9.1.final (Java HotSpot(TM) Client VM, Java 1.6.0_26)
```


----------



## 0x7F800000 (4. Sep 2011)

Danke mvitz!

Das bekräftigt meine Vermutung, dass Scala-Code nur unter 64bit-Server-JVM ordentlich läuft, diesen Effekt kannte ich von Java bisher nicht, das ist eine außerordentlich wichtige Feststellung, danke euch beiden!


----------



## mvitz (4. Sep 2011)

So, damit es noch vollständig ist:

Windows7 32bit Server VM -->

```
generisch: 21.328ms
direkt: 4.21ms
```

Server VM ist im JDK enthalten, und wurde per 
	
	
	
	





```
SET JAVA_OPTS="-server"
```
 gesetzt.

scala -J-version --> 
	
	
	
	





```
Welcome to Scala version 2.9.1.final (Java HotSpot(TM) Server VM, Java 1.6.0_26)
```


----------



## 0x7F800000 (4. Sep 2011)

Hm, das Manöver mit JAVA_OPTS="-server" hat nicht geholfen:

```
Fehler: Hauptklasse server konnte nicht gefunden oder geladen werden
```
Läuft so nicht...

Aber OK! Ist nicht so wichtig, der Plan ist klar: 64-bit Server JVM installieren, freuen! 

Leute, ich bin euch außerordentlich dankbar, wäre mir das nicht aufgefallen, hätte ich in dem nächsten halben Jahr eine MENGE Code gegen die Wand gefahren, aber jetzt muss ich lediglich einige wenige Design-Entscheidungen korrigieren. 

Erstmal keine weiteren Fragen


----------

