Skip to content

Specialize Functions #1807

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 20 commits into from

Conversation

felixmulder
Copy link
Contributor

@felixmulder felixmulder commented Dec 15, 2016

This PR is a bit of a work in progress, but I'd like feedback on the code from @DarkDimius and/or @odersky.

Todo:

  • Specialize unobscured Function1 subclasses
  • Test for presence of specialized apply
  • Test for absence of call to boxing runtime

PS: got stuck at Zurich airport, so haven't run the full test-suite yet. Letting our CI do the heavy lifting instead..

@felixmulder
Copy link
Contributor Author

Perhaps it would be a good idea to move these phases to after erasure, since

class Func1 extends Function1[Int, Int] {
  def apply(i: Int) = i + 1
}

// this cast will be typed as `Int => Int` until erasure
(new Func1: Function1[Int, Int])(1)

and the transformation doesn't catch it currently.

new Splitter), // Expand selections involving union types into conditionals
List(new VCInlineMethods, // Inlines calls to value class methods
new Splitter, // Expand selections involving union types into conditionals
new SpecializeFunction1), // Specialized Function1 by replacing super with specialized super
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do those two transformations have to be in different groups?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was unsure of what impact the interleaved phases would have here. DispatchToSpecializedApply needs to know that the identifier type has a specialized apply before dispatching to it - so the addition to the decls (in SpecializeFunction1) needs to happen before this phase's transformation is applied.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can they be the same phase? If so, what restriction on the transformation API am I missing?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DispatchToSpecializedApply needs to know that the identifier type has a specialized apply before dispatching to it

  1. the specialized apply already exists in scala.Function1, you don't need to wait for them to be added.
  2. tree tranformer runs after denotTransformer, so the new definitions will be already present in types.

@@ -251,7 +251,7 @@ object NameOps {
case nme.clone_ => nme.clone_
}

