Skip to content

Commit

Permalink
Add -YstringConcat to support indy string concat
Browse files Browse the repository at this point in the history
JEP 280, released in JDK 9, proposes a new way to compile string
concatenation using `invokedynamic` and `StringConcatFactory`.
This new approach generates less bytecode, doesn't have to incur
the overhead of `StringBuilder` allocations, and allows users to
pick swap the concatenation technique at runtime.

This commit adds a `-YstringConcat` flag and the `indy` option
leverages `invokedynamic` and `StringConcatFactory`.
  • Loading branch information
harpocrates committed Mar 26, 2021
1 parent 5d025ac commit 00369f8
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 29 deletions.
108 changes: 85 additions & 23 deletions src/compiler/scala/tools/nsc/backend/jvm/BCodeBodyBuilder.scala
Expand Up @@ -991,44 +991,106 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder {
}
}

/* Generate string concatenation code based on the strategy specified at `-YstringConcat` */
def genStringConcat(tree: Tree): BType = {
lineNumber(tree)
liftStringConcat(tree) match {
// Optimization for expressions of the form "" + x. We can avoid the StringBuilder.
// Optimization for expressions of the form "" + x
case List(Literal(Constant("")), arg) =>
genLoad(arg, ObjectRef)
genCallMethod(String_valueOf, InvokeStyle.Static, arg.pos)

case concatenations =>
val approxBuilderSize = concatenations.map {
case Literal(Constant(s: String)) => s.length
case Literal(c @ Constant(value)) if c.isNonUnitAnyVal => String.valueOf(c).length
case _ =>
// could add some guess based on types of primitive args.
// or, we could stringify all the args onto the stack, compute the exact size of
// the StringBuilder.
// or, just let https://openjdk.java.net/jeps/280 (or a re-implementation thereof in our 2.13.x stdlib) do all the hard work at link time
0
}.sum
bc.genStartConcat(tree.pos, approxBuilderSize)
def isEmptyString(t: Tree) = t match {
case Literal(Constant("")) => true
case _ => false
}
for (elem <- concatenations if !isEmptyString(elem)) {
val loadedElem = elem match {

val concatArguments = concatenations.view
.filter {
case Literal(Constant("")) => false // empty strings are no-ops in concatenation
case _ => true
}
.map {
case Apply(boxOp, value :: Nil) if currentRun.runDefinitions.isBox(boxOp.symbol) =>
// Eliminate boxing of primitive values. Boxing is introduced by erasure because
// there's only a single synthetic `+` method "added" to the string class.
value
case other => other
}
.toList

if (currentSettings.YstringConcat.value == "inline" ||
currentSettings.YstringConcat.value == "detect" && currentSettings.target.value == "8") {

// Estimate capacity needed for the string builder
val approxBuilderSize = concatArguments.view.map {
case Literal(Constant(s: String)) => s.length
case Literal(c @ Constant(_)) if c.isNonUnitAnyVal => String.valueOf(c).length
case _ => 0
}.sum
bc.genNewStringBuilder(tree.pos, approxBuilderSize)

for (elem <- concatArguments) {
val elemType = tpeTK(elem)
genLoad(elem, elemType)
bc.genStringBuilderAppend(elemType, elem.pos)
}
bc.genStringBuilderEnd(tree.pos)
} else {

/* `StringConcatFactory#makeConcatWithConstants` accepts max 200 argument slots. If
* the string concatenation is longer (unlikely), we spill into multiple calls
*/
val MaxIndySlots = 200
val TagArg = '\u0001' // indicates a hole (in the recipe string) for an argument
val TagConst = '\u0002' // indicates a hole (in the recipe string) for a constant

val recipe = new StringBuilder()
val argTypes = Seq.newBuilder[asm.Type]
val constVals = Seq.newBuilder[String]
var totalArgSlots = 0
var countConcats = 1 // ie. 1 + how many times we spilled

for (elem <- concatArguments) {
val tpe = tpeTK(elem)
val elemSlots = tpe.size

// Unlikely spill case
if (totalArgSlots + elemSlots >= MaxIndySlots) {
bc.genIndyStringConcat(recipe.toString, argTypes.result(), constVals.result())
countConcats += 1
totalArgSlots = 0
recipe.setLength(0)
argTypes.clear()
constVals.clear()
}

case _ => elem
elem match {
case Literal(Constant(s: String)) =>
if (s.contains(TagArg) || s.contains(TagConst)) {
totalArgSlots += elemSlots
recipe.append(TagConst)
constVals += s
} else {
recipe.append(s)
}

case other =>
totalArgSlots += elemSlots
recipe.append(TagArg)
val tpe = tpeTK(elem)
argTypes += tpe.toASMType
genLoad(elem, tpe)
}
}
bc.genIndyStringConcat(recipe.toString, argTypes.result(), constVals.result())

// If we spilled, generate one final concat
if (countConcats > 1) {
bc.genIndyStringConcat(
TagArg.toString * countConcats,
Seq.fill(countConcats)(StringRef.toASMType),
Seq.empty
)
}
val elemType = tpeTK(loadedElem)
genLoad(loadedElem, elemType)
bc.genConcat(elemType, loadedElem.pos)
}
bc.genEndConcat(tree.pos)
}
StringRef
}
Expand Down
39 changes: 33 additions & 6 deletions src/compiler/scala/tools/nsc/backend/jvm/BCodeIdiomatic.scala
Expand Up @@ -175,10 +175,11 @@ abstract class BCodeIdiomatic {

} // end of method genPrimitiveShift()

/*
/* Creates a new `StringBuilder` instance with the requested capacity
*
* can-multi-thread
*/
final def genStartConcat(pos: Position, size: Int): Unit = {
final def genNewStringBuilder(pos: Position, size: Int): Unit = {
jmethod.visitTypeInsn(Opcodes.NEW, JavaStringBuilderClassName)
jmethod.visitInsn(Opcodes.DUP)
jmethod.visitLdcInsn(Integer.valueOf(size))
Expand All @@ -191,10 +192,11 @@ abstract class BCodeIdiomatic {
)
}

/*
/* Issue a call to `StringBuilder#append` for the right element type
*
* can-multi-thread
*/
def genConcat(elemType: BType, pos: Position): Unit = {
final def genStringBuilderAppend(elemType: BType, pos: Position): Unit = {
val paramType: BType = elemType match {
case ct: ClassBType if ct.isSubtypeOf(StringRef).get => StringRef
case ct: ClassBType if ct.isSubtypeOf(jlStringBufferRef).get => jlStringBufferRef
Expand All @@ -211,13 +213,38 @@ abstract class BCodeIdiomatic {
invokevirtual(JavaStringBuilderClassName, "append", bt.descriptor, pos)
}

/*
/* Extract the built `String` from the `StringBuilder`
*:
* can-multi-thread
*/
final def genEndConcat(pos: Position): Unit = {
final def genStringBuilderEnd(pos: Position): Unit = {
invokevirtual(JavaStringBuilderClassName, "toString", "()Ljava/lang/String;", pos)
}

/* Concatenate top N arguments on the stack with `StringConcatFactory#makeConcatWithConstants`
* (only works for JDK 9+)
*
* can-multi-thread
*/
final def genIndyStringConcat(
recipe: String,
argTypes: Seq[asm.Type],
constants: Seq[String]
): Unit = {
jmethod.visitInvokeDynamicInsn(
"makeConcatWithConstants",
asm.Type.getMethodDescriptor(StringRef.toASMType, argTypes:_*),
new asm.Handle(
asm.Opcodes.H_INVOKESTATIC,
"java/lang/invoke/StringConcatFactory",
"makeConcatWithConstants",
"(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;",
false
),
(recipe +: constants):_*
)
}

/*
* Emits one or more conversion instructions based on the types given as arguments.
*
Expand Down
1 change: 1 addition & 0 deletions src/compiler/scala/tools/nsc/settings/ScalaSettings.scala
Expand Up @@ -278,6 +278,7 @@ trait ScalaSettings extends StandardScalaSettings with Warnings { _: MutableSett

val exposeEmptyPackage = BooleanSetting ("-Yexpose-empty-package", "Internal only: expose the empty package.").internalOnly()
val Ydelambdafy = ChoiceSetting ("-Ydelambdafy", "strategy", "Strategy used for translating lambdas into JVM code.", List("inline", "method"), "method")
val YstringConcat = ChoiceSetting ("-YstringConcat", "strategy", "Strategy used for translating string concatenation into JVM code.", List("inline", "indy", "detect"), "detect")

// Allows a specialised jar to be written. For instance one that provides stable hashing of content, or customisation of the file storage
val YjarFactory = StringSetting ("-YjarFactory", "classname", "factory for jar files", classOf[DefaultJarFactory].getName)
Expand Down

0 comments on commit 00369f8

Please sign in to comment.