Optimize for-in-string loops

For-in-string loop can be generated using specialized 'length' and
'charAt' method calls, and with cached string length.
Note that update of the string variable in loop body doesn't affect
loop execution semantics.

 #KT-21322 Fixed Target versions 1.2.20
This commit is contained in:
Dmitry Petrov
2017-11-27 16:40:05 +03:00
parent e2fa613b70
commit 5b8c0d4c3e
10 changed files with 107 additions and 14 deletions
@@ -22,10 +22,11 @@ import org.jetbrains.kotlin.codegen.range.inExpression.CallBasedInExpressionGene
import org.jetbrains.kotlin.codegen.range.inExpression.InExpressionGenerator
import org.jetbrains.kotlin.psi.KtForExpression
import org.jetbrains.kotlin.psi.KtSimpleNameExpression
import org.jetbrains.org.objectweb.asm.Type
class CharSequenceRangeValue : RangeValue {
class CharSequenceRangeValue(private val canCacheLength: Boolean, private val charSequenceClassType: Type?) : RangeValue {
override fun createForLoopGenerator(codegen: ExpressionCodegen, forExpression: KtForExpression) =
ForInCharSequenceLoopGenerator(codegen, forExpression)
ForInCharSequenceLoopGenerator(codegen, forExpression, canCacheLength, charSequenceClassType)
override fun createInExpressionGenerator(codegen: ExpressionCodegen, operatorReference: KtSimpleNameExpression): InExpressionGenerator =
CallBasedInExpressionGenerator(codegen, operatorReference)
@@ -25,6 +25,7 @@ import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall
import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall
import org.jetbrains.kotlin.resolve.jvm.AsmTypes
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.types.checker.KotlinTypeChecker
import org.jetbrains.org.objectweb.asm.Type
@@ -43,6 +44,7 @@ fun ExpressionCodegen.createRangeValueForExpression(rangeExpression: KtExpressio
val rangeType = bindingContext.getType(rangeExpression)!!
val asmRangeType = asmType(rangeType)
val builtIns = state.module.builtIns
return when {
asmRangeType.sort == Type.ARRAY ->
ArrayRangeValue(!isLocalVarReference(rangeExpression, bindingContext))
@@ -50,8 +52,10 @@ fun ExpressionCodegen.createRangeValueForExpression(rangeExpression: KtExpressio
PrimitiveRangeRangeValue()
isPrimitiveProgression(rangeType) ->
PrimitiveProgressionRangeValue()
isSubtypeOfCharSequence(rangeType, state.module.builtIns) ->
CharSequenceRangeValue()
isSubtypeOfString(rangeType, builtIns) ->
CharSequenceRangeValue(true, AsmTypes.JAVA_STRING_TYPE)
isSubtypeOfCharSequence(rangeType, builtIns) ->
CharSequenceRangeValue(false, null)
else ->
IterableRangeValue()
}
@@ -63,6 +67,9 @@ fun isLocalVarReference(rangeExpression: KtExpression, bindingContext: BindingCo
return resultingDescriptor is LocalVariableDescriptor && resultingDescriptor.isVar
}
private fun isSubtypeOfString(type: KotlinType, builtIns: KotlinBuiltIns) =
KotlinTypeChecker.DEFAULT.isSubtypeOf(type, builtIns.stringType)
private fun isSubtypeOfCharSequence(type: KotlinType, builtIns: KotlinBuiltIns) =
KotlinTypeChecker.DEFAULT.isSubtypeOf(type, builtIns.getBuiltInClassByName(Name.identifier("CharSequence")).defaultType)
@@ -22,12 +22,19 @@ import org.jetbrains.kotlin.psi.KtForExpression
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.org.objectweb.asm.Label
import org.jetbrains.org.objectweb.asm.Type
import org.jetbrains.org.objectweb.asm.commons.InstructionAdapter
class ForInCharSequenceLoopGenerator(codegen: ExpressionCodegen, forExpression: KtForExpression)
: AbstractForLoopGenerator(codegen, forExpression)
{
class ForInCharSequenceLoopGenerator(
codegen: ExpressionCodegen,
forExpression: KtForExpression,
private val canCacheLength: Boolean,
private val charSequenceClassType: Type?
) : AbstractForLoopGenerator(codegen, forExpression) {
private var indexVar: Int = 0
private var charSequenceVar: Int = 0
private var charSequenceLengthVar: Int = 0
private val charSequenceType = charSequenceClassType ?: CHAR_SEQUENCE_TYPE
override fun beforeLoop() {
super.beforeLoop()
@@ -42,9 +49,16 @@ class ForInCharSequenceLoopGenerator(codegen: ExpressionCodegen, forExpression:
// NB even if we already have a loop range stored in local variable, that variable might be modified in the loop body
// (see controlStructures/forInCharSequenceMut.kt).
// We should always store the corresponding CharSequence to a local variable to preserve the Iterator-based behavior.
charSequenceVar = createLoopTempVariable(CHAR_SEQUENCE_TYPE)
charSequenceVar = createLoopTempVariable(charSequenceType)
value.put(asmLoopRangeType, v)
v.store(charSequenceVar, CHAR_SEQUENCE_TYPE)
v.store(charSequenceVar, charSequenceType)
if (canCacheLength) {
charSequenceLengthVar = createLoopTempVariable(Type.INT_TYPE)
v.load(charSequenceVar, charSequenceType)
v.invokeCharSequenceMethod("length", "()I")
v.store(charSequenceLengthVar, Type.INT_TYPE)
}
v.iconst(0)
v.store(indexVar, Type.INT_TYPE)
@@ -54,15 +68,20 @@ class ForInCharSequenceLoopGenerator(codegen: ExpressionCodegen, forExpression:
override fun checkPreCondition(loopExit: Label) {
v.load(indexVar, Type.INT_TYPE)
v.load(charSequenceVar, CHAR_SEQUENCE_TYPE)
v.invokeinterface(CHAR_SEQUENCE_TYPE.internalName, "length", "()I")
if (canCacheLength) {
v.load(charSequenceLengthVar, Type.INT_TYPE)
}
else {
v.load(charSequenceVar, charSequenceType)
v.invokeCharSequenceMethod("length", "()I")
}
v.ificmpge(loopExit)
}
override fun assignToLoopParameter() {
v.load(charSequenceVar, CHAR_SEQUENCE_TYPE)
v.load(charSequenceVar, charSequenceType)
v.load(indexVar, Type.INT_TYPE)
v.invokeinterface(CHAR_SEQUENCE_TYPE.internalName, "charAt", "(I)C")
v.invokeCharSequenceMethod("charAt", "(I)C")
StackValue.onStack(Type.CHAR_TYPE).put(asmElementType, codegen.v)
v.store(loopParameterVar, asmElementType)
}
@@ -71,6 +90,16 @@ class ForInCharSequenceLoopGenerator(codegen: ExpressionCodegen, forExpression:
v.iinc(indexVar, 1)
}
private fun InstructionAdapter.invokeCharSequenceMethod(name: String, desc: String) {
val charSequenceClassType = charSequenceClassType
if (charSequenceClassType != null) {
invokevirtual(charSequenceClassType.internalName, name, desc, false)
}
else {
invokeinterface(CHAR_SEQUENCE_TYPE.internalName, name, desc)
}
}
companion object {
val CHAR_SEQUENCE_TYPE: Type = Type.getObjectType("java/lang/CharSequence")
}
@@ -0,0 +1,11 @@
// WITH_RUNTIME
fun box(): String {
var str = "OK"
var r = ""
for (ch in str) {
r += ch
str = "zzz"
}
return r
}
@@ -0,0 +1,15 @@
// WITH_RUNTIME
fun test() {
var s = ""
for (c in "testString") {
s += c
}
}
// 0 iterator
// 0 hasNext
// 0 nextChar
// 0 INVOKEINTERFACE
// 1 ISTORE 4
// 1 ILOAD 4
@@ -14567,6 +14567,12 @@ public class IrBlackBoxCodegenTestGenerated extends AbstractIrBlackBoxCodegenTes
doTest(fileName);
}
@TestMetadata("forInStringVarUpdatedInLoopBody.kt")
public void testForInStringVarUpdatedInLoopBody() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/ranges/forInStringVarUpdatedInLoopBody.kt");
doTest(fileName);
}
@TestMetadata("forIntRange.kt")
public void testForIntRange() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/ranges/forIntRange.kt");
@@ -14567,6 +14567,12 @@ public class BlackBoxCodegenTestGenerated extends AbstractBlackBoxCodegenTest {
doTest(fileName);
}
@TestMetadata("forInStringVarUpdatedInLoopBody.kt")
public void testForInStringVarUpdatedInLoopBody() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/ranges/forInStringVarUpdatedInLoopBody.kt");
doTest(fileName);
}
@TestMetadata("forIntRange.kt")
public void testForIntRange() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/ranges/forIntRange.kt");
@@ -1301,6 +1301,12 @@ public class BytecodeTextTestGenerated extends AbstractBytecodeTextTest {
doTest(fileName);
}
@TestMetadata("forInStringSpecialized.kt")
public void testForInStringSpecialized() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/bytecodeText/forLoop/forInStringSpecialized.kt");
doTest(fileName);
}
@TestMetadata("forInUntil.kt")
public void testForInUntil() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/bytecodeText/forLoop/forInUntil.kt");
@@ -14567,6 +14567,12 @@ public class LightAnalysisModeTestGenerated extends AbstractLightAnalysisModeTes
doTest(fileName);
}
@TestMetadata("forInStringVarUpdatedInLoopBody.kt")
public void testForInStringVarUpdatedInLoopBody() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/ranges/forInStringVarUpdatedInLoopBody.kt");
doTest(fileName);
}
@TestMetadata("forIntRange.kt")
public void testForIntRange() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/ranges/forIntRange.kt");
@@ -26,7 +26,7 @@ import org.junit.runner.RunWith;
import java.io.File;
import java.util.regex.Pattern;
/** This class is generated by {@link org.jetbrains.kotlin.generators.tests.TestsPackage}. DO NOT MODIFY MANUALLY test*/
/** This class is generated by {@link org.jetbrains.kotlin.generators.tests.TestsPackage}. DO NOT MODIFY MANUALLY */
@SuppressWarnings("all")
@TestMetadata("compiler/testData/codegen/box")
@TestDataPath("$PROJECT_ROOT")
@@ -15839,6 +15839,12 @@ public class JsCodegenBoxTestGenerated extends AbstractJsCodegenBoxTest {
doTest(fileName);
}
@TestMetadata("forInStringVarUpdatedInLoopBody.kt")
public void testForInStringVarUpdatedInLoopBody() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/ranges/forInStringVarUpdatedInLoopBody.kt");
doTest(fileName);
}
@TestMetadata("forIntRange.kt")
public void testForIntRange() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/ranges/forIntRange.kt");