def specializedFor(classTargs: List[Types.Type], classTargsNames: List[Name], methodTargs: List[Types.Type], methodTarsNames: List[Name])(implicit ctx: Context): name.ThisName = {
def specializedFor(classTargs: List[Types.Type], classTargsNames: List[Name], methodTargs: List[Types.Type] = scala.Nil, methodTarsNames: List[Name] = scala.Nil)(implicit ctx: Context): name.ThisName = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer not to provide default arguments here, as it's easy to misuse this method.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fair enough

tree match {
case Apply(select @ Select(id, nme.apply), arg :: Nil) =>
val params = List(arg.tpe, tree.tpe)
val specializedApply = nme.apply.specializedFor(params, params.map(_.typeSymbol.name))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

todo: this will pollute name table a lot. I would be nice to check if the name already exists before creating it, as if it doesn't, the specialization doesn't exist either.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this check be done in specializedFor?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes) would be nice to have an extra boolean arg that checks if it should force creation.

import Denotations._, SymDenotations._, Scopes._, StdNames._, NameOps._, Names._
import ast.tpd

class SpecializeFunction1 extends MiniPhaseTransform with DenotTransformer {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why only function1?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we agreed to start there :)

But I could make it generic, albeit we only have specializations for JFunction0, JFunction1 and JFunction2


blacklisted = Set(
"scala.compat.java8.JFunction1",
"scala.runtime.AbstractFunction1",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do you need to blacklist those 3? As those functions don't know the actual type parameters of parent class, they shoundn't be specialized.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was me being careful, I initially had some errors because of trying to apply the transformations to these, but the blacklist is never checked against in the current set of tests - as you pointed out they're caught by the earlier checks.


def transformDenot(cref: ClassDenotation, t1: Type, r: Type, func1: Type)(implicit ctx: Context): SingleDenotation = {
val specializedFunction: TypeRef =
ctx.requiredClassRef(functionPkg ++ specializedName(functionName, t1, r))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

requiredClassRef would fail compilation if there's no such class. I don't think it should be used here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getClassIfDefined instead?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes:-)

cref.copySymDenotation(info = newInfo)
}

private def transformTree(tmpl: Template, func1: Tree, t1: Type, r: Type)(implicit ctx: Context) = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd define it inline, as you use it once and the name is less informative then TransformTemplate.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to keep things apart so that it would be easier to get a quick overview of the workings of SpecializeFunction1, but perhaps it was taking it a bit far...

ctx.requiredClassRef(functionPkg ++ specializedName(functionName, t1, r))

def replaceFunction1(in: List[TypeRef]): List[TypeRef] =
in.foldRight(List.empty[TypeRef]) { (tp, acc) =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you are recreating a list, removing all advantages of structural sharing.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use mapConserve

(if (func1 eq t) specializedFunc1 else t) :: trees
}

val body = tmpl.body.foldRight(List.empty[Tree]) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use mapConserve

.map(_.name)
.toList

assert(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be also nice to check that the actual implementation is in the right apply.
Ie, apply$mcII$sp isn't a forwarder to generic apply that unboxes.

@DarkDimius
Copy link
Contributor

Perhaps it would be a good idea to move these phases to after erasure, since

I don't think so. After erasure the information is already lost, and I don't see why you cant translate this block now...

// this cast will be typed as Int => Int until erasure
(new Func1: Function1[Int, Int])(1)

Where is a cast here? There shound be no cast needed here at all.

@felixmulder
Copy link
Contributor Author

felixmulder commented Dec 16, 2016

The cast stops the optimization from applying, currently

@DarkDimius
Copy link
Contributor

but why is cast there in the first place? There shouldn't be any casts needed.

@felixmulder
Copy link
Contributor Author

Without the cast (as should be in normal code) - the optimization applies and all is good. With the cast the optimization doesn't kick in - so the question arises: should it?

@felixmulder
Copy link
Contributor Author

So - you're totally right, there aren't any casts needed. But if there's a cast. The optimization doesn't apply.

@felixmulder felixmulder force-pushed the topic/specialize-func1 branch from 27afa90 to 44253d4 Compare December 23, 2016 15:47
@felixmulder felixmulder force-pushed the topic/specialize-func1 branch 3 times, most recently from 70cd9ba to 43fed4c Compare January 2, 2017 13:28
@felixmulder felixmulder changed the title [WIP] Specialize Function1 Specialize Function1 Jan 24, 2017
@felixmulder felixmulder force-pushed the topic/specialize-func1 branch 2 times, most recently from 95ad5ab to 591bc8d Compare February 15, 2017 16:14
@felixmulder
Copy link
Contributor Author

@DarkDimius - changes discussed today incorporated into the PR.

@felixmulder felixmulder force-pushed the topic/specialize-func1 branch 2 times, most recently from 7db2613 to 7a40ed1 Compare February 16, 2017 11:35
@felixmulder felixmulder changed the title Specialize Function1 Specialize Functions Feb 16, 2017
private[this] var retTypes: Set[Symbol] = _

override def prepareForUnit(tree: Tree)(implicit ctx: Context) = {
retTypes = Set(defn.UnitClass,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not take primitive value classes from ctx.definitons?
Same with argTypes.

.getOrElse(assert(false, "Could not find constructor for object `Test`"))
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be nice to have more run-tests that check if classes&lambdas inherit the right specialized functions using reflection.


// Transformations -----------------------------------------------------------

/** Transforms all classes extending `Function1[-T1, +R]` so that
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Outdated comment?


// Extractors ----------------------------------------------------------------
private object ShouldTransformDenot {
def unapply(cref: ClassDenotation)(implicit ctx: Context): Option[Seq[SpecializationTarget]] =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's very simple implementation that doesn't warrant pattern-matching that allocates options.
Additionally, it's used only once. I'd inline it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to keep it as a pattern, maybe consider using unapplySeq

functionPkg ++ specializedName(functionName ++ arity, args, ret)
}

val specializedApply: Symbol = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if specializedParent doesn't exist?

case cref @ ShouldTransformDenot(targets) => {
val specializedSymbols: Map[Symbol, (Symbol, Symbol)] = (for (SpecializationTarget(target, args, ret, original) <- targets) yield {
val arity = args.length
val specializedParent = ctx.getClassIfDefined {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shoulnd't this class always exist?
Wasn't it already extracted in targets?

def replace(in: List[TypeRef]): List[TypeRef] =
in.map { tref =>
val sym = tref.symbol
specializedSymbols.get(sym).map { case (specializedParent, _) =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I this this is a very inefficient implementation.

Instead of building a Seq[(Symbol, (Symbol, Symbol)] and then converting it and operating it,
you can use the fact that both specializedSymbols and parents maintain the same order and traverse them together.

This will allow for an implementation that doesn't allocate any tuples, doesn't convert collections and traverses collections only once.


val ClassInfo(prefix, cls, parents, decls, info) = cref.classInfo
val newParents = replace(parents)
val newInfo = ClassInfo(prefix, cls, newParents, specializeApplys(decls), info)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you don't need power of entire DenotTransformer. InfoTransformer will do.

*/
override def transformTemplate(tree: Template)(implicit ctx: Context, info: TransformerInfo) =
tree match {
case tmpl @ ShouldTransformTree(targets) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here.

override def transformTemplate(tree: Template)(implicit ctx: Context, info: TransformerInfo) =
tree match {
case tmpl @ ShouldTransformTree(targets) => {
val symbolMap = (for ((tree, SpecializationTarget(target, args, ret, orig)) <- targets) yield {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here, but even worse -> you flatten maps.

@felixmulder felixmulder force-pushed the topic/specialize-func1 branch from 9c05e0b to 615070e Compare February 17, 2017 12:57

def transformInfo(tp: Type, sym: Symbol)(implicit ctx: Context) = tp match {
case tp: ClassInfo if !sym.is(Flags.Package) && (tp.decls ne EmptyScope) => {
val newDecls = tp.decls.cloneScope
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why clone scope when no changes are performed?

case Apply(select @ Select(id, nme.apply), arg :: Nil) => {
val params = List(arg.tpe, tree.tpe)
val specializedApply = specializedName(nme.apply, params)
val hasOverridenSpecializedApply = id.tpe.decls.iterator.exists {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why iterate through all by hand instead of performing member lookup?
Why do you need to check if sym is override?

sym => sym.is(Flags.Override) && (sym.name eq specializedApply)
}

if (hasOverridenSpecializedApply) tpd.Apply(tpd.Select(id, specializedApply), arg :: Nil)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's wrong about dispatching to an abstract one?

@felixmulder felixmulder force-pushed the topic/specialize-func1 branch 2 times, most recently from 9c6871d to 3df75d5 Compare February 21, 2017 17:16
)
func2 = defn.FunctionClass(2)
func2Applys = List(
specApply(func2, defn.UnitType, List(defn.IntType, defn.IntType)),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use loops 😭

@felixmulder felixmulder force-pushed the topic/specialize-func1 branch from b4f3c67 to e17672b Compare May 8, 2017 12:36
@smarter smarter modified the milestones: 0.3 Tech Preview, 0.2 Tech Preview Jul 11, 2017
@Duhemm Duhemm mentioned this pull request Oct 11, 2017
@Duhemm
Copy link
Contributor

Duhemm commented Oct 11, 2017

Superseded by #3306

@Duhemm Duhemm closed this Oct 11, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants