├── .gitignore ├── index.js ├── test.js ├── package.json ├── LICENSE ├── benchmark.js ├── readme.md └── alt-benchmark.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // You may be tempted to copy and paste this, 4 | // but take a look at the commit history first, 5 | // this is a moving target so relying on the module 6 | // is the best way to make sure the optimization 7 | // method is kept up to date and compatible with 8 | // every Node version. 9 | 10 | function flatstr (s) { 11 | s | 0 12 | return s 13 | } 14 | 15 | module.exports = flatstr -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | var test = require('tap').test 3 | var flatstr = require('.') 4 | 5 | test('does not throw', function (t) { 6 | t.doesNotThrow(() => { 7 | flatstr('abc') 8 | }) 9 | t.doesNotThrow(() => { 10 | flatstr({}) 11 | }) 12 | t.doesNotThrow(() => { 13 | flatstr(1) 14 | }) 15 | t.doesNotThrow(() => { 16 | flatstr(null) 17 | }) 18 | t.end() 19 | }) 20 | 21 | test('returns the same value that was passed in', function (t) { 22 | var o = {} 23 | t.is(flatstr('abc'), 'abc') 24 | t.is(flatstr(o), o) 25 | t.end() 26 | }) 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flatstr", 3 | "version": "1.0.12", 4 | "description": "Flattens the underlying C structures of a concatenated JavaScript string", 5 | "main": "index.js", 6 | "browser": { 7 | "v8": "./v8" 8 | }, 9 | "tags": [ 10 | "perf", 11 | "performance", 12 | "strings", 13 | "concatenation" 14 | ], 15 | "author": "David Mark Clements", 16 | "license": "MIT", 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/davidmarkclements/flatstr.git" 20 | }, 21 | "bugs": { 22 | "url": "https://github.com/davidmarkclements/flatstr/issues" 23 | }, 24 | "homepage": "https://github.com/davidmarkclements/flatstr#readme", 25 | "devDependencies": { 26 | "fastbench": "^1.0.1", 27 | "tap": "^12.0.1" 28 | }, 29 | "dependencies": {} 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 David Mark Clements 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 | -------------------------------------------------------------------------------- /benchmark.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | var nul = process.platform === 'win32' ? '\\\\.\\NUL' : '/dev/null' 3 | var bench = require('fastbench') 4 | var stream = require('fs').createWriteStream(nul) 5 | var flatstr = require('./') 6 | var largeStr = JSON.stringify(require('./package.json')) 7 | largeStr += largeStr 8 | largeStr += largeStr 9 | 10 | var run = bench([ 11 | function unflattenedManySmallConcats (cb) { 12 | stream.write(makeStr('a', 200)) 13 | setImmediate(cb) 14 | }, 15 | function flattenedManySmallConcats (cb) { 16 | stream.write(flatstr(makeStr('a', 200))) 17 | setImmediate(cb) 18 | }, 19 | function unflattenedSeveralLargeConcats (cb) { 20 | stream.write(makeStr(largeStr, 10)) 21 | setImmediate(cb) 22 | }, 23 | function flattenedSeveralLargeConcats (cb) { 24 | stream.write(flatstr(makeStr(largeStr, 10))) 25 | setImmediate(cb) 26 | }, 27 | function unflattenedExponentialSmallConcats (cb) { 28 | stream.write(makeExpoStr('a', 12)) 29 | setImmediate(cb) 30 | }, 31 | function flattenedExponentialSmallConcats (cb) { 32 | stream.write(flatstr(makeExpoStr('a', 12))) 33 | setImmediate(cb) 34 | }, 35 | function unflattenedExponentialLargeConcats (cb) { 36 | stream.write(makeExpoStr(largeStr, 7)) 37 | setImmediate(cb) 38 | }, 39 | function flattenedExponentialLargeConcats (cb) { 40 | stream.write(flatstr(makeExpoStr(largeStr, 7))) 41 | setImmediate(cb) 42 | } 43 | ], 10000) 44 | 45 | run(run) 46 | 47 | function makeStr (str, concats) { 48 | var s = '' 49 | while (concats--) { 50 | s += str 51 | } 52 | return s 53 | } 54 | 55 | function makeExpoStr (str, concats) { 56 | var s = str 57 | while (concats--) { 58 | s += s 59 | } 60 | return s 61 | } 62 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # flatstr 2 | 3 | Flattens the underlying C structures of a concatenated JavaScript string 4 | 5 | ## About 6 | 7 | If you're doing lots of string concatenation and then writing that 8 | string somewhere, you may find that passing your string through 9 | `flatstr` vastly improves performance. 10 | 11 | ## Usage 12 | 13 | ```js 14 | var flatstr = require('flatstr') 15 | flatstr(someHeavilyConcatenatedString) 16 | ``` 17 | 18 | ## Benchmarks 19 | 20 | Benchmarks test flat vs non-flat strings being written to 21 | an `fs.WriteStream`. 22 | 23 | ``` 24 | unflattenedManySmallConcats*10000: 147.540ms 25 | flattenedManySmallConcats*10000: 105.994ms 26 | unflattenedSeveralLargeConcats*10000: 287.901ms 27 | flattenedSeveralLargeConcats*10000: 226.121ms 28 | unflattenedExponentialSmallConcats*10000: 410.533ms 29 | flattenedExponentialSmallConcats*10000: 219.973ms 30 | unflattenedExponentialLargeConcats*10000: 2774.230ms 31 | flattenedExponentialLargeConcats*10000: 1862.815ms 32 | ``` 33 | 34 | In each case, flattened strings win, 35 | here's the performance gains from using `flatstr` 36 | 37 | ``` 38 | ManySmallConcats: 28% 39 | SeveralLargeConcats: 21% 40 | ExponentialSmallConcats: 46% 41 | ExponentialLargeConcats: 33% 42 | ``` 43 | 44 | ## How does it work 45 | 46 | In the v8 C++ layer, JavaScript strings can be represented in two ways. 47 | 48 | 1. As an array 49 | 2. As a tree 50 | 51 | When JavaScript strings are concatenated, tree structures are used 52 | to represent them. For the concat operation, this is cheaper than 53 | reallocating a larger array. However, performing other operations 54 | on the tree structures can become costly (particularly where lots of 55 | concatenation has occurred). 56 | 57 | V8 has a a method called `String::Flatten`which converts the tree into a C array. This method is typically called before operations that walk through the bytes of the string (for instance, when testing against a regular expression). It may also be called if a string is accessed many times over, 58 | as an optimization on the string. However, strings aren't always flattened. One example is when we pass a string into a `WriteStream`, at some point the string will be converted to a buffer, and this may be expensive if the underlying representation is a tree. 59 | 60 | `String::Flatten` is not exposed as a JavaScript function, but it can be triggered as a side effect. 61 | 62 | There are several ways to indirectly call `String::Flatten` (see `alt-benchmark.js`), 63 | but coercion to a number appears to be (one of) the cheapest. 64 | 65 | However since Node 10 the V8 version has stopped using Flatten in all 66 | places identified. Thus the code has been updated to seamlessly 67 | use the native runtime function `%FlattenString` without having to use 68 | the `--allow-natives-syntax` flag directly. 69 | 70 | One final note: calling flatstr too much can in fact negatively effect performance. For instance, don't call it every time you concat (if that 71 | was performant, v8 wouldn't be using trees in the first place). The best 72 | place to use flatstr is just prior to passing it to an API that eventually 73 | runs non-v8 code (such as `fs.WriteStream`, or perhaps `xhr` or DOM apis in the browser). 74 | 75 | 76 | ## Acknowledgements 77 | 78 | * Sponsored by nearForm 79 | 80 | ## License 81 | 82 | MIT 83 | -------------------------------------------------------------------------------- /alt-benchmark.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | var nul = process.platform === 'win32' ? '\\\\.\\NUL' : '/dev/null' 3 | var bench = require('fastbench') 4 | var stream = require('fs').createWriteStream(nul) 5 | var alt0 = require('./') 6 | var largeStr = JSON.stringify(require('./package.json')) 7 | largeStr += largeStr 8 | largeStr += largeStr 9 | 10 | var run = bench([ 11 | function alt0ManySmallConcats (cb) { 12 | stream.write(alt0(makeStr('a', 200))) 13 | setImmediate(cb) 14 | }, 15 | function alt1ManySmallConcats (cb) { 16 | stream.write(alt1(makeStr('a', 200))) 17 | setImmediate(cb) 18 | }, 19 | function alt2ManySmallConcats (cb) { 20 | stream.write(alt2(makeStr('a', 200))) 21 | setImmediate(cb) 22 | }, 23 | function alt3ManySmallConcats (cb) { 24 | stream.write(alt3(makeStr('a', 200))) 25 | setImmediate(cb) 26 | }, 27 | function alt4ManySmallConcats (cb) { 28 | stream.write(alt4(makeStr('a', 200))) 29 | setImmediate(cb) 30 | }, 31 | function alt5ManySmallConcats (cb) { 32 | stream.write(alt5(makeStr('a', 200))) 33 | setImmediate(cb) 34 | }, 35 | function alt6ManySmallConcats (cb) { 36 | stream.write(alt6(makeStr('a', 200))) 37 | setImmediate(cb) 38 | }, 39 | function alt7ManySmallConcats (cb) { 40 | stream.write(alt7(makeStr('a', 200))) 41 | setImmediate(cb) 42 | }, 43 | function alt8ManySmallConcats (cb) { 44 | stream.write(alt8(makeStr('a', 200))) 45 | setImmediate(cb) 46 | }, 47 | function alt9ManySmallConcats (cb) { 48 | stream.write(alt9(makeStr('a', 200))) 49 | setImmediate(cb) 50 | }, 51 | function alt0SeveralLargeConcats (cb) { 52 | stream.write(alt0(makeStr(largeStr, 10))) 53 | setImmediate(cb) 54 | }, 55 | function alt1SeveralLargeConcats (cb) { 56 | stream.write(alt1(makeStr(largeStr, 10))) 57 | setImmediate(cb) 58 | }, 59 | function alt2SeveralLargeConcats (cb) { 60 | stream.write(alt2(makeStr(largeStr, 10))) 61 | setImmediate(cb) 62 | }, 63 | function alt3SeveralLargeConcats (cb) { 64 | stream.write(alt3(makeStr(largeStr, 10))) 65 | setImmediate(cb) 66 | }, 67 | function alt4SeveralLargeConcats (cb) { 68 | stream.write(alt4(makeStr(largeStr, 10))) 69 | setImmediate(cb) 70 | }, 71 | function alt5SeveralLargeConcats (cb) { 72 | stream.write(alt5(makeStr(largeStr, 10))) 73 | setImmediate(cb) 74 | }, 75 | function alt6SeveralLargeConcats (cb) { 76 | stream.write(alt6(makeStr(largeStr, 10))) 77 | setImmediate(cb) 78 | }, 79 | function alt7SeveralLargeConcats (cb) { 80 | stream.write(alt7(makeStr(largeStr, 10))) 81 | setImmediate(cb) 82 | }, 83 | function alt8SeveralLargeConcats (cb) { 84 | stream.write(alt8(makeStr(largeStr, 10))) 85 | setImmediate(cb) 86 | }, 87 | function alt9SeveralLargeConcats (cb) { 88 | stream.write(alt9(makeStr(largeStr, 10))) 89 | setImmediate(cb) 90 | }, 91 | function alt0ExponentialSmallConcats (cb) { 92 | stream.write(alt0(makeExpoStr('a', 12))) 93 | setImmediate(cb) 94 | }, 95 | function alt1ExponentialSmallConcats (cb) { 96 | stream.write(alt1(makeExpoStr('a', 12))) 97 | setImmediate(cb) 98 | }, 99 | function alt2ExponentialSmallConcats (cb) { 100 | stream.write(alt2(makeExpoStr('a', 12))) 101 | setImmediate(cb) 102 | }, 103 | function alt3ExponentialSmallConcats (cb) { 104 | stream.write(alt3(makeExpoStr('a', 12))) 105 | setImmediate(cb) 106 | }, 107 | function alt4ExponentialSmallConcats (cb) { 108 | stream.write(alt4(makeExpoStr('a', 12))) 109 | setImmediate(cb) 110 | }, 111 | function alt5ExponentialSmallConcats (cb) { 112 | stream.write(alt5(makeExpoStr('a', 12))) 113 | setImmediate(cb) 114 | }, 115 | function alt6ExponentialSmallConcats (cb) { 116 | stream.write(alt6(makeExpoStr('a', 12))) 117 | setImmediate(cb) 118 | }, 119 | function alt7ExponentialSmallConcats (cb) { 120 | stream.write(alt7(makeExpoStr('a', 12))) 121 | setImmediate(cb) 122 | }, 123 | function alt8ExponentialSmallConcats (cb) { 124 | stream.write(alt8(makeExpoStr('a', 12))) 125 | setImmediate(cb) 126 | }, 127 | function alt9ExponentialSmallConcats (cb) { 128 | stream.write(alt9(makeExpoStr('a', 12))) 129 | setImmediate(cb) 130 | }, 131 | function alt0ExponentialLargeConcats (cb) { 132 | stream.write(alt0(makeExpoStr(largeStr, 7))) 133 | setImmediate(cb) 134 | }, 135 | function alt1ExponentialLargeConcats (cb) { 136 | stream.write(alt1(makeExpoStr(largeStr, 7))) 137 | setImmediate(cb) 138 | }, 139 | function alt2ExponentialLargeConcats (cb) { 140 | stream.write(alt2(makeExpoStr(largeStr, 7))) 141 | setImmediate(cb) 142 | }, 143 | function alt3ExponentialLargeConcats (cb) { 144 | stream.write(alt3(makeExpoStr(largeStr, 7))) 145 | setImmediate(cb) 146 | }, 147 | function alt4ExponentialLargeConcats (cb) { 148 | stream.write(alt4(makeExpoStr(largeStr, 7))) 149 | setImmediate(cb) 150 | }, 151 | function alt5ExponentialLargeConcats (cb) { 152 | stream.write(alt5(makeExpoStr(largeStr, 7))) 153 | setImmediate(cb) 154 | }, 155 | function alt6ExponentialLargeConcats (cb) { 156 | stream.write(alt6(makeExpoStr(largeStr, 7))) 157 | setImmediate(cb) 158 | }, 159 | function alt7ExponentialLargeConcats (cb) { 160 | stream.write(alt7(makeExpoStr(largeStr, 7))) 161 | setImmediate(cb) 162 | }, 163 | function alt8ExponentialLargeConcats (cb) { 164 | stream.write(alt8(makeExpoStr(largeStr, 7))) 165 | setImmediate(cb) 166 | }, 167 | function alt9ExponentialLargeConcats (cb) { 168 | stream.write(alt9(makeExpoStr(largeStr, 7))) 169 | setImmediate(cb) 170 | } 171 | ], 10000) 172 | 173 | run(run) 174 | 175 | var rx = /()/ 176 | function alt1 (s) { 177 | rx.test(s) 178 | return s 179 | } 180 | function alt2 (s) { 181 | rx.exec(s) 182 | return s 183 | } 184 | 185 | function alt3 (s) { 186 | s | 0 187 | return s 188 | } 189 | 190 | function alt4 (s) { 191 | ~s 192 | return s 193 | } 194 | 195 | function alt5 (s) { 196 | escape(s) 197 | return s 198 | } 199 | 200 | function alt6 (s) { 201 | unescape(s) 202 | return s 203 | } 204 | 205 | function alt7 (s) { 206 | parseInt(s, 10) 207 | return s 208 | } 209 | 210 | function alt8 (s) { 211 | parseFloat(s) 212 | return s 213 | } 214 | 215 | function alt9 (s) { 216 | alt9[s] = null 217 | return s 218 | } 219 | 220 | function makeStr (str, concats) { 221 | var s = '' 222 | while (concats--) { 223 | s += str 224 | } 225 | return s 226 | } 227 | 228 | function makeExpoStr (str, concats) { 229 | var s = str 230 | while (concats--) { 231 | s += s 232 | } 233 | return s 234 | } 235 | --------------------------------------------------------------------------------