1# String Builder optimizations 2 3## Overview 4 5This set of optimizations targets String Builder usage specifics. 6 7## Rationality 8 9String Builder is used to construct a string out of smaller pieces. In some cases it is optimal to use String Builder to collect intermediate parts, but in other cases we prefer naive string concatenation, due to an overhead introduced by String Builder object. 10 11## Dependence 12 13* BoundsAnalysis 14* AliasAnalysis 15* LoopAnalysis 16* DominatorsTree 17 18## Algorithms 19 20**Remove unnecessary String Builder** 21 22Example: 23```TS 24let input: String = ... 25let sb = new StringBuilder(input) 26let output = sb.toString() 27``` 28Since there are no `StringBuilder::append()`-calls in between `constructor` and `toString()`-call, `input` string is equal to `output` string. So, the example code is equivalent to 29```TS 30let input: String = ... 31let output = input 32``` 33 34**Replace String Builder with string concatenation** 35 36String concatenation expressed as a plus-operator over string operands turned into a String Builder usage by a frontend. 37 38Example: 39```TS 40let a, b: String 41... 42let output = a + b 43``` 44Frontend output equivalent: 45```TS 46let a, b: String 47... 48let sb = new StringBuilder() 49sb.append(a) 50sb.append(b) 51let output = sb.toString() 52``` 53The overhead of String Builder object exceeds the benefits of its usage (comparing to a naive string concatenation) for a small number of operands (two in the example above). So, we replace String Builder in such a cases back to naive string concatenation. 54 55**Optimize concatenation loops** 56 57Consider a string accumulation loop example: 58```TS 59let inputs: string[] = ... // array of strings 60let output = "" 61for (let input in inputs) 62 output += input 63``` 64Like in **Replace String Builder with string concatenation** section, frontend replaces string accumulation `output += input` with a String Builder usage, resulting in a huge performance degradation (comparing to a naive string concatenation), because *at each loop iteration* String Builder object is constructed, used to append two operands, builds resulting string, and discarded. 65 66The equivalent code looks like the following: 67```TS 68let inputs: string[] = ... // array of strings 69let output = "" 70for (let input in inputs) { 71 let sb = new StringBuilder() 72 sb.append(output) 73 sb.append(input) 74 output = sb.toString() 75} 76``` 77To optimize cases like this, we implement the following equivalent transformation: 78```TS 79let inputs: string[] = ... // array of strings 80let sb = new StringBuilder() 81for (let input in inputs) { 82 sb.append(input) 83} 84let output = sb.toString() 85``` 86 87**Merge StringBuilder::append calls chain** 88 89Consider a code sample like the following: 90 91```TS 92// String semantics 93let result = str0 + str1 94 95// StringBuilder semantics 96let sb = new StringBuilder() 97sb.append(str0) 98sb.append(str1) 99let result = sb.toString() 100``` 101 102Here we call `StringBuilder::append` twice. Proposed algorith merges them into a single call to (in this case) `StringBuilder::append2`. Merging up to 4 consecutive calls supported. 103 104Optimized example is equivalent to: 105 106```TS 107// StringBuilder semantics 108let sb = new StringBuilder() 109sb.append2(str0, str1) 110let result = sb.toString() 111``` 112 113**Merge StringBuilder objects chain** 114 115Consider a code sample like the following: 116 117```TS 118// String semantics 119let result0 = str00 + str01 120let result = result0 + str10 + str11 121 122// StringBuilder semantics 123let sb0 = new StringBuilder() 124sb0.append(str00) 125sb0.append(str01) 126 127let sb1 = new StringBuilder() 128sb1.append(sb0.toString()) 129sb1.append(str10) 130sb1.append(str11) 131let result = sb1.toString() 132``` 133 134Here we construct `result0` and `sb0` and use it only once as a first argument of the concatenation which comes next. As we can see, two `StringBuilder` objects created. Instead, we can use only one of them as follows: 135 136```TS 137// StringBuilder semantics 138let sb0 = new StringBuilder() 139sb0.append(str00) 140sb0.append(str01) 141sb0.append(str10) 142sb0.append(str11) 143let result = sb0.toString() 144``` 145 146Proposed algorithm merges consecutive chain of `StringBuilder` objects into a single object (if possible). 147 148## Pseudocode 149 150**Complete algorithm** 151 152```C# 153function SimplifyStringBuilder(graph: Graph) 154 foreach loop in graph 155 OptimizeStringConcatenation(loop) 156 157 OptimizeStringBuilderChain() 158 159 foreach block in graph (in RPO) 160 OptimizeStringBuilderToString(block) 161 OptimizeStringConcatenation(block) 162 OptimizeStringBuilderAppendChain(block) 163``` 164 165Below we describe the algorithm in more details 166 167**Remove unnecessary String Builder** 168 169The algorithm works as follows: first we search for all the StringBuilder instances in a basic block, then we replace all their toString-call usages with instance constructor argument until we meet any other usage in RPO. 170```C# 171function OptimizeStringBuilderToString(block: BasicBlock) 172 foreach ctor being StringBuilder constructor with String argument in block 173 let instance be StringBuilder instance of ctor-call 174 let arg be ctor-call string argument 175 foreach usage of instance (in RPO) 176 if usage is toString-call 177 replace usage with arg 178 else 179 break 180 if instance is not used 181 remove ctor from block 182 remove instance from block 183``` 184**Replace String Builder with string concatenation** 185 186The algorithm works as follows: first we search for all the StringBuilder instances in a basic block, then we check if the use of instance matches concatenation pattern for 2, 3, or 4 arguments. If so, we replace the whole use of StringBuilder object with concatenation intrinsics. 187```C# 188function OptimizeStringConcatenation(block: BasicBlock) 189 foreach ctor being StringBuilder default constructor in block 190 let instance be StringBuilder instance of ctor-call 191 let match = MatchConcatenation(instance) 192 let appendCount = match.appendCount be number of append-calls of instance 193 let append = match.append be an array of append-calls of instance 194 let toStringCall = match.toStringCall be toString-call of instance 195 let concat01 = ConcatIntrinsic(append[0].input(1), append[1].input(1)) 196 remove append[0] from block 197 remove append[1] from block 198 switch appendCount 199 case 2 200 replace toStringCall with concat01 201 break 202 case 3 203 let concat012 = ConcatIntrinsic(concat01, append[2].input(1)) 204 remove append[2] from block 205 replace toStringCall with concat012 206 break 207 case 4 208 let concat23 = ConcatIntrinsic(append[2].input(1), append[3].input(1)) 209 let concat0123 = ConcatIntrinsic(concat01, concat23) 210 remove append[2] from block 211 remove append[3] from block 212 replace toStringCall with concat0123 213 remove toStringCall from block 214 remove ctor from block 215 remove instance from block 216 217function ConcatIntrinsic(arg0, arg1): IntrinsicInst 218 return concatenation intrinsic for arg0 and arg1 219 220type Match 221 toStringCall: CallInst 222 appendCount: Integer 223 append: array of CallInst 224 225function MatchConcatenation(instance: StringBuilder): Match 226 let match: Match 227 foreach usage of instance 228 if usage is toString-call 229 set match.toStringCall = usage 230 elif usage is append-call 231 add usage to match.append array 232 increment match.appendCount 233 return match 234``` 235**Optimize concatenation loops** 236 237The algorithm works as follows: first we recursively process all the inner loops of current loop, then we search for string concatenation patterns within a current loop. For each pattern found we reconnect StringBuilder usage instructions in a correct way (making them point to the only one instance we have chosen), move chosen String Builder object creation and initial string value appending to a loop pre-header, move chosen StringBuilder object toString-call to loop exit block. We cleanup unused instructions at the end. 238```C# 239function OptimizeStringConcatenation(loop: Loop) 240 foreach innerLoop being inner loop of loop 241 OptimizeStringConcatenation(innerLoop) 242 let matches = MatchConcatenationLoop(loop) 243 foreach match in matches) 244 ReconnectInstructions(match) 245 HoistInstructionsToPreHeader(match) 246 HoistInstructionsToExitBlock(match) 247 } 248 Cleanup(loop, matches) 249 250type Match 251 accValue: PhiInst 252 initialValue: Inst 253 // instructions to be hoisted to preheader 254 preheader: type 255 instance: Inst 256 appendAccValue: IntrinsicInst 257 // instructions to be left inside loop 258 loop: type 259 appendIntrinsics: array of IntrinsicInst 260 // instructions to be deleted 261 temp: array of type 262 toStringCall: Inst 263 instance: Inst 264 appendAccValue: IntrinsicInst 265 // instructions to be hoisted to exit block 266 exit: type 267 toStringCall: Inst 268 269function MatchConcatenationLoop(loop: Loop) 270 let matches: array of Match 271 foreach accValue being string accumulator in a loop 272 let match: Match 273 foreach instance being StringBuilder instance used to update accValue (in RPO) 274 if match is empty 275 // Fill preheader and exit parts of a match 276 set match.accValue = accValue 277 set match.initialValue = FindInitialValue(accValue) 278 set match.exit.toStringCall = FindToStringCall(instance) 279 set match.preheader.instance = instance 280 set match.preheader.appendAccValue = FindAppendIntrinsic(instance, accValue) 281 // Init loop part of a match 282 add other append instrinsics to match.loop.appendInstrinsics array 283 else 284 // Fill loop and temporary parts of a match 285 let temp: TemporaryInstructions 286 set temp.instance = instance 287 set temp.toStringCall = FindToStringCall(instance) 288 foreach appendIntrinsic in FindAppendIntrinsics(instance) 289 if appendIntrinsic.input(1) is accValue 290 set temp.appendAccValue = appendIntrinsic 291 else 292 add appendIntricsic to match.loop.appendInstrinsics array 293 add temp to match.temp array 294 add match to matches array 295 return matches 296 297function ReconnectInstructions(match: Match) 298 match.preheader.appendAcc.setInput(0, match.preheader.instance) 299 match.preheader.appendAcc.setInput(1, be match.initialValue) 300 match.exit.toStringCall.setInput(0, match.preheader.instance) 301 foreach user being users of match.accValue outside loop 302 user.replaceInput(match.accValue, match.exit.toStringCall) 303 304function HoistInstructionsToPreHeader(match: Match) 305 foreach inst in match.preheader 306 hoist inst to loop preheader 307 fix broken save states 308 309function HoistInstructionsToExitBlock(match: Match) 310 let exitBlock be to exit block of loop 311 hoist match.exit.toStringCall to exitBlock 312 foreach input being inputs of match.exit.toStringCall inside loop 313 hoist input to exitBlock 314 315function Cleanup(loop: Loop, matches: array of Match) 316 foreach block in loop 317 fix save states in block 318 foreach match in matches 319 foreach temp in match.temp 320 foreach inst in temp 321 remove inst 322 foreach block in loop 323 foreach phi in block 324 if phi is not used 325 remove phi from block 326``` 327 328**Merge StringBuilder::append calls chain** 329 330The algorithm works as follows. First, we find all the `StringBuilder` objects and their `append` calls in a current `block`. Second, we split vector of calls found into a groups of 2, 3, or 4 elements. We replace each group by a corresponding `StringBuilder::appendN` call. 331 332```C# 333function OptimizeStringBuilderAppendChain(block: BasicBlock) 334 foreach [instance, calls] being StringBuilder instance and its vector of append calls in block 335 foreach group being consicutive subvector of 2, 3, or 4 from calls 336 replace group with instance.appendN call 337``` 338 339**Merge StringBuilder objects chain** 340 341The algorithm works as follows. The algorithm traverses blocks of graph in Post Order, and instructions of each block in Reverse Order, this allows us iteratively merge potentially long chains of `StringBuilder` objects into a single object. First, we search a pairs `[instance, inputInstance]` of `StringBuilder` objects which we can merge, merge condition is: last call to `inputInstance.toString()` appended as a first argument to `instance`. Then we remove first call to `StringBuilder::append` from `instance` and last call to `StringBuilder::toString` from `inputInstance`. We retarget remaining calls of `instance` to `inputInstance`. 342 343```C# 344function OptimizeStringBuilderChain() 345 foreach block in graph in PO 346 foreach two objects [instance, inputInstance] being a consicutive pair of Stringbuilders in block in reverse order 347 if CanMerge(instance, inputInstance) 348 let firstAppend be 1st StringBuilder::append call of instance 349 let lastToString be last StringBuilder::toString call of inputInstance 350 remove firstAppend from instance users 351 remove lastToString from inputInstance users 352 foreach call being user of instance 353 call.setInput(0, inputInstance) // retarget append call to inputInstance 354``` 355 356## Examples 357 358**Remove unnecessary String Builder** 359 360ETS function example: 361```TS 362function toString0(str: String): String { 363 return new StringBuilder(str).toString(); 364} 365``` 366 367IR before transformation: 368 369(Save state and null check instructions are skipped for simplicity) 370``` 371Method: std.core.String ETSGLOBAL::toString0(std.core.String) 372 373BB 1 374prop: start 375 0.ref Parameter arg 0 -> (v5) 376succs: [bb 0] 377 378BB 0 preds: [bb 1] 379prop: 380 3.ref LoadAndInitClass 'std.core.StringBuilder' ss -> (v4) 381 4.ref NewObject 15300 v3, ss -> (v5, v10) 382 5.void CallStatic 51211 std.core.StringBuilder::<ctor> v4, v0, ss 383 10.ref CallVirtual 51332 std.core.StringBuilder::toString v4, ss -> (v11) 384 11.ref Return v10 385succs: [bb 2] 386 387BB 2 preds: [bb 0] 388prop: end 389``` 390IR after transformation: 391``` 392Method: std.core.String ETSGLOBAL::toString0(std.core.String) 393 394BB 1 395prop: start 396 0.ref Parameter arg 0 -> (v10) 397succs: [bb 0] 398 399BB 0 preds: [bb 1] 400prop: 401 10.ref Return v0 402succs: [bb 2] 403 404BB 2 preds: [bb 0] 405prop: end 406``` 407 408**Replace String Builder with string concatenation** 409 410ETS function example: 411```TS 412function concat0(a: String, b: String): String { 413 return a + b; 414} 415``` 416IR before transformation: 417``` 418Method: std.core.String ETSGLOBAL::concat0(std.core.String, std.core.String) 419 420BB 1 421prop: start 422 0.ref Parameter arg 0 -> (v10) 423 1.ref Parameter arg 1 -> (v13) 424succs: [bb 0] 425 426BB 0 preds: [bb 1] 427prop: 428 4.ref LoadAndInitClass 'std.core.StringBuilder' ss -> (v5) 429 5.ref NewObject 11355 v4, ss -> (v13, v10, v6) 430 6.void CallStatic 60100 std.core.StringBuilder::<ctor> v5, ss 431 10.ref Intrinsic.StdCoreSbAppendString v5, v0, ss 432 13.ref Intrinsic.StdCoreSbAppendString v5, v1, ss 433 16.ref CallStatic 60290 std.core.StringBuilder::toString v5, ss -> (v17) 434 17.ref Return v16 435succs: [bb 2] 436 437BB 2 preds: [bb 0] 438prop: end 439``` 440IR after transformation: 441``` 442Method: std.core.String ETSGLOBAL::concat0(std.core.String, std.core.String) 443 444BB 1 445prop: start 446 0.ref Parameter arg 0 -> (v18) 447 1.ref Parameter arg 1 -> (v18) 448succs: [bb 0] 449 450BB 0 preds: [bb 1] 451prop: 452 18.ref Intrinsic.StdCoreStringConcat2 v0, v1, ss -> (v17) 453 17.ref Return v18 454succs: [bb 2] 455 456BB 2 preds: [bb 0] 457prop: end 458``` 459 460**Optimize concatenation loops** 461 462ETS function example: 463```TS 464function concat_loop0(a: String, n: int): String { 465 let str: String = ""; 466 for (let i = 0; i < n; ++i) 467 str = str + a; 468 return str; 469} 470``` 471IR before transformation: 472``` 473Method: std.core.String ETSGLOBAL::concat_loop0(std.core.String, i32) 474 475BB 4 476prop: start 477 0.ref Parameter arg 0 -> (v9p) 478 1.i32 Parameter arg 1 -> (v10p) 479 3.i64 Constant 0x0 -> (v7p) 480 30.i64 Constant 0x1 -> (v29) 481succs: [bb 0] 482 483BB 0 preds: [bb 4] 484prop: prehead 485 4.ref LoadString 63726 v5 -> (v8p) 486succs: [bb 3] 487 488BB 3 preds: [bb 0, bb 2] 489prop: head, loop 1, depth 1 490 7p.i32 Phi v3(bb0), v29(bb2) -> (v29, v13) 491 8p.ref Phi v4(bb0), v28(bb2) -> (v31, v12) 492 9p.ref Phi v0(bb0), v9p(bb2) -> (v12, v9p, v25) 493 10p.i32 Phi v1(bb0), v10p(bb2) -> (v10p, v13) 494 13.b Compare GE i32 v7p, v10p -> (v14) 495 14. IfImm NE b v13, 0x0 496succs: [bb 1, bb 2] 497 498BB 2 preds: [bb 3] 499prop: loop 1, depth 1 500 16.ref LoadAndInitClass 'std.core.StringBuilder' ss -> (v17) 501 17.ref NewObject 22178 v16, ss -> (v28, v25, v22) 502 18.void CallStatic 60220 std.core.StringBuilder::<ctor> v17, ss 503 22.ref Intrinsic.StdCoreSbAppendString v17, v8p, ss 504 25.ref Intrinsic.StdCoreSbAppendString v17, v9p, ss 505 28.ref CallStatic 60410 std.core.StringBuilder::toString v17, ss -> (v11p, v8p) 506 29.i32 Add v7p, v30 -> (v7p) 507succs: [bb 3] 508 509BB 1 preds: [bb 3] 510prop: 511 31.ref Return v8p 512succs: [bb 5] 513 514BB 5 preds: [bb 1] 515prop: end 516``` 517IR after transformation: 518``` 519Method: std.core.String ETSGLOBAL::concat_loop0(std.core.String, i32) 520 521BB 4 522prop: start 523 0.ref Parameter arg 0 -> (v25) 524 1.i32 Parameter arg 1 -> (v40, v13) 525 3.i64 Constant 0x0 -> (v40, v7p) 526 30.i64 Constant 0x1 -> (v29) 527succs: [bb 0] 528 529BB 0 preds: [bb 4] 530prop: prehead 531 4.ref LoadString 63726 ss -> (v22) 532 16.ref LoadAndInitClass 'std.core.StringBuilder' ss -> (v17) 533 17.ref NewObject 22178 v16, ss -> (v25, v28, v22, v18) 534 18.void CallStatic 60220 std.core.StringBuilder::<ctor> v17, ss 535 22.ref Intrinsic.StdCoreSbAppendString v17, v4, ss 536 40.b Compare GE i32 v3, v1 -> (v41) 537 41. IfImm NE b v40, 0x0 538succs: [bb 1, bb 2] 539 540BB 2 preds: [bb 0, bb 2] 541prop: head, loop 1, depth 1 542 7p.i32 Phi v3(bb0), v29(bb2) -> (v29) 543 25.ref Intrinsic.StdCoreSbAppendString v17, v0, ss 544 29.i32 Add v7p, v30 -> (v13, v7p) 545 13.b Compare GE i32 v29, v1 -> (v14) 546 14. IfImm NE b v13, 0x0 547succs: [bb 1, bb 2] 548 549BB 1 preds: [bb 2, bb 0] 550prop: 551 28.ref CallStatic 60410 std.core.StringBuilder::toString v17, ss 552 31.ref Return v28 553succs: [bb 5] 554 555BB 5 preds: [bb 1] 556prop: end 557``` 558 559**Merge StringBuilde::append calls chain** 560 561ETS function example: 562```TS 563function append2(str0: string, str1: string): string { 564 let sb = new StringBuilder(); 565 566 sb.append(str0); 567 sb.append(str1); 568 569 return sb.toString(); 570} 571``` 572IR before transformation: 573``` 574Method: std.core.String ETSGLOBAL::append2(std.core.String, std.core.String) 575 576BB 1 577prop: start 578 0.ref Parameter arg 0 -> (v13) 579 1.ref Parameter arg 1 -> (v14) 580succs: [bb 0] 581 582BB 0 preds: [bb 1] 583 4.ref LoadAndInitClass 'std.core.StringBuilder' v3 -> (v5) 584 5.ref NewObject 15705 v4, ss -> (v19, v16, v13, v6) 585 6.void CallStatic 86153 std.core.StringBuilder::<ctor> v5, ss 586 13.ref Intrinsic.StdCoreSbAppendString v5, v0, ss 587 16.ref Intrinsic.StdCoreSbAppendString v5, v1, ss 588 19.ref Intrinsic.StdCoreSbToString v5, ss 589 20.ref Return v19 590succs: [bb 2] 591 592BB 2 preds: [bb 0] 593prop: end 594``` 595IR after transformation: 596``` 597Method: std.core.String ETSGLOBAL::append2(std.core.String, std.core.String) 598 599BB 1 600prop: start 601 0.ref Parameter arg 0 -> (v13) 602 1.ref Parameter arg 1 -> (v14) 603succs: [bb 0] 604 605BB 0 preds: [bb 1] 606 4.ref LoadAndInitClass 'std.core.StringBuilder' ss -> (v5) 607 5.ref NewObject 15705 v4, ss -> (v19, v18, v6) 608 6.void CallStatic 86153 std.core.StringBuilder::<ctor> v5, ss 609 18.ref Intrinsic.StdCoreSbAppendString2 v5, v0, v1, ss 610 19.ref Intrinsic.StdCoreSbToString v5, ss 611 20.ref Return v19 612succs: [bb 2] 613 614BB 2 preds: [bb 0] 615prop: end 616``` 617 618**Merge StringBuilder objects chain** 619 620ETS function example: 621```TS 622function concat2(a: String, b: String): String { 623 let sb0 = new StringBuilder() 624 sb0.append(a) 625 let sb1 = new StringBuilder() 626 sb1.append(sb0.toString()) 627 sb1.append(b) 628 return sb1.toString(); 629} 630``` 631IR before transformation: 632``` 633Method: std.core.String ETSGLOBAL::concat2(std.core.String, std.core.String) 634 635BB 1 636prop: start 637 0.ref Parameter arg 0 -> (v10) 638 1.ref Parameter arg 1 -> (v24) 639succs: [bb 0] 640 641BB 0 preds: [bb 1] 642 4.ref LoadAndInitClass 'std.core.StringBuilder' ss -> (v5) 643 5.ref NewObject 12080 v4, ss -> (v18, v10, v6) 644 6.void CallStatic 86229 std.core.StringBuilder::<ctor> v5, ss 645 10.ref Intrinsic.StdCoreSbAppendString v5, v0, ss 646 12.ref LoadAndInitClass 'std.core.StringBuilder' ss -> (v13) 647 13.ref NewObject 12080 v12, ss -> (v27, v24, v21, v14) 648 14.void CallStatic 86229 std.core.StringBuilder::<ctor> v13, ss 649 18.ref Intrinsic.StdCoreSbToString v5, ss -> (v21) 650 21.ref Intrinsic.StdCoreSbAppendString v13, v18, ss 651 24.ref Intrinsic.StdCoreSbAppendString v13, v1, ss 652 27.ref Intrinsic.StdCoreSbToString v13, ss -> (v28) 653 28.ref Return v27 654succs: [bb 2] 655 656BB 2 preds: [bb 0] 657prop: end 658``` 659IR after transformation: 660``` 661Method: std.core.String ETSGLOBAL::concat2(std.core.String, std.core.String) 662 663BB 1 664prop: start 665 0.ref Parameter arg 0 -> (v10) 666 1.ref Parameter arg 1 -> (v24) 667succs: [bb 0] 668 669BB 0 preds: [bb 1] 670 4.ref LoadAndInitClass 'std.core.StringBuilder' ss -> (v5) 671 5.ref NewObject 12080 v4, ss -> (v27, v24, v18, v10, v6) 672 6.void CallStatic 86229 std.core.StringBuilder::<ctor> v5, ss 673 10.ref Intrinsic.StdCoreSbAppendString v5, v0, ss 674 24.ref Intrinsic.StdCoreSbAppendString v5, v1, ss 675 27.ref Intrinsic.StdCoreSbToString v5, ss -> (v28) 676 28.ref Return v27 677succs: [bb 2] 678 679BB 2 preds: [bb 0] 680prop: end 681``` 682 683## Links 684 685* Implementation 686 * [simplify_string_builder.h](../optimizer/optimizations/simplify_string_builder.h) 687 * [simplify_string_builder.cpp](../optimizer/optimizations/simplify_string_builder.cpp) 688* Tests 689 * [ets_stringbuilder.ets](../../plugins/ets/tests/checked/ets_stringbuilder.ets) 690 * [ets_string_builder_append_merge.ets](../../plugins/ets/tests/checked/ets_string_builder_append_merge.ets) 691 * [ets_string_builder_merge.ets](../../plugins/ets/tests/checked/ets_string_builder_merge.ets) 692 * [ets_string_concat.ets](../../plugins/ets/tests/checked/ets_string_concat.ets) 693 * [ets_string_concat_loop.ets](../../plugins/ets/tests/checked/ets_string_concat_loop.ets) 694