-
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
dottybot
left a comment
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.asClassand 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 awayI 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 awayThere 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.
sandrawstring 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.