spire icon indicating copy to clipboard operation
spire copied to clipboard

inline algebra typeclass methods

Open erikerlandson opened this issue 1 year ago • 19 comments

fix #1309

erikerlandson avatar Dec 26 '23 15:12 erikerlandson

Note, I cannot inline fromInt or fromDouble because in Fractional and Numeric they inherit multiply from both Field and ConvertibleFromDouble, and so they must be overridden there. inline defs are always final, so these cannot be inlined at DoubleIsField.

Just leaving fromInt and fromDouble as-is seems reasonable, since fixing this little knot would likely require larger interface changes.

erikerlandson avatar Dec 26 '23 17:12 erikerlandson

Thanks for the PR! In the PR description/comments would you mind sharing a small program whose bytecode would be optimized because of these changes?

Yes, I can provide an example that invokes at least one method from each instance (Field, NRoot, Trig, etc). I can exercise all the inlined methods if you want.

erikerlandson avatar Dec 26 '23 19:12 erikerlandson

I can exercise all the inlined methods if you want.

Thanks, just one is sufficient! Essentially a manual test just to demonstrate that this PR actually works, and to demonstrate how to take advantage of it. Since typeclasses are often used in dynamic dispatch situations, I don't think the inlining can work in those cases. Probably it can only work when the typeclass instance itself is inlined.

armanbilge avatar Dec 27 '23 01:12 armanbilge

Probably it can only work when the typeclass instance itself is inlined.

That is what my experiments show - if the calling code itself is not inline, it will default to the standard non-inlined typeclass calls.

Here is a code example that demonstrates in-lining of methods from "all" the Double typeclasses. If you compile this in scala-3, you should see that any references to actual typeclass methods have been elided by inlining:

object inlineTest {
    import spire.implicits.*

    type DoubleTypeClasses = Field[Double]
        & NRoot[Double]
        & Trig[Double]
        & Order[Double]
        & Signed[Double]
        & IsRational[Double]
        & TruncatedDivisionCRing[Double]

    inline def inlinedCalls(x: Double, y: Double)(using alg: DoubleTypeClasses): Double =
        val t1 = alg.plus(x, y)
        val t2 = alg.sqrt(x)
        val t3 = alg.exp(y)
        val t4 = alg.compare(x, y)
        val t5 = alg.abs(x)
        val t6 = alg.tmod(x, y)
        val t7 = alg.floor(y)

        t1 + t2 + t3 + t4 + t5 + t6 + t7

    val x = 1.0
    val y = 2.0

    // with scala-3 inlined typeclasses, the resulting bytecode should
    // show no reference to actual typeclass methods, only raw double
    // operations or calls to 'Math.xxx' methods or other java methods.
    val z = inlinedCalls(x, y)
}

erikerlandson avatar Dec 27 '23 16:12 erikerlandson

@armanbilge as you can see, this change breaks MIMA. Before we proceed, is this going to be a problem?

I can limit the ABI damage to scala-3 only, since this doesn't really apply to scala-2 anyway

erikerlandson avatar Dec 27 '23 16:12 erikerlandson

@armanbilge that seems to work :tada:

The methods are all inlined. It seems to be inserting a call to implicit function, although its result is unused in the subsequent bytecode:

      20: getstatic     #33                 // Field spire/implicits$.MODULE$:Lspire/implicits$;
      23: invokevirtual #37                 // Method spire/implicits$.ImplicitDoubleAlgebra:()Lspire/std/DoubleAlgebra;

Maybe it gets removed in linking? https://docs.scala-lang.org/scala3/reference/contextual/givens.html# "The inlining of given instances will not inline/duplicate the implementation of the given, it will just inline the instantiation of that instance. This is used to help dead code elimination of the given instances that are not used after inlining."

I'm a bit puzzled by it, because in my experiments described on #1309, the implicit object never appeared in the byte code at all, but that was an actual object, not an implicit function.

erikerlandson avatar Dec 27 '23 21:12 erikerlandson

