-
Notifications
You must be signed in to change notification settings - Fork 1.1k
String interpolation optimization miniphase #3961
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
String interpolation optimization miniphase #3961
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hello, and thank you for opening this PR! 🎉
All contributors have signed the CLA, thank you! ❤️
Have an awesome day! ☀️
else if (id == nme.s) { | ||
try { | ||
val escapedStrs = strs.mapConserve { str => | ||
val strValue = str.asInstanceOf[Literal].const.stringValue |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think you can assume that str
is always a constant literal. One could write:
val foo = "Hello"
StringContext(foo).s()
|
||
override def transformApply(tree: Apply)(implicit ctx: Context): Tree = { | ||
tree match { | ||
case StringContextIntrinsic(strs: List[Tree], elems: List[Tree]) => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would do something similar to https://github.com/scala/scala/blob/c2a5883891a68180b143eb462c8b0cebc8d3b021/src/library/scala/StringContext.scala#L119. It is very hard to understand what the code below does
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
private object StringContextIntrinsic { | ||
def unapply(tree: Apply)(implicit ctx: Context): Option[(List[Tree], List[Tree])] = { | ||
tree match { | ||
case Apply(Select(Apply(Select(Ident(nme.StringContext), nme.apply), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We cannot rely on names alone to enable this optimization. There might be some other class called StringContext
somewhere. Instead you should check the .symbol
of the tree to make sure it's scala.StringContext
. You can add something like this to https://github.com/lampepfl/dotty/blob/master/compiler/src/dotty/tools/dotc/core/Definitions.scala:
lazy val StringContextType: TypeRef = ctx.requiredClassRef("scala.StringContext")
def StringContextClass(implicit ctx: Context) = StringContextType.symbol.asClass
and then defn.StringContextClass
will be the symbol you want to check against.
Do we usually write unit tests for mini phases? In particular, I'd like to check if the correct tree is generated after passing it to the phase. Is there any example of this kind of test? |
We rarely unit test mini phases (I think there's some in https://github.com/lampepfl/dotty/tree/master/compiler/test/dotty/tools/dotc/transform), we usually do end-to-end testing instead. Here I would suggest adding some tests that check that the same bytecode is generated when using the interpolator or when writing the equivalent code by hand, see https://github.com/lampepfl/dotty/blob/master/compiler/test/dotty/tools/backend/jvm/InlineBytecodeTests.scala for example. |
case Apply(Select(Apply(Select(ident, nme.apply), List(SeqLiteral(strs, _))), fn), | ||
List(SeqLiteral(elems, _))) => | ||
val clsSym = ident.symbol.companionClass | ||
if (clsSym.eq(defn.StringContextClass) && strs.forall(_.isInstanceOf[Literal]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could check if ident.symbol
is equal to defn.StringContextModuleClass
instead after adding StringContextModuleClass
to Definitions.scala
ecab729
to
474ff95
Compare
What is the status of the PR? Given the performance improvements it would be great to get this in. |
Just waiting on code review from my end. |
|
||
private object StringContextIntrinsic { | ||
def unapply(tree: Apply)(implicit ctx: Context): Option[(List[Tree], List[Tree])] = { | ||
tree match { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would first check if tree.symbol
is StringContext.s
or StringContext.raw
for performance
|
||
/** | ||
* Created by wojtekswiderski on 2018-01-24. | ||
*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can remove the author attribution. Also add a comment explaining what this phase does
|
||
override def phaseName: String = "stringInterpolatorOpt" | ||
|
||
private object StringContextIntrinsic { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a comment explaining what this extractor does
private object StringContextIntrinsic { | ||
def unapply(tree: Apply)(implicit ctx: Context): Option[(List[Tree], List[Tree])] = { | ||
tree match { | ||
case Apply(Select(Apply(Select(ident, nme.apply), List(SeqLiteral(strs, _))), fn), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comparing names is not reliable, prefer symbols (for StringContext.apply
)
if (fn == nme.raw_) Some(strs, elems) | ||
else if (fn == nme.s) { | ||
try { | ||
val escapedStrs = strs.mapConserve { str => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mapConserve
is not useful here, use map
private object StringContextIntrinsic { | ||
def unapply(tree: Apply)(implicit ctx: Context): Option[(List[Tree], List[Tree])] = { | ||
tree match { | ||
case Apply(Select(Apply(Select(ident, nme.apply), List(SeqLiteral(strs, _))), fn), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could define an extractor for a sequence of literals to avoid the multiple casts:
/** Match a list of constant literals */
object Literals {
def unapply(tree: SeqLiteral): Option[List[Literal]] = ...
}
And then change the return type of the StringContextIntrinsic
extractor:
object StringContextIntrinsic {
def unapply(tree: Apply)(implicit ctx: Context): Option[(List[Literal], List[Tree])] = ...
}
| def meth1: String = raw"$one plus $two$three\n" | ||
| def meth2: String = "" + one + " plus " + two + three + "\\n" | ||
|} | ||
""".stripMargin |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use normal indentation:
"""
|class Foo {
| val one = 1
| val two = "two"
| val three = 3.0
|
| def meth1: String = raw"$one plus $two$three\n"
| def meth2: String = "" + one + " plus " + two + three + "\\n"
|}
""".stripMargin
| def meth1: String = s"$one plus $two$three\n" | ||
| def meth2: String = "" + one + " plus " + two + three + "\n" | ||
|} | ||
""".stripMargin |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use normal indentation:
ad3cf75
to
11e6975
Compare
// Make sure that StringContext still works with idents | ||
val foo = "Hello" | ||
val bar = "World" | ||
println(StringContext(foo, bar).s(" ")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a test for:
def myStringContext= { println("Side effect!"); StringContext }
println(myStringContext(foo, bar).s(" ")) // this shouldn't be optimised away
I am not sure it is handled by your implementation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added the test and looks like it handles it correctly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, my example was erroneous. What I had is mind was:
println(myStringContext("Foo", "Bar").s(" ")) // this shouldn't be optimised away
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one too, should not be optimised:
println({ println("Side effect n2!"); StringContext }.apply("Titi", "Toto").s(" ")) // this shouldn't be optimised away
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added tests and a fix.
def StringContextRaw(implicit ctx: Context) = StringContextRawR.symbol | ||
def StringContextModule(implicit ctx: Context) = StringContextClass.companionModule | ||
lazy val StringContextModule_applyR = StringContextModule.requiredMethodRef(nme.apply) | ||
def StringContextModule_apply(implicit ctx: Context) = StringContextModule_applyR.symbol |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@smarter Can you comment on this? Should we expose only the ones used in the optimisation (i.e. StringContextRaw
, StringContextS
, StringContextModule_apply
)? Should these values be inlined in the mini phase and be initialised in prepareForUnit
?
def unapply(tree: Ident)(implicit ctx: Context): Option[Ident] = { | ||
if (tree.symbol.eq(defn.StringContextModule)) Some(tree) else None | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inline this extractor into StringContextApply
} | ||
} | ||
|
||
private object StringContextApply { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see that this extractor is just a test and does not return any result (but the tree itself). You can return a Boolean
instead:
private object StringContextApply {
def unapply(tree: Select)(implicit ctx: Context): Boolean =
tree.symbol.eq(defn.StringContextModule_apply) && {
val qualifier = tree.qualifier
qualifier.isInstanceOf[Ident] && qualifier.symbol.eq(efn.StringContextModule)
}
}
test performance please |
performance test scheduled: 2 job(s) in queue, 1 running. |
Performance test finished successfully: Visit http://dotty-bench.epfl.ch/3961/ to see the changes. Benchmarks is based on merging with master (f2bb231) |
@Wojtechnology thanks! |
…-quotes FIx source of performance regression in #3961
Addresses #3308.
s
andraw
string interpolators use string concatenation instead ofStringContext
. Seeing a 4x increase in performance on a very simple initial benchmark:With optimization:
Without:
Performance for string interpolators is on par with string concatenation.