What is the status of specialization in Scala 3? While I was recently posting a few bug reports regarding @specialized for Scala 2.13, I was made aware Dotty 0.27 was simply ignoring @specialized.
What is the current status and plan?
will @specialized be implemented eventually?
will some automatic specialization be implemented?
This is very different from the @specialized annotation, as it only specializes methods. It canāt export specialized types like @specialized does, so it is quite limited. It is also less practical, as it requires users to instantiate each specialization manually, and separately-instantiated specializations will not be shared.
Could transparent also work on classes? For example, here is a snippet from FScape (DSP framework):
type Shp[E] = FlowShape[E, E]
class Stage[A, E <: BufElem[A]](implicit tpe: StreamType[A, E])
extends GraphStage[Shp[E]] {
val shape: Shp[E] = ???
def createLogic(): Logic[A, E] = {
val res: Logic[_, _] = if (tpe.isDouble) {
new Logic[Double, BufD](shape.asInstanceOf[Shp[BufD]])(_ - _)
} else if (tpe.isInt) {
new Logic[Int , BufI](shape.asInstanceOf[Shp[BufI]])(_ - _)
} else {
assert (tpe.isLong)
new Logic[Long , BufL](shape.asInstanceOf[Shp[BufL]])(_ - _)
}
res.asInstanceOf[Logic[A, E]]
}
}
class Logic[@specialized A, E <: BufElem[A]](shape: Shp[E])(diff: (A, A) => A)
(implicit tpe: StreamType[A, E])
extends GraphStageLogic(shape) { ... }
(BufElem wraps the primitive arrays in this case).
This is a predictable way I found to have guaranteed specialized instances in Scala 2. Is it conceivable to allow for transparent class? I think that would be great and capture most of the performance optimisations possible today in Scala 2.
Forgive me if Iām being an idiot since I know very little about @specialized, but why canāt inline of a union between the primitives be sufficient?
type Specialized2[A, B] = A | B
object Specialized2 {
type Res[A, B] = (A, B) match {
case (Int, Int) => Int
case (Long, Long) => Long
case (Float, Float) => Float
case (Double, Double) => Double
}
extension [A, B](inline x : Specialized2[A, B])
inline def + (inline y : Specialized2[A, B]) : Res[A, B] = (x, y) match {
case (x : Int, y : Int) => (x + y).asInstanceOf[Res[A, B]]
case (x : Long, y : Long) => (x + y).asInstanceOf[Res[A, B]]
case (x : Float, y : Float) => (x + y).asInstanceOf[Res[A, B]]
case (x : Double, y : Double) => (x + y).asInstanceOf[Res[A, B]]
case _ => ???
}
}
import Specialized2._
inline def foo(a : Specialized2[Int, Double]) = a + a
@main def main : Unit = {
val fooDbl = foo(1.0)
val fooInt = foo(1)
println(fooDbl)
println(fooInt)
}
(I would have rather have used opaque types, but they are currently cannot be used with inline methods)
Inlining can give you some āabstraction without regretā with regard to type classes such as in that example, but itās a totally different thing than what @specialized does.
trait Numeric[A] {
def zero: A
extension (a: A) {
def +(b: A): A
def *(b: A): A
def -(b: A): A
def /(b: A): A
}
}
given Numeric[Double] {
inline def zero = 0.0
extension (a: Double) {
inline def +(b: Double) = a + b
inline def *(b: Double) = a * b
inline def -(b: Double) = a - b
inline def /(b: Double) = a / b
}
}
abstract class MathLib[N: Numeric] {
def computeThing(a: N, b: N): N
}
object MathLib {
inline def apply[N: Numeric] = new MathLib[N] {
def computeThing(a: N, b: N): N = {
(a + b) * (a - b)
}
}
}
Now we have written one generic method that can work on all Numeric types, and generate efficient code without having to incur all the usual indirection.
What this doesnāt do however is eliminate boxing, like @specialized does. When you call MathLib[Double].computeThing(4.2, 2.4) the compiler will generate bytecode equivalent to the following:
final class $anon() extends MathLib[Double](given_Numeric_Double) {
def computeThing(a: Double, b: Double): Double = {
(a + b) * (a - b)
}
override def computeThing(a: Object, b: Object): Object = {
Double.box(computeThing(Double.unbox(a), Double.unbox(b)))
}
}
val a: MathLib[Double] = new $anon()
Double.unbox(a.computeThing(Double.box(4.2), Double.box(2.4)))
So boxing and unboxing still happens (twice). The bytecode optimizerāif enabledāmay still be able to optimize this away though, if Scala 3 can reuse the 2.13 optimizer. @specialized does a lot of fragile magic so the compiler knows at the call sites that heās dealing with a specialized class and call the specialized methods directly.
I had not noticed this, thank you so much. I guess that is the reason for the carefully chosen dot product example with array arguments so that boxing is not an issue.
I am confused and concerned after reading those answers. Does this mean transparent is supposed to replace @specialized? I have used @specialized to prevent boxing as that was causing significant performance issues in my code. Can transparent be used to the same goal?
What is the experience of people writing high performance code with this solution?
Iād like to revive this topic, seeing that function specialization was added and that thereās a PR for tuple specialization.
The lack of support for @specialized is a blocker for a multitude of libraries that were left with no viable alternative (neither inline nor code generation produce a stable, binary compatible api, that hides the specialization from the user)
Rectifying what I wrote, there is no PR, but an open issue.
Recently Spire also ported to scala 3, and lament the loss of performance. I donāt see projects like breeze and algebird (and transitively, Spark) making the upgrade without this either.
Waiting for project Valhalla to deliver this functionality on the JVM means probably waiting 3+ years, donāt think we can wait that much.