Maybe it gets removed in linking?

If by "linking" you mean the JVM optimizes it away at runtime, then maybe? Scala.js and Scala Native have an explicit linking step, but Scala JVM does not.

The reason that static call may be there is to force the companion object to initialize. Not sure.

armanbilge avatar Dec 28 '23 15:12 armanbilge

@armanbilge The four native types are complete. I think that is where I wanted to get for this. One minor dangling mystery is that mima check failed for log on NRoot[Int] and NRoot[Long], I'm guessing because it's exposed somewhere else besides IntAlgebra. I removed the inline for that and fpow just to see if it passed (it did).

erikerlandson avatar Dec 29 '23 16:12 erikerlandson

It should be okay to leave inline for fpow.

The reason it failed for log is because log is actually not part of the NRoot parent interface. I'm not sure why that method is there. Anyway, when you add inline, it takes away the usual method declaration. This hasn't been a problem in the other cases because the method declaration is always inherited from the parent interface.

armanbilge avatar Dec 29 '23 17:12 armanbilge

@armanbilge the only question I have left is whether there is a way to get rid of the call to implicit function in the byte-code. I doubt I will have a second chance to mess with the signatures without failing MIMA.

erikerlandson avatar Dec 29 '23 20:12 erikerlandson

the only question I have left is whether there is a way to get rid of the call to implicit function

I suspect that might be required for semantics, but I'm not sure. Might be a question for the compiler team (I wouldn't be surprised if there was an existing issue on this topic).

armanbilge avatar Dec 29 '23 20:12 armanbilge

@armanbilge I think I have worked out what is going on. Basically, scala compiles implicit def ... differently than given .... If I declare using given x: X = ..., then scala will not include any call to x() in its bytecode, but if I use the equivalent implicit def x: X = ..., then it will add the x() call.

I don't think it's currently feasible to use given ImplicitDoubleAlgebra ... and friends with spire, because it also requires the user to import xxx.{*, given}, and so doing it breaks the spire code, and presumably any client code currently calling spire.

I could try filing a compiler issue about this, but even if it is feasible to get the compiler to understand when it can elide this call, I doubt it would be a high priority for the compiler devs.

erikerlandson avatar Dec 30 '23 17:12 erikerlandson

@armanbilge given the above, I think this is the best possible solution for spire, so I'm OK to merge if you are.

I suppose it might be feasible to add some new import that uses given for scala-3 users, but that seems like a separate discussion.

erikerlandson avatar Dec 30 '23 17:12 erikerlandson

I could try filing a compiler issue about this, but even if it is feasible to get the compiler to understand when it can elide this call, I doubt it would be a high priority for the compiler devs.

It looks like a bug to me, and specifically a bug with given, not with implicit def. I don't think it's an optimization issue, it's an issue of correctness / semantics, so I believe it would be high priority. Let's see what they say.

  • https://github.com/lampepfl/dotty/issues/19348

armanbilge avatar Dec 30 '23 22:12 armanbilge

@armanbilge the behavior in your reproducer definitely seems like a bug.

erikerlandson avatar Dec 30 '23 23:12 erikerlandson

@armanbilge so we should consider @static given ?

erikerlandson avatar Jan 08 '24 14:01 erikerlandson

I experimented with @static today in Cats Effect and it has two issues:

  1. I do not see a way to apply it compatibly to existing APIs
  2. It's completely broken on Scala.js
  • https://github.com/typelevel/cats-effect/pull/3934

armanbilge avatar Jan 08 '24 16:01 armanbilge

I don't think any given declarations can be backward compatible with existing spire, but if we demonstrated they can compile to faster code with scala-3 I could imagine providing some new scala-3 only givens to import.

erikerlandson avatar Jan 08 '24 17:01 erikerlandson

If we can get @static to work, we can just directly apply to implicit defs. No need to fuss with given for that.

armanbilge avatar Jan 08 '24 17:01 armanbilge