├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── docs ├── css │ └── main.css ├── index.html ├── jmh-result.json ├── jmh.log └── js │ └── charts.js ├── jmh-run.sh ├── pom.xml ├── scala-string-format-core ├── pom.xml └── src │ ├── main │ └── scala │ │ └── com │ │ └── komanov │ │ └── stringformat │ │ ├── InputArg.java │ │ ├── JavaFormats.java │ │ └── ScalaFormats.scala │ └── test │ └── scala │ └── com │ └── komanov │ └── stringformat │ ├── FormatsTest.scala │ └── InputArgApp.scala ├── scala-string-format-test ├── pom.xml └── src │ └── main │ └── scala │ └── com │ └── komanov │ └── stringformat │ └── jmh │ ├── Benchmarks.scala │ ├── NewStringBenchmark.scala │ ├── SimpleBenchmarks.scala │ └── StringBuilderBenchmark.scala └── scala-string-format ├── pom.xml └── src ├── main └── scala │ └── com │ └── komanov │ └── stringformat │ ├── FastStringFactory.java │ ├── OptimizedConcatenation1.scala │ ├── OptimizedConcatenation2.scala │ └── macros │ └── MacroConcat.scala └── test └── scala └── com └── komanov └── stringformat ├── OptimizedConcatenation1Test.scala ├── OptimizedConcatenation2Test.scala └── macros └── MacrosConcatTest.scala /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | target 3 | *.iml 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - oraclejdk8 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Dmitry Komanov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Optimizations described in the blog post are adopted in [scala upstream](https://github.com/scala/scala/pull/6093). This repo is just for history purposed. 2 | 3 | A source code for the article "Scala: String Interpolation Performance" at [medium]([https://medium.com/@dkomanov/scala-serialization-419d175c888a](https://medium.com/@dkomanov/scala-string-interpolation-performance-21dc85e83afd)) is moved to [another repository]([https://github.com/dkomanov/stuff/tree/master/src/com/komanov/serialization](https://github.com/dkomanov/stuff/tree/master/src/com/komanov/stringformat)). [Charts](https://komanov.com/charts/scala-string-format/). 4 | -------------------------------------------------------------------------------- /docs/css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 70px; 3 | } 4 | 5 | .ssf-chart { 6 | height: 600px; 7 | } 8 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Scala: String Interpolation Performance by Dmitry Komanov 9 | 10 | 11 | 12 | 13 | 14 | 15 | 31 | 32 |
33 | 34 |

Scala: String Interpolation Performance

35 | 36 |

Introduction

37 | 38 |

39 | Here are present actual charts for performance comparison of string formatting in Java/Scala for the corresponding 40 | post «Scala: String Interpolation Performance». 41 |

42 | 43 |

44 | The legend for tests. «String length» is a length of a result string (after formatting). 45 |

46 | 47 |

48 | Tests performed via JMH, 2 forks, 3 warmup 49 | runs and 7 iteration (3 seconds each). Ubuntu 16.04, linux-kernel 4.4.0-51-generic, JDK 1.8.0_91, scala library 2.12. 50 | The configuration of a hardware is Intel® Core™ i7–5600U CPU @ 2.60GHz × 4 (2 core + 2 HT) with 16 GB RAM. 51 |

52 | 53 |

Chart

54 | 55 | 65 | 66 | 67 | avg - average, p0 - percentile 0 (min), p50 - percentile 50 (median), p95 - percentile 95, p100 - percentile 100 68 | (max) 69 | 70 | 71 |
72 | 73 |

74 | 75 |

76 | 77 |
78 | 79 |

80 | Full JMH log is here. 81 |

82 | 83 |

84 |   85 |

86 | 87 | 90 | 91 |
92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /docs/js/charts.js: -------------------------------------------------------------------------------- 1 | function loadScalaSerializationChartsPage(rawList) { 2 | // ordered according to view 3 | var DataSizeToView = [ 4 | { 5 | name: "tiny (7)", 6 | value: "Tiny" 7 | }, 8 | { 9 | name: "very short (17)", 10 | value: "VeryShort" 11 | }, 12 | { 13 | name: "short (29)", 14 | value: "Short" 15 | }, 16 | { 17 | name: "medium (75)", 18 | value: "Medium" 19 | }, 20 | { 21 | name: "long (212)", 22 | value: "Long" 23 | }, 24 | { 25 | name: "very long (1004)", 26 | value: "VeryLong" 27 | }, 28 | { 29 | name: "very long size miss (1006)", 30 | value: "VeryLongSizeMiss" 31 | } 32 | ]; 33 | var Methods = [ 34 | { 35 | name: 'javaConcat', 36 | color: '#f4cccc' 37 | }, 38 | { 39 | name: 'stringFormat', 40 | color: '#f49999' 41 | }, 42 | { 43 | name: 'messageFormat', 44 | color: '#ff0000' 45 | }, 46 | { 47 | name: 'scalaConcat', 48 | color: '#cc0000' 49 | }, 50 | { 51 | name: 'concatOptimized1', 52 | color: '#e69138' 53 | }, 54 | { 55 | name: 'concatOptimized2', 56 | color: '#ff9900' 57 | }, 58 | { 59 | name: 'concatOptimizedMacros', 60 | color: '#999900' 61 | }, 62 | { 63 | name: 'slf4j', 64 | color: '#6aa84f' 65 | }, 66 | { 67 | name: 'sInterpolator', 68 | color: '#0000ff' 69 | }, 70 | { 71 | name: 'fInterpolator', 72 | color: '#cccccc' 73 | }, 74 | { 75 | name: 'rawInterpolator', 76 | color: '#999999' 77 | }, 78 | { 79 | name: 'sfiInterpolator', 80 | color: '#000000' 81 | } 82 | ]; 83 | 84 | var data = convertData(rawList); 85 | 86 | $('.ssf-value-btn').click(function () { 87 | var property = $(this).attr('data-property'); 88 | 89 | buildChartAndRawData({ 90 | elem: '.big-chart', 91 | title: 'String formatting', 92 | subtitle: 'times, nanos', 93 | property: property 94 | }); 95 | 96 | $('.ssf-value-btn').removeClass('active'); 97 | $(this).addClass('active'); 98 | }); 99 | 100 | $('.ssf-value-btn[data-property=avg]').click(); 101 | 102 | function buildChartAndRawData(opts) { 103 | buildChart(opts.elem, opts.title, opts.subtitle, getDataForChart(opts.property)); 104 | buildRawData(opts.elem + '-raw-data', opts.property) 105 | } 106 | 107 | function buildChart(elem, title, subtitle, dataArray) { 108 | var chartData = google.visualization.arrayToDataTable(dataArray); 109 | 110 | var series = {}; 111 | $.each(Methods, function (index, method) { 112 | series[index] = { 113 | color: method.color 114 | } 115 | }); 116 | 117 | var options = { 118 | chart: { 119 | title: title, 120 | subtitle: subtitle 121 | }, 122 | series: series 123 | }; 124 | 125 | var chart = new google.charts.Bar($(elem).get(0)); 126 | 127 | chart.draw(chartData, google.charts.Bar.convertOptions(options)); 128 | } 129 | 130 | function buildRawData(elem, property) { 131 | var table = $('') 132 | .addClass('table table-striped table-condensed'); 133 | 134 | var head = $(''); 135 | head.append($('').append(head)); 140 | 141 | var tbody = $(''); 142 | $.each(data, function (index, value) { 143 | var tr = $(''); 144 | 145 | function addTd(text) { 146 | tr.append($('
').html('Method \\ String Length')); 136 | $.each(DataSizeToView, function (index, dataSize) { 137 | head.append($('').html(dataSize.name)); 138 | }); 139 | table.append($('
').html(text)); 147 | } 148 | 149 | addTd(value.name); 150 | 151 | $.each(DataSizeToView, function (index, dataSize) { 152 | var time = getTime(value, dataSize.value, property); 153 | addTd(time); 154 | }); 155 | 156 | tbody.append(tr); 157 | }); 158 | table.append(tbody); 159 | 160 | $(elem).empty().append(table); 161 | } 162 | 163 | function getOrUpdate(map, name, defaultValue) { 164 | var v = map[name] || defaultValue || {}; 165 | map[name] = v; 166 | return v; 167 | } 168 | 169 | function convertData(list) { 170 | // { 171 | // name: javaConcat, 172 | // values: { 173 | // 7: {avg, p0...}, 174 | // 101: {avg, p0...} 175 | // } 176 | // } 177 | // 178 | var map = {}; 179 | 180 | $.each(list, function (index, entry) { 181 | // code from Benchmarks! 182 | var type = entry.params 183 | ? entry.params.arg 184 | : ''; 185 | 186 | var name = extractName(entry.benchmark); 187 | var pm = entry.primaryMetric; 188 | 189 | var timesByStringLength = getOrUpdate(map, name, {name: name, values: {}}).values; 190 | timesByStringLength[type] = { 191 | avg: pm.score, 192 | p0: pm.scorePercentiles['0.0'], 193 | p50: pm.scorePercentiles['50.0'], 194 | p95: pm.scorePercentiles['95.0'], 195 | p100: pm.scorePercentiles['100.0'] 196 | }; 197 | }); 198 | 199 | var result = []; 200 | $.each(Methods, function (index, method) { 201 | result.push(map[method.name] || {name: method.name}); 202 | }); 203 | 204 | return result; 205 | } 206 | 207 | function extractName(benchmark) { 208 | //'com.komanov.stringformat.jmh.ManyParamsBenchmark.concat' 209 | 210 | var index = benchmark.lastIndexOf('.'); 211 | if (index == -1) { 212 | throw new Error('Expected a dot in a benchmark: ' + benchmark); 213 | } 214 | return benchmark.substring(index + 1); 215 | } 216 | 217 | function getDataForChart(property) { 218 | var result = []; 219 | 220 | var header = ['string length']; 221 | $.each(data, function (index, value) { 222 | header.push(value.name); 223 | }); 224 | result.push(header); 225 | 226 | $.each(DataSizeToView, function (index, dataSize) { 227 | var line = [dataSize.name]; 228 | $.each(data, function (index, value) { 229 | var time = getTime(value, dataSize.value, property); 230 | line.push(time); 231 | }); 232 | result.push(line); 233 | }); 234 | 235 | return result; 236 | } 237 | 238 | function getTime(value, dataSize, property) { 239 | return Math.floor(((value.values || {})[dataSize] || {})[property] || 0.0); 240 | } 241 | } 242 | 243 | $(document).ready(function () { 244 | loadGoogleCharts(loadJmhResult); 245 | }); 246 | 247 | function loadGoogleCharts(onLoadCallback) { 248 | google.charts.load('current', {'packages': ['bar']}); 249 | google.charts.setOnLoadCallback(onLoadCallback); 250 | } 251 | 252 | function loadJmhResult() { 253 | var url = 'jmh-result.json'; 254 | $.getJSON(url, function (list) { 255 | loadScalaSerializationChartsPage(list); 256 | }); 257 | } 258 | -------------------------------------------------------------------------------- /jmh-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | ec() { 4 | echo $* 1>&2 5 | $* 6 | } 7 | 8 | ec mvn clean install 9 | 10 | ec java -jar scala-string-format-test/target/benchmarks.jar -rf json -rff jmh-result.json > jmh.log 11 | 12 | ec mv jmh-result.json docs/ 13 | ec mv jmh.log docs/ 14 | 15 | echo "Don't forget to push docs!" 16 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | com.komanov 6 | scala-string-format-all 7 | 1.0-SNAPSHOT 8 | pom 9 | 10 | 11 | scala-string-format 12 | scala-string-format-core 13 | scala-string-format-test 14 | 15 | 16 | 17 | UTF-8 18 | 1.16 19 | default 20 | 1.8 21 | benchmarks 22 | 23 | 24 | 25 | 26 | scala-tools.org 27 | Scala-tools Maven2 Repository 28 | http://scala-tools.org/repo-releases 29 | 30 | 31 | 32 | 33 | 34 | scala-tools.org 35 | Scala-tools Maven2 Repository 36 | http://scala-tools.org/repo-releases 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | org.apache.maven.plugins 45 | maven-compiler-plugin 46 | 3.5.1 47 | 48 | ${javac.target} 49 | ${javac.target} 50 | ${javac.target} 51 | -unchecked 52 | -deprecation 53 | -proc:none 54 | 55 | 56 | 57 | org.apache.maven.plugins 58 | maven-surefire-plugin 59 | 2.19.1 60 | 61 | ${project.basedir}/src/test/scala 62 | 63 | 64 | 65 | tests 66 | test 67 | 68 | test 69 | 70 | 71 | 72 | 73 | 74 | net.alchim31.maven 75 | scala-maven-plugin 76 | 3.2.2 77 | 78 | ${project.basedir}/src/test/scala 79 | 80 | -deprecation 81 | -feature 82 | -Xmax-classfile-name 83 | 240 84 | 85 | -deprecation 86 | ${javac.target} 87 | ${javac.target} 88 | 89 | 90 | 91 | scala-compile-first 92 | process-resources 93 | 94 | add-source 95 | compile 96 | 97 | 98 | 99 | scala-test-compile 100 | process-test-resources 101 | 102 | testCompile 103 | 104 | 105 | 106 | process-sources 107 | 108 | compile 109 | 110 | 111 | 112 | 113 | 114 | org.apache.maven.plugins 115 | maven-release-plugin 116 | 117 | 118 | org.apache.maven.plugins 119 | maven-resources-plugin 120 | 2.6 121 | 122 | ${project.build.sourceEncoding} 123 | true 124 | 125 | 126 | 127 | org.apache.maven.plugins 128 | maven-jar-plugin 129 | 2.4 130 | 131 | 132 | 133 | true 134 | true 135 | 136 | 137 | ${project.version} 138 | ${project.version} 139 | ${buildNumber} 140 | ${project.artifactId} 141 | ${maven.build.timestamp} 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | org.codehaus.mojo 152 | buildnumber-maven-plugin 153 | 1.3 154 | 155 | 156 | org.apache.maven.plugins 157 | maven-dependency-plugin 158 | 2.8 159 | 160 | 161 | org.apache.maven.plugins 162 | maven-compiler-plugin 163 | 164 | 165 | net.alchim31.maven 166 | scala-maven-plugin 167 | 168 | 169 | org.apache.maven.plugins 170 | maven-jar-plugin 171 | 172 | 173 | org.apache.maven.plugins 174 | maven-surefire-plugin 175 | 176 | 177 | org.apache.maven.plugins 178 | maven-failsafe-plugin 179 | 180 | 181 | org.apache.maven.plugins 182 | maven-resources-plugin 183 | 184 | 185 | 186 | 187 | 188 | -------------------------------------------------------------------------------- /scala-string-format-core/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.komanov 8 | scala-string-format-core 9 | 1.0-SNAPSHOT 10 | 11 | 12 | scala-string-format-all 13 | com.komanov 14 | 1.0-SNAPSHOT 15 | 16 | 17 | 18 | 19 | org.scala-lang 20 | scala-library 21 | 2.12.0 22 | 23 | 24 | org.slf4j 25 | slf4j-api 26 | 1.7.21 27 | 28 | 29 | com.komanov 30 | scala-string-format 31 | 1.0-SNAPSHOT 32 | 33 | 34 | 35 | 36 | 37 | org.specs2 38 | specs2-core_2.12 39 | 3.8.6 40 | test 41 | 42 | 43 | org.specs2 44 | specs2-matcher-extra_2.12 45 | 3.8.6 46 | test 47 | 48 | 49 | org.specs2 50 | specs2-mock_2.12 51 | 3.8.6 52 | test 53 | 54 | 55 | org.specs2 56 | specs2-junit_2.12 57 | 3.8.6 58 | test 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /scala-string-format-core/src/main/scala/com/komanov/stringformat/InputArg.java: -------------------------------------------------------------------------------- 1 | package com.komanov.stringformat; 2 | 3 | public enum InputArg { 4 | Tiny(1, ""), 5 | VeryShort(1, "12345"), 6 | Short(100, "string__10"), 7 | Medium(10000, "string________________________32"), 8 | Long(100000, "string___________________________________________________________________________________________100"), 9 | VeryLong(10000000, "string______________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________495"), 10 | VeryLongSizeMiss(Integer.MAX_VALUE, "string______________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________495"), 11 | /*IDEA*/; 12 | 13 | public final int value1; 14 | public final String value2; 15 | 16 | InputArg(int value1, String value2) { 17 | this.value1 = value1; 18 | this.value2 = value2; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /scala-string-format-core/src/main/scala/com/komanov/stringformat/JavaFormats.java: -------------------------------------------------------------------------------- 1 | package com.komanov.stringformat; 2 | 3 | import org.slf4j.helpers.MessageFormatter; 4 | 5 | import java.text.MessageFormat; 6 | import java.util.Locale; 7 | 8 | public class JavaFormats { 9 | public static String concat(int value1, String value2, Object nullObject) { 10 | return value1 + "a" + value2 + "b" + value2 + nullObject; 11 | } 12 | 13 | public static String stringFormat(int value1, String value2, Object nullObject) { 14 | return String.format(Locale.ENGLISH, "%da%sb%s%s", value1, value2, value2, nullObject); 15 | } 16 | 17 | public static String messageFormat(int value1, String value2, Object nullObject) { 18 | return MessageFormat.format("{0,number,#}a{1}b{2}{3}", value1, value2, value2, nullObject); 19 | } 20 | 21 | public static String slf4j(int value1, String value2, Object nullObject) { 22 | return MessageFormatter 23 | .arrayFormat("{}a{}b{}{}", new Object[]{value1, value2, value2, nullObject}) 24 | .getMessage(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /scala-string-format-core/src/main/scala/com/komanov/stringformat/ScalaFormats.scala: -------------------------------------------------------------------------------- 1 | package com.komanov.stringformat 2 | 3 | import com.komanov.stringformat.macros.MacroConcat._ 4 | 5 | object ScalaFormats { 6 | 7 | def concat(value1: Int, value2: String, nullObject: Object): String = { 8 | value1 + "a" + value2 + "b" + value2 + nullObject 9 | } 10 | 11 | def optimizedConcat1(value1: Int, value2: String, nullObject: Object): String = { 12 | OptimizedConcatenation1.concat(Int.box(value1), "a", value2, "b", value2, nullObject) 13 | } 14 | 15 | def optimizedConcat2(value1: Int, value2: String, nullObject: Object): String = { 16 | OptimizedConcatenation2.concat(Int.box(value1), "a", value2, "b", value2, nullObject) 17 | } 18 | 19 | def optimizedConcatMacros(value1: Int, value2: String, nullObject: Object): String = { 20 | so"${value1}a${value2}b$value2$nullObject" 21 | } 22 | 23 | def sInterpolator(value1: Int, value2: String, nullObject: Object): String = { 24 | s"${value1}a${value2}b$value2$nullObject" 25 | } 26 | 27 | def fInterpolator(value1: Int, value2: String, nullObject: Object): String = { 28 | f"${value1}a${value2}b$value2$nullObject" 29 | } 30 | 31 | def rawInterpolator(value1: Int, value2: String, nullObject: Object): String = { 32 | raw"${value1}a${value2}b$value2$nullObject" 33 | } 34 | 35 | def sfiInterpolator(value1: Int, value2: String, nullObject: Object): String = { 36 | sfi"${value1}a${value2}b$value2$nullObject" 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /scala-string-format-core/src/test/scala/com/komanov/stringformat/FormatsTest.scala: -------------------------------------------------------------------------------- 1 | package com.komanov.stringformat 2 | 3 | import org.specs2.mutable.SpecificationWithJUnit 4 | import org.specs2.specification.core.Fragment 5 | 6 | class FormatsTest extends SpecificationWithJUnit { 7 | 8 | val formats: List[(String, (Int, String, Object) => String)] = List( 9 | "javaConcat" -> JavaFormats.concat, 10 | "stringFormat" -> JavaFormats.stringFormat, 11 | "messageFormat" -> JavaFormats.messageFormat, 12 | "slf4j" -> JavaFormats.slf4j, 13 | "scalaConcat" -> ScalaFormats.concat, 14 | "optimizedConcat1" -> ScalaFormats.optimizedConcat1, 15 | "optimizedConcat2" -> ScalaFormats.optimizedConcat2, 16 | "optimizedConcatMacros" -> ScalaFormats.optimizedConcatMacros, 17 | "sInterpolator" -> ScalaFormats.sInterpolator, 18 | "fInterpolator" -> ScalaFormats.fInterpolator, 19 | "rawInterpolator" -> ScalaFormats.rawInterpolator, 20 | "sfiInterpolator" -> ScalaFormats.sfiInterpolator 21 | ) 22 | 23 | Fragment.foreach(formats) { case (name, f) => 24 | s"$name" should { 25 | "product the same result as JavaConcat" >> { 26 | f(1, "str", null) must beEqualTo(JavaFormats.concat(1, "str", null)) 27 | f(1, null, "str") must beEqualTo(JavaFormats.concat(1, null, "str")) 28 | } 29 | } 30 | } 31 | 32 | val formatsWithInputArgs = for { 33 | (name, f) <- formats 34 | arg <- InputArg.values 35 | } yield (name, arg, f) 36 | 37 | Fragment.foreach(formatsWithInputArgs) { case (name, arg, f) => 38 | s"$name" should { 39 | s"product the same result as JavaConcat for $arg" >> { 40 | f(arg.value1, arg.value2, null) must beEqualTo(JavaFormats.concat(arg.value1, arg.value2, null)) 41 | } 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /scala-string-format-core/src/test/scala/com/komanov/stringformat/InputArgApp.scala: -------------------------------------------------------------------------------- 1 | package com.komanov.stringformat 2 | 3 | object InputArgApp extends App { 4 | 5 | for (a <- InputArg.values()) { 6 | println(s"$a: ${JavaFormats.concat(a.value1, a.value2, null).length}") 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /scala-string-format-test/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | com.komanov.jmh 6 | scala-string-format-test 7 | 1.0 8 | jar 9 | 10 | 11 | scala-string-format-all 12 | com.komanov 13 | 1.0-SNAPSHOT 14 | 15 | 16 | 17 | 18 | org.openjdk.jmh 19 | jmh-core 20 | ${jmh.version} 21 | 22 | 23 | org.openjdk.jmh 24 | jmh-generator-annprocess 25 | ${jmh.version} 26 | provided 27 | 28 | 29 | com.komanov 30 | scala-string-format-core 31 | 1.0-SNAPSHOT 32 | 33 | 34 | 35 | 36 | 3.0.4 37 | 38 | 39 | 40 | 41 | 42 | org.codehaus.mojo 43 | build-helper-maven-plugin 44 | 1.8 45 | 46 | 47 | add-source 48 | generate-sources 49 | 50 | add-source 51 | 52 | 53 | 54 | ${project.basedir}/src/main/scala 55 | ${project.basedir}/target/generated-sources/jmh 56 | 57 | 58 | 59 | 60 | 61 | 62 | 65 | 66 | 67 | org.codehaus.mojo 68 | exec-maven-plugin 69 | 1.2.1 70 | 71 | 72 | process-sources 73 | 74 | java 75 | 76 | 77 | true 78 | org.openjdk.jmh.generators.bytecode.JmhBytecodeGenerator 79 | 80 | ${project.basedir}/target/classes/ 81 | ${project.basedir}/target/generated-sources/jmh/ 82 | ${project.basedir}/target/classes/ 83 | ${jmh.generator} 84 | 85 | 86 | 87 | 88 | 89 | 90 | org.openjdk.jmh 91 | jmh-generator-bytecode 92 | ${jmh.version} 93 | 94 | 95 | 96 | 97 | 100 | 101 | 102 | org.apache.maven.plugins 103 | maven-shade-plugin 104 | 2.2 105 | 106 | 107 | package 108 | 109 | shade 110 | 111 | 112 | ${uberjar.name} 113 | 114 | 115 | org.openjdk.jmh.Main 116 | 117 | 118 | 119 | 120 | 124 | *:* 125 | 126 | META-INF/*.SF 127 | META-INF/*.DSA 128 | META-INF/*.RSA 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | maven-resources-plugin 139 | 2.6 140 | 141 | 142 | maven-site-plugin 143 | 3.3 144 | 145 | 146 | maven-source-plugin 147 | 2.2.1 148 | 149 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /scala-string-format-test/src/main/scala/com/komanov/stringformat/jmh/Benchmarks.scala: -------------------------------------------------------------------------------- 1 | package com.komanov.stringformat.jmh 2 | 3 | import java.util.concurrent.TimeUnit 4 | 5 | import com.komanov.stringformat.{InputArg, JavaFormats, ScalaFormats} 6 | import org.openjdk.jmh.annotations._ 7 | 8 | @State(Scope.Benchmark) 9 | @BenchmarkMode(Array(Mode.AverageTime)) 10 | @OutputTimeUnit(TimeUnit.NANOSECONDS) 11 | @Fork(value = 2, jvmArgs = Array("-Xmx2G")) 12 | @Measurement(iterations = 7, time = 3, timeUnit = TimeUnit.SECONDS) 13 | @Warmup(iterations = 3, time = 3, timeUnit = TimeUnit.SECONDS) 14 | abstract class BenchmarkBase 15 | 16 | class ManyParamsBenchmark extends BenchmarkBase { 17 | 18 | @Param 19 | var arg: InputArg = InputArg.Tiny 20 | 21 | var nullObject: Object = null 22 | 23 | @Benchmark 24 | def javaConcat(): String = { 25 | JavaFormats.concat(arg.value1, arg.value2, nullObject) 26 | } 27 | 28 | @Benchmark 29 | def scalaConcat(): String = { 30 | ScalaFormats.concat(arg.value1, arg.value2, nullObject) 31 | } 32 | 33 | @Benchmark 34 | def stringFormat(): String = { 35 | JavaFormats.stringFormat(arg.value1, arg.value2, nullObject) 36 | } 37 | 38 | @Benchmark 39 | def messageFormat(): String = { 40 | JavaFormats.messageFormat(arg.value1, arg.value2, nullObject) 41 | } 42 | 43 | @Benchmark 44 | def slf4j(): String = { 45 | JavaFormats.slf4j(arg.value1, arg.value2, nullObject) 46 | } 47 | 48 | @Benchmark 49 | def concatOptimized1(): String = { 50 | ScalaFormats.optimizedConcat1(arg.value1, arg.value2, nullObject) 51 | } 52 | 53 | @Benchmark 54 | def concatOptimized2(): String = { 55 | ScalaFormats.optimizedConcat2(arg.value1, arg.value2, nullObject) 56 | } 57 | 58 | @Benchmark 59 | def concatOptimizedMacros(): String = { 60 | ScalaFormats.optimizedConcatMacros(arg.value1, arg.value2, nullObject) 61 | } 62 | 63 | @Benchmark 64 | def sInterpolator(): String = { 65 | ScalaFormats.sInterpolator(arg.value1, arg.value2, nullObject) 66 | } 67 | 68 | @Benchmark 69 | def fInterpolator(): String = { 70 | ScalaFormats.fInterpolator(arg.value1, arg.value2, nullObject) 71 | } 72 | 73 | @Benchmark 74 | def rawInterpolator(): String = { 75 | ScalaFormats.rawInterpolator(arg.value1, arg.value2, nullObject) 76 | } 77 | 78 | @Benchmark 79 | def sfiInterpolator(): String = { 80 | ScalaFormats.sfiInterpolator(arg.value1, arg.value2, nullObject) 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /scala-string-format-test/src/main/scala/com/komanov/stringformat/jmh/NewStringBenchmark.scala: -------------------------------------------------------------------------------- 1 | package com.komanov.stringformat.jmh 2 | 3 | import com.komanov.stringformat.FastStringFactory 4 | import org.openjdk.jmh.annotations.Benchmark 5 | 6 | object NewStringBenchmarkData { 7 | val chars = new Array[Char](1006) 8 | val sb = new java.lang.StringBuilder(chars.length) 9 | .append(chars) 10 | } 11 | 12 | class NewStringBenchmark extends BenchmarkBase { 13 | 14 | @Benchmark 15 | def baseline: String = { 16 | "" 17 | } 18 | 19 | @Benchmark 20 | def newString: String = { 21 | new String(NewStringBenchmarkData.chars) 22 | } 23 | 24 | @Benchmark 25 | def fastString: String = { 26 | FastStringFactory.fastNewString(NewStringBenchmarkData.chars) 27 | } 28 | 29 | @Benchmark 30 | def sbToString: String = { 31 | NewStringBenchmarkData.sb.toString 32 | } 33 | 34 | @Benchmark 35 | def fastSb: String = { 36 | FastStringFactory.fastNewString(NewStringBenchmarkData.sb) 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /scala-string-format-test/src/main/scala/com/komanov/stringformat/jmh/SimpleBenchmarks.scala: -------------------------------------------------------------------------------- 1 | package com.komanov.stringformat.jmh 2 | 3 | import org.openjdk.jmh.annotations.Benchmark 4 | 5 | class EmptyStringBenchmark extends BenchmarkBase { 6 | 7 | @Benchmark 8 | def baseline: String = { 9 | "" 10 | } 11 | 12 | @Benchmark 13 | def sInterpolator: String = { 14 | s"" 15 | } 16 | 17 | @Benchmark 18 | def sfiInterpolator: String = { 19 | import com.komanov.stringformat.macros.MacroConcat._ 20 | sfi"" 21 | } 22 | } 23 | 24 | class ConstStringBenchmark extends BenchmarkBase { 25 | 26 | @Benchmark 27 | def baseline: String = { 28 | "abc" 29 | } 30 | 31 | @Benchmark 32 | def sInterpolator: String = { 33 | s"abc" 34 | } 35 | 36 | @Benchmark 37 | def sfiInterpolator: String = { 38 | import com.komanov.stringformat.macros.MacroConcat._ 39 | sfi"abc" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /scala-string-format-test/src/main/scala/com/komanov/stringformat/jmh/StringBuilderBenchmark.scala: -------------------------------------------------------------------------------- 1 | package com.komanov.stringformat.jmh 2 | 3 | import org.openjdk.jmh.annotations.Benchmark 4 | 5 | class StringBuilderBenchmark extends BenchmarkBase { 6 | 7 | @Benchmark 8 | def javaStringBuilder: String = { 9 | new java.lang.StringBuilder() 10 | .append("abc") 11 | .append("def") 12 | .toString 13 | } 14 | 15 | @Benchmark 16 | def javaStringBuilder2: String = { 17 | new java.lang.StringBuilder() 18 | .append("string______________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________495") 19 | .append("string______________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________495") 20 | .toString 21 | } 22 | 23 | @Benchmark 24 | def scalaStringBuilder: String = { 25 | new scala.collection.mutable.StringBuilder() 26 | .append("abc") 27 | .append("def") 28 | .toString 29 | } 30 | 31 | @Benchmark 32 | def scalaStringBuilder2: String = { 33 | new scala.collection.mutable.StringBuilder() 34 | .append("string______________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________495") 35 | .append("string______________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________495") 36 | .toString 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /scala-string-format/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.komanov 8 | scala-string-format 9 | 1.0-SNAPSHOT 10 | 11 | 12 | scala-string-format-all 13 | com.komanov 14 | 1.0-SNAPSHOT 15 | 16 | 17 | 18 | 19 | org.scala-lang 20 | scala-library 21 | 2.12.0 22 | 23 | 24 | org.scala-lang 25 | scala-reflect 26 | 2.12.0 27 | 28 | 29 | 30 | 31 | 32 | org.specs2 33 | specs2-core_2.12 34 | 3.8.6 35 | test 36 | 37 | 38 | org.specs2 39 | specs2-matcher-extra_2.12 40 | 3.8.6 41 | test 42 | 43 | 44 | org.specs2 45 | specs2-mock_2.12 46 | 3.8.6 47 | test 48 | 49 | 50 | org.specs2 51 | specs2-junit_2.12 52 | 3.8.6 53 | test 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /scala-string-format/src/main/scala/com/komanov/stringformat/FastStringFactory.java: -------------------------------------------------------------------------------- 1 | package com.komanov.stringformat; 2 | 3 | import java.lang.invoke.MethodHandle; 4 | import java.lang.invoke.MethodHandles; 5 | import java.lang.reflect.Constructor; 6 | import java.lang.reflect.Field; 7 | 8 | public class FastStringFactory { 9 | 10 | private static final MethodHandle stringBuilderValueGetter = getValueHandler(); 11 | private static final MethodHandle newString = getNewStringHandler(); 12 | 13 | public static String fastNewString(StringBuilder sb) throws Throwable { 14 | if (sb.capacity() != sb.length()) { 15 | throw new IllegalArgumentException("Expected filled StringBuilder!"); 16 | } 17 | 18 | return fastNewString(getValue(sb)); 19 | } 20 | 21 | public static char[] getValue(StringBuilder sb) throws Throwable { 22 | return (char[]) stringBuilderValueGetter.invoke(sb); 23 | } 24 | 25 | public static String fastNewString(char[] chars) throws Throwable { 26 | return (String) newString.invokeExact(chars, true); 27 | } 28 | 29 | private static MethodHandle getValueHandler() { 30 | try { 31 | Field field = getValueField(); 32 | return MethodHandles.lookup().unreflectGetter(field); 33 | } 34 | catch (Exception e) { 35 | throw new RuntimeException(e); 36 | } 37 | } 38 | 39 | private static Field getValueField() throws NoSuchFieldException { 40 | Field field = StringBuilder.class.getSuperclass().getDeclaredField("value"); 41 | field.setAccessible(true); 42 | return field; 43 | } 44 | 45 | private static MethodHandle getNewStringHandler() { 46 | try { 47 | Constructor constructor = String.class.getDeclaredConstructor(char[].class, boolean.class); 48 | constructor.setAccessible(true); 49 | return MethodHandles.lookup().unreflectConstructor(constructor); 50 | } 51 | catch (Exception e) { 52 | throw new RuntimeException(e); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /scala-string-format/src/main/scala/com/komanov/stringformat/OptimizedConcatenation1.scala: -------------------------------------------------------------------------------- 1 | package com.komanov.stringformat 2 | 3 | import scala.annotation.varargs 4 | 5 | object OptimizedConcatenation1 { 6 | 7 | type StringBuilder = java.lang.StringBuilder 8 | 9 | def concat(o1: Object, o2: Object): String = { 10 | concatNonNull(orNull(o1), orNull(o2)) 11 | } 12 | 13 | def concat(o1: Object, o2: Object, o3: Object): String = { 14 | concatNonNull(orNull(o1), orNull(o2), orNull(o3)) 15 | } 16 | 17 | def concat(o1: Object, o2: Object, o3: Object, o4: Object): String = { 18 | concatNonNull(orNull(o1), orNull(o2), orNull(o3), orNull(o4)) 19 | } 20 | 21 | def concat(o1: Object, o2: Object, o3: Object, o4: Object, o5: Object): String = { 22 | concatNonNull(orNull(o1), orNull(o2), orNull(o3), orNull(o4), orNull(o5)) 23 | } 24 | 25 | def concat(o1: Object, o2: Object, o3: Object, o4: Object, o5: Object, o6: Object): String = { 26 | concatNonNull(orNull(o1), orNull(o2), orNull(o3), orNull(o4), orNull(o5), orNull(o6)) 27 | } 28 | 29 | @varargs 30 | def concatObjects(objects: Object*): String = { 31 | if (objects.isEmpty) { 32 | "" 33 | } else { 34 | val sb = new StringBuilder(objects.size * 8) 35 | objects.foldLeft(sb)((sb, s) => if (s != null) sb.append(s) else sb).toString 36 | } 37 | } 38 | 39 | def concat(s1: String, s2: String): String = { 40 | concatNonNull(orNull(s1), orNull(s2)) 41 | } 42 | 43 | def concat(s1: String, s2: String, s3: String): String = { 44 | concatNonNull(orNull(s1), orNull(s2), orNull(s3)) 45 | } 46 | 47 | def concat(s1: String, s2: String, s3: String, s4: String): String = { 48 | concatNonNull(orNull(s1), orNull(s2), orNull(s3), orNull(s4)) 49 | } 50 | 51 | def concat(s1: String, s2: String, s3: String, s4: String, s5: String): String = { 52 | concatNonNull(orNull(s1), orNull(s2), orNull(s3), orNull(s4), orNull(s5)) 53 | } 54 | 55 | def concat(s1: String, s2: String, s3: String, s4: String, s5: String, s6: String): String = { 56 | concatNonNull(orNull(s1), orNull(s2), orNull(s3), orNull(s4), orNull(s5), orNull(s6)) 57 | } 58 | 59 | @varargs 60 | def concatStrings(strings: String*): String = { 61 | if (strings.isEmpty) { 62 | "" 63 | } else { 64 | val len = strings.map(s => if (s == null) 0 else s.length).sum 65 | val sb = new StringBuilder(len) 66 | strings.foldLeft(sb)((sb, s) => if (s != null) sb.append(s) else sb).toString 67 | } 68 | } 69 | 70 | private def concatNonNull(s1: String, s2: String): String = { 71 | new StringBuilder(s1.length + s2.length) 72 | .append(s1) 73 | .append(s2) 74 | .toString 75 | } 76 | 77 | private def concatNonNull(s1: String, s2: String, s3: String): String = { 78 | new StringBuilder(s1.length + s2.length + s3.length) 79 | .append(s1) 80 | .append(s2) 81 | .append(s3) 82 | .toString 83 | } 84 | 85 | private def concatNonNull(s1: String, s2: String, s3: String, s4: String): String = { 86 | new StringBuilder(s1.length + s2.length + s3.length + s4.length) 87 | .append(s1) 88 | .append(s2) 89 | .append(s3) 90 | .append(s4) 91 | .toString 92 | } 93 | 94 | private def concatNonNull(s1: String, s2: String, s3: String, s4: String, s5: String): String = { 95 | new StringBuilder(s1.length + s2.length + s3.length + s4.length + s5.length) 96 | .append(s1) 97 | .append(s2) 98 | .append(s3) 99 | .append(s4) 100 | .append(s5) 101 | .toString 102 | } 103 | 104 | private def concatNonNull(s1: String, s2: String, s3: String, s4: String, s5: String, s6: String): String = { 105 | new StringBuilder(s1.length + s2.length + s3.length + s4.length + s5.length + s6.length) 106 | .append(s1) 107 | .append(s2) 108 | .append(s3) 109 | .append(s4) 110 | .append(s5) 111 | .append(s6) 112 | .toString 113 | } 114 | 115 | @inline 116 | private def orNull(o: Object): String = if (o == null) "null" else o.toString 117 | 118 | @inline 119 | private def orNull(s: String): String = if (s == null) "null" else s 120 | 121 | } 122 | -------------------------------------------------------------------------------- /scala-string-format/src/main/scala/com/komanov/stringformat/OptimizedConcatenation2.scala: -------------------------------------------------------------------------------- 1 | package com.komanov.stringformat 2 | 3 | import scala.annotation.varargs 4 | 5 | object OptimizedConcatenation2 { 6 | 7 | type StringBuilder = java.lang.StringBuilder 8 | 9 | def concat(o1: Object, o2: Object): String = { 10 | concatNonNull(orNull(o1), orNull(o2)) 11 | } 12 | 13 | def concat(o1: Object, o2: Object, o3: Object): String = { 14 | concatNonNull(orNull(o1), orNull(o2), orNull(o3)) 15 | } 16 | 17 | def concat(o1: Object, o2: Object, o3: Object, o4: Object): String = { 18 | concatNonNull(orNull(o1), orNull(o2), orNull(o3), orNull(o4)) 19 | } 20 | 21 | def concat(o1: Object, o2: Object, o3: Object, o4: Object, o5: Object): String = { 22 | concatNonNull(orNull(o1), orNull(o2), orNull(o3), orNull(o4), orNull(o5)) 23 | } 24 | 25 | def concat(o1: Object, o2: Object, o3: Object, o4: Object, o5: Object, o6: Object): String = { 26 | concatNonNull(orNull(o1), orNull(o2), orNull(o3), orNull(o4), orNull(o5), orNull(o6)) 27 | } 28 | 29 | @varargs 30 | def concatObjects(objects: Object*): String = { 31 | if (objects.isEmpty) { 32 | "" 33 | } else { 34 | val sb = new StringBuilder(objects.size * 8) 35 | objects.foldLeft(sb)((sb, s) => if (s != null) sb.append(s) else sb).toString 36 | } 37 | } 38 | 39 | def concat(s1: String, s2: String): String = { 40 | concatNonNull(orNull(s1), orNull(s2)) 41 | } 42 | 43 | def concat(s1: String, s2: String, s3: String): String = { 44 | concatNonNull(orNull(s1), orNull(s2), orNull(s3)) 45 | } 46 | 47 | def concat(s1: String, s2: String, s3: String, s4: String): String = { 48 | concatNonNull(orNull(s1), orNull(s2), orNull(s3), orNull(s4)) 49 | } 50 | 51 | def concat(s1: String, s2: String, s3: String, s4: String, s5: String): String = { 52 | concatNonNull(orNull(s1), orNull(s2), orNull(s3), orNull(s4), orNull(s5)) 53 | } 54 | 55 | def concat(s1: String, s2: String, s3: String, s4: String, s5: String, s6: String): String = { 56 | concatNonNull(orNull(s1), orNull(s2), orNull(s3), orNull(s4), orNull(s5), orNull(s6)) 57 | } 58 | 59 | @varargs 60 | def concatStrings(strings: String*): String = { 61 | if (strings.isEmpty) { 62 | "" 63 | } else { 64 | val len = strings.map(s => if (s == null) 0 else s.length).sum 65 | val sb = new StringBuilder(len) 66 | strings.foldLeft(sb)((sb, s) => if (s != null) sb.append(s) else sb).toString 67 | } 68 | } 69 | 70 | private def concatNonNull(s1: String, s2: String): String = { 71 | val sb = new StringBuilder(s1.length + s2.length) 72 | .append(s1) 73 | .append(s2) 74 | FastStringFactory.fastNewString(sb) 75 | } 76 | 77 | private def concatNonNull(s1: String, s2: String, s3: String): String = { 78 | val sb = new StringBuilder(s1.length + s2.length + s3.length) 79 | .append(s1) 80 | .append(s2) 81 | .append(s3) 82 | 83 | FastStringFactory.fastNewString(sb) 84 | } 85 | 86 | private def concatNonNull(s1: String, s2: String, s3: String, s4: String): String = { 87 | val sb = new StringBuilder(s1.length + s2.length + s3.length + s4.length) 88 | .append(s1) 89 | .append(s2) 90 | .append(s3) 91 | .append(s4) 92 | FastStringFactory.fastNewString(sb) 93 | } 94 | 95 | private def concatNonNull(s1: String, s2: String, s3: String, s4: String, s5: String): String = { 96 | val sb = new StringBuilder(s1.length + s2.length + s3.length + s4.length + s5.length) 97 | .append(s1) 98 | .append(s2) 99 | .append(s3) 100 | .append(s4) 101 | .append(s5) 102 | FastStringFactory.fastNewString(sb) 103 | } 104 | 105 | private def concatNonNull(s1: String, s2: String, s3: String, s4: String, s5: String, s6: String): String = { 106 | val sb = new StringBuilder(s1.length + s2.length + s3.length + s4.length + s5.length + s6.length) 107 | .append(s1) 108 | .append(s2) 109 | .append(s3) 110 | .append(s4) 111 | .append(s5) 112 | .append(s6) 113 | FastStringFactory.fastNewString(sb) 114 | } 115 | 116 | @inline 117 | private def orNull(o: Object): String = if (o == null) "null" else o.toString 118 | 119 | @inline 120 | private def orNull(s: String): String = if (s == null) "null" else s 121 | 122 | } 123 | -------------------------------------------------------------------------------- /scala-string-format/src/main/scala/com/komanov/stringformat/macros/MacroConcat.scala: -------------------------------------------------------------------------------- 1 | package com.komanov.stringformat.macros 2 | 3 | import scala.language.experimental.macros 4 | import scala.reflect.macros.whitebox 5 | 6 | object MacroConcat { 7 | 8 | implicit class SuperFastInterpolator(sc: StringContext) { 9 | def so(args: Any*): String = macro soImpl 10 | 11 | def sfi(args: Any*): String = macro sfiImpl 12 | } 13 | 14 | def soImpl(c: whitebox.Context)(args: c.Expr[Any]*): c.Expr[String] = { 15 | import c.universe._ 16 | 17 | val constantParts = extractConstantPartsAndTreatEscapes(c) 18 | 19 | if (args.isEmpty) { 20 | return c.Expr(Literal(Constant(constantParts.mkString("")))) 21 | } 22 | 23 | val initialLength = constantParts.map(_.length).sum 24 | 25 | val (valDeclarations, lenNames, arguments) = args.zipWithIndex.map { 26 | case (e, index) => 27 | val name = TermName("__local" + index) 28 | e.actualType match { 29 | case tt if tt.typeSymbol.isClass && tt.typeSymbol.asClass.isPrimitive => 30 | val expr = q"val $name = $e.toString" 31 | (List(expr), List(name), Ident(name)) 32 | case _ => 33 | val expr = 34 | q""" 35 | val $name = { 36 | val tmp = $e 37 | if (tmp == null) "null" else tmp.toString 38 | }""" 39 | (List(expr), List(name), Ident(name)) 40 | } 41 | }.unzip3 42 | 43 | val allParts = getAllPartsForAppend(c)(constantParts, arguments) 44 | 45 | // code generation 46 | 47 | val plusLenExpr = lengthSum(c)(initialLength, lenNames.flatten.toList) 48 | 49 | val newStringBuilderAndAppendExpr = newStringBuilderWithAppends(c)(q"new java.lang.StringBuilder($plusLenExpr)", allParts.reverse) 50 | 51 | val stats = valDeclarations.flatten.toList :+ 52 | q"$newStringBuilderAndAppendExpr.toString" 53 | 54 | c.Expr( 55 | q"..$stats" 56 | ) 57 | } 58 | 59 | def sfiImpl(c: whitebox.Context)(args: c.Expr[Any]*): c.Expr[String] = { 60 | import c.universe._ 61 | 62 | val constantParts = extractConstantPartsAndTreatEscapes(c) 63 | 64 | if (args.isEmpty) { 65 | return c.Expr(Literal(Constant(constantParts.mkString("")))) 66 | } 67 | 68 | var initialLength = constantParts.map(_.length).sum 69 | 70 | val (valDeclarations, lenNames, arguments: Seq[c.universe.Tree]) = args.zipWithIndex.map { 71 | case (e, index) => 72 | val name = TermName("__local" + index) 73 | e.actualType match { 74 | case tt if tt.typeSymbol.isClass && tt.typeSymbol.asClass.isPrimitive => 75 | // A kind of optimization to not calculate primitive length in advance, let the StringBuilder 76 | // to deal with primitive toString (it's better than i.e. Int.toString static method). 77 | initialLength += 9 78 | (Nil, Nil, e.tree) 79 | case tt if tt <:< typeOf[CharSequence] => 80 | val expr = 81 | q""" 82 | val $name = { 83 | val tmp = $e 84 | if (tmp eq null) "null" else tmp 85 | }""" 86 | (List(expr), List(name), Ident(name)) 87 | case _ => 88 | val expr = 89 | q""" 90 | val $name = { 91 | val tmp = $e 92 | if (tmp == null) "null" else tmp.toString 93 | }""" 94 | (List(expr), List(name), Ident(name)) 95 | } 96 | }.unzip3 97 | 98 | val allParts = getAllPartsForAppend(c)(constantParts, arguments) 99 | 100 | // code generation 101 | 102 | val plusLenExpr = lengthSum(c)(initialLength, lenNames.flatten.toList) 103 | 104 | val stringBuilderWithAppends = newStringBuilderWithAppends(c)(q"new java.lang.StringBuilder(len)", allParts.reverse) 105 | 106 | val stats = valDeclarations.flatten.toList ++ 107 | List(q"val len = $plusLenExpr") :+ 108 | q"$stringBuilderWithAppends.toString" 109 | 110 | c.Expr( 111 | q"..$stats" 112 | ) 113 | } 114 | 115 | private def getAllPartsForAppend(c: whitebox.Context)(rawParts: Seq[String], arguments: Seq[c.universe.Tree]) = { 116 | import c.universe._ 117 | 118 | def nilOrConst(s: String) = { 119 | if (s.isEmpty) Nil else List(Literal(Constant(s))) 120 | } 121 | 122 | rawParts.zipAll(arguments, null, null).flatMap { 123 | case (raw, null) => nilOrConst(raw) 124 | case (raw, arg) if raw != null => nilOrConst(raw) :+ arg 125 | }.toList 126 | } 127 | 128 | private def extractConstantPartsAndTreatEscapes(c: whitebox.Context): List[String] = { 129 | import c.universe._ 130 | 131 | val rawParts = c.prefix.tree match { 132 | case Apply(_, List(Apply(_, list))) => list 133 | } 134 | 135 | rawParts.map { 136 | case Literal(Constant(rawPart: String)) => StringContext.treatEscapes(rawPart) 137 | } 138 | } 139 | 140 | private def newStringBuilderWithAppends(c: whitebox.Context) 141 | (newStringBuilderExpr: c.universe.Tree, 142 | expressions: List[c.universe.Tree]): c.universe.Tree = { 143 | import c.universe._ 144 | 145 | if (expressions.isEmpty) { 146 | newStringBuilderExpr 147 | } else { 148 | Apply(Select(newStringBuilderWithAppends(c)(newStringBuilderExpr, expressions.tail), TermName("append")), List(expressions.head)) 149 | } 150 | } 151 | 152 | private def lengthSum(c: whitebox.Context)(const: Int, names: List[c.universe.TermName]): c.universe.Tree = { 153 | import c.universe._ 154 | 155 | if (names.isEmpty) { 156 | Literal(Constant(const)) 157 | } else { 158 | Apply(Select(Select(Ident(names.head), TermName("length")), TermName("$plus")), List(lengthSum(c)(const, names.tail))) 159 | } 160 | } 161 | 162 | } 163 | -------------------------------------------------------------------------------- /scala-string-format/src/test/scala/com/komanov/stringformat/OptimizedConcatenation1Test.scala: -------------------------------------------------------------------------------- 1 | package com.komanov.stringformat 2 | 3 | import org.specs2.mutable.SpecificationWithJUnit 4 | 5 | class OptimizedConcatenation1Test extends SpecificationWithJUnit { 6 | 7 | "concat" should { 8 | "objects" >> { 9 | OptimizedConcatenation1.concat(o1, o2) must be_===(s1 + s2) 10 | OptimizedConcatenation1.concat(o1, o2, o3) must be_===(s1 + s2 + s3) 11 | OptimizedConcatenation1.concat(o1, o2, o3, o4) must be_===(s1 + s2 + s3 + s4) 12 | OptimizedConcatenation1.concat(o1, o2, o3, o4, o5) must be_===(s1 + s2 + s3 + s4 + s5) 13 | OptimizedConcatenation1.concat(o1, o2, o3, o4, o5, o6) must be_===(s1 + s2 + s3 + s4 + s5 + s6) 14 | OptimizedConcatenation1.concatObjects(o1, o2, o3, o4, o5, o1, o2, o3, o4, o5) must be_===(s1 + s2 + s3 + s4 + s5 + s1 + s2 + s3 + s4 + s5) 15 | } 16 | 17 | "strings" >> { 18 | OptimizedConcatenation1.concat(s1, s2) must be_===(s1 + s2) 19 | OptimizedConcatenation1.concat(s1, s2, s3) must be_===(s1 + s2 + s3) 20 | OptimizedConcatenation1.concat(s1, s2, s3, s4) must be_===(s1 + s2 + s3 + s4) 21 | OptimizedConcatenation1.concat(s1, s2, s3, s4, s5) must be_===(s1 + s2 + s3 + s4 + s5) 22 | OptimizedConcatenation1.concat(s1, s2, s3, s4, s5, s6) must be_===(s1 + s2 + s3 + s4 + s5 + s6) 23 | OptimizedConcatenation1.concatStrings(s1, s2, s3, s4, s5, s1, s2, s3, s4, s5) must be_===(s1 + s2 + s3 + s4 + s5 + s1 + s2 + s3 + s4 + s5) 24 | } 25 | 26 | "replace null with 'null' string" >> { 27 | OptimizedConcatenation1.concat(null, Int.box(1)) must be_===("null1") 28 | OptimizedConcatenation1.concat(null, "1") must be_===("null1") 29 | } 30 | } 31 | 32 | val o1: Object = Int.box(10000) 33 | val o2: Object = Int.box(200000) 34 | val o3: Object = Int.box(3000000) 35 | val o4: Object = Int.box(40000000) 36 | val o5: Object = Int.box(500000000) 37 | val o6: Object = Int.box(60000000) 38 | val o7: Object = Int.box(7000000) 39 | 40 | val s1 = "10000" 41 | val s2 = "200000" 42 | val s3 = "3000000" 43 | val s4 = "40000000" 44 | val s5 = "500000000" 45 | val s6 = "60000000" 46 | val s7 = "7000000" 47 | 48 | } 49 | -------------------------------------------------------------------------------- /scala-string-format/src/test/scala/com/komanov/stringformat/OptimizedConcatenation2Test.scala: -------------------------------------------------------------------------------- 1 | package com.komanov.stringformat 2 | 3 | import org.specs2.mutable.SpecificationWithJUnit 4 | 5 | class OptimizedConcatenation2Test extends SpecificationWithJUnit { 6 | 7 | "concat" should { 8 | "objects" >> { 9 | OptimizedConcatenation2.concat(o1, o2) must be_===(s1 + s2) 10 | OptimizedConcatenation2.concat(o1, o2, o3) must be_===(s1 + s2 + s3) 11 | OptimizedConcatenation2.concat(o1, o2, o3, o4) must be_===(s1 + s2 + s3 + s4) 12 | OptimizedConcatenation2.concat(o1, o2, o3, o4, o5) must be_===(s1 + s2 + s3 + s4 + s5) 13 | OptimizedConcatenation2.concat(o1, o2, o3, o4, o5, o6) must be_===(s1 + s2 + s3 + s4 + s5 + s6) 14 | OptimizedConcatenation2.concatObjects(o1, o2, o3, o4, o5, o1, o2, o3, o4, o5) must be_===(s1 + s2 + s3 + s4 + s5 + s1 + s2 + s3 + s4 + s5) 15 | } 16 | 17 | "strings" >> { 18 | OptimizedConcatenation2.concat(s1, s2) must be_===(s1 + s2) 19 | OptimizedConcatenation2.concat(s1, s2, s3) must be_===(s1 + s2 + s3) 20 | OptimizedConcatenation2.concat(s1, s2, s3, s4) must be_===(s1 + s2 + s3 + s4) 21 | OptimizedConcatenation2.concat(s1, s2, s3, s4, s5) must be_===(s1 + s2 + s3 + s4 + s5) 22 | OptimizedConcatenation2.concat(s1, s2, s3, s4, s5, s6) must be_===(s1 + s2 + s3 + s4 + s5 + s6) 23 | OptimizedConcatenation2.concatStrings(s1, s2, s3, s4, s5, s1, s2, s3, s4, s5) must be_===(s1 + s2 + s3 + s4 + s5 + s1 + s2 + s3 + s4 + s5) 24 | } 25 | 26 | "replace null with 'null' string" >> { 27 | OptimizedConcatenation2.concat(null, Int.box(1)) must be_===("null1") 28 | OptimizedConcatenation2.concat(null, "1") must be_===("null1") 29 | } 30 | } 31 | 32 | val o1: Object = Int.box(10000) 33 | val o2: Object = Int.box(200000) 34 | val o3: Object = Int.box(3000000) 35 | val o4: Object = Int.box(40000000) 36 | val o5: Object = Int.box(500000000) 37 | val o6: Object = Int.box(60000000) 38 | val o7: Object = Int.box(7000000) 39 | 40 | val s1 = "10000" 41 | val s2 = "200000" 42 | val s3 = "3000000" 43 | val s4 = "40000000" 44 | val s5 = "500000000" 45 | val s6 = "60000000" 46 | val s7 = "7000000" 47 | 48 | } 49 | -------------------------------------------------------------------------------- /scala-string-format/src/test/scala/com/komanov/stringformat/macros/MacrosConcatTest.scala: -------------------------------------------------------------------------------- 1 | package com.komanov.stringformat.macros 2 | 3 | import com.komanov.stringformat.macros.MacroConcat._ 4 | import org.specs2.mock.Mockito 5 | import org.specs2.mutable.SpecificationWithJUnit 6 | 7 | class MacrosConcatTest extends SpecificationWithJUnit with Mockito { 8 | 9 | "Super Fast Interpolation" should { 10 | "work as s" >> { 11 | sfi"" must be_===(s"") 12 | sfi"abc" must be_===(s"abc") 13 | sfi"$o1" must be_===(s"$o1") 14 | sfi"${o1}after" must be_===(s"${o1}after") 15 | sfi"before${o1}" must be_===(s"before${o1}") 16 | sfi"before${o1}after" must be_===(s"before${o1}after") 17 | sfi"$o1$o2" must be_===(s"$o1$o2") 18 | sfi"!$o1!$o2!" must be_===(s"!$o1!$o2!") 19 | sfi"!$s1!$s2!$s3!$s4!$s5!$s6!$s7!$o1!$o2!$o3!$o4!$o5!$o6!$o7!" must be_===(s"!$s1!$s2!$s3!$s4!$s5!$s6!$s7!$o1!$o2!$o3!$o4!$o5!$o6!$o7!") 20 | } 21 | 22 | "serialize null as s" >> { 23 | sfi"$nullObject" must be_===(s"$nullObject") 24 | } 25 | 26 | "support expressions" >> { 27 | sfi"${car.name}" must be_===(s"${car.name}") 28 | sfi"${if (true) "1" else "0"}" must be_===(s"${if (true) "1" else "0"}") 29 | } 30 | 31 | "support parametric types" >> { 32 | testSfi(1) must be_===(testS(1)) 33 | testSfi('A') must be_===(testS('A')) 34 | testSfi("A") must be_===(testS("A")) 35 | } 36 | 37 | def testSfi[A](a: A): String = sfi"$a" 38 | 39 | def testS[A](a: A): String = s"$a" 40 | 41 | "don't call expression multiple times" >> { 42 | val m = mock[Something] 43 | 44 | m.id returns 1 45 | m.name returns "name" 46 | m.obj returns null 47 | 48 | sfi"${m.id} - ${m.name} - ${m.obj} - ${m.id}" must be_===("1 - name - null - 1") 49 | 50 | got { 51 | two(m).id 52 | one(m).name 53 | one(m).obj 54 | noMoreCallsTo(m) 55 | } 56 | } 57 | } 58 | 59 | "Optimized Concatenation Interpolation" should { 60 | "work as s" >> { 61 | so"" must be_===(s"") 62 | so"abc" must be_===(s"abc") 63 | so"$o1" must be_===(s"$o1") 64 | so"${o1}after" must be_===(s"${o1}after") 65 | so"before${o1}" must be_===(s"before${o1}") 66 | so"before${o1}after" must be_===(s"before${o1}after") 67 | so"$o1$o2" must be_===(s"$o1$o2") 68 | so"!$o1!$o2!" must be_===(s"!$o1!$o2!") 69 | so"!$s1!$s2!$s3!$s4!$s5!$s6!$s7!$o1!$o2!$o3!$o4!$o5!$o6!$o7!" must be_===(s"!$s1!$s2!$s3!$s4!$s5!$s6!$s7!$o1!$o2!$o3!$o4!$o5!$o6!$o7!") 70 | } 71 | 72 | "serialize null as s" >> { 73 | so"$nullObject" must be_===(s"$nullObject") 74 | } 75 | 76 | "support expressions" >> { 77 | so"${car.name}" must be_===(s"${car.name}") 78 | } 79 | 80 | "don't call expression multiple times" >> { 81 | val m = mock[Something] 82 | 83 | m.id returns 1 84 | m.name returns "name" 85 | m.obj returns null 86 | 87 | so"${m.id} - ${m.name} - ${m.obj} - ${m.id}" must be_===("1 - name - null - 1") 88 | 89 | got { 90 | two(m).id 91 | one(m).name 92 | one(m).obj 93 | noMoreCallsTo(m) 94 | } 95 | } 96 | } 97 | 98 | val o1: Object = Int.box(10000) 99 | val o2: Object = Int.box(200000) 100 | val o3: Object = Int.box(3000000) 101 | val o4: Object = Int.box(40000000) 102 | val o5: Object = Int.box(500000000) 103 | val o6: Object = Int.box(60000000) 104 | val o7: Object = Int.box(7000000) 105 | val nullObject: Object = null 106 | 107 | val s1 = "10000" 108 | val s2 = "200000" 109 | val s3 = "3000000" 110 | val s4 = "40000000" 111 | val s5 = "500000000" 112 | val s6 = "60000000" 113 | val s7 = "7000000" 114 | 115 | val car = Car("f1") 116 | 117 | trait Something { 118 | def name: String 119 | 120 | def id: Int 121 | 122 | def obj: Object 123 | } 124 | 125 | case class Car(name: String) 126 | 127 | } 128 | --------------------------------------------------------------------------------