Skip to content

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

Merged

Conversation

Wojtechnology
Copy link
Contributor

Addresses #3308. s and raw string interpolators use string concatenation instead of StringContext. Seeing a 4x increase in performance on a very simple initial benchmark:

With optimization:

[info] Benchmark                  Mode  Cnt      Score       Error   Units
[info] Test.testRawInterpolator  thrpt    3  42419.376 ±  8525.679  ops/ms
[info] Test.testSInterpolator    thrpt    3  41732.471 ±  7622.324  ops/ms
[info] Test.testStrConcat        thrpt    3  40346.563 ± 14475.086  ops/ms

Without:

[info] Benchmark                  Mode  Cnt      Score      Error   Units
[info] Test.testRawInterpolator  thrpt    3   9304.561 ±  996.445  ops/ms
[info] Test.testSInterpolator    thrpt    3   8325.524 ±  677.976  ops/ms
[info] Test.testStrConcat        thrpt    3  40857.625 ± 3487.954  ops/ms

Performance for string interpolators is on par with string concatenation.

Copy link
Member

@dottybot dottybot left a 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
Copy link
Contributor

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]) =>
Copy link
Contributor

@allanrenucci allanrenucci Feb 5, 2018

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

Copy link
Contributor Author

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),
Copy link
Member

@smarter smarter Feb 5, 2018

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.

@Wojtechnology
Copy link
Contributor Author

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?

@smarter
Copy link
Member

smarter commented Feb 11, 2018

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])
Copy link
Member

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

@odersky
Copy link
Contributor

odersky commented Mar 19, 2018

What is the status of the PR? Given the performance improvements it would be great to get this in.

@Wojtechnology
Copy link
Contributor Author

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 {
Copy link
Contributor

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.
*/
Copy link
Contributor

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 {
Copy link
Contributor

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),
Copy link
Contributor

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 =>
Copy link
Contributor

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),
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

Choose a reason for hiding this comment

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

Use normal indentation:

@Wojtechnology Wojtechnology force-pushed the ws-optimize-string-interpolation branch from ad3cf75 to 11e6975 Compare March 25, 2018 15:08
// Make sure that StringContext still works with idents
val foo = "Hello"
val bar = "World"
println(StringContext(foo, bar).s(" "))
Copy link
Contributor

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

Copy link
Contributor Author

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.

Copy link
Contributor

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

Copy link
Contributor

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

Copy link
Contributor Author

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
Copy link
Contributor

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
}
}
Copy link
Contributor

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 {
Copy link
Contributor

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)
    }
}

@allanrenucci
Copy link
Contributor

test performance please

@dottybot
Copy link
Member

performance test scheduled: 2 job(s) in queue, 1 running.

@dottybot
Copy link
Member

Performance test finished successfully:

Visit http://dotty-bench.epfl.ch/3961/ to see the changes.

Benchmarks is based on merging with master (f2bb231)

@allanrenucci allanrenucci merged commit e87a5d1 into scala:master May 1, 2018
@OlivierBlanvillain
Copy link
Contributor

@Wojtechnology thanks!

nicolasstucki added a commit that referenced this pull request May 11, 2018
…-quotes

FIx source of performance regression in #3961
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants