Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 38 additions & 6 deletions benchmark.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,73 @@ var bench = require('fastbench')
var stream = require('fs').createWriteStream('/dev/null')
var flatstr = require('./')
var largeStr = JSON.stringify(require('./package.json'))
largeStr += largeStr
largeStr += largeStr
largeStr += largeStr

var run = bench([
function unflattenedManySmallConcats(cb) {
stream.write(makeStr('a', 200))
setImmediate(cb)
},
},
function flattenedManySmallConcats(cb) {
stream.write(flatstr(makeStr('a', 200)))
setImmediate(cb)
},
function flattenedManySmallConcatsTwice(cb) {
stream.write(flatstr(flatstr(makeStr('a', 200))))
setImmediate(cb)
},
function flattenedManySmallConcatsTriple(cb) {
stream.write(flatstr(flatstr(flatstr(makeStr('a', 200)))))
setImmediate(cb)
},
function unflattenedSeveralLargeConcats(cb) {
stream.write(makeStr(largeStr, 10))
setImmediate(cb)
},
},
function flattenedSeveralLargeConcats(cb) {
stream.write(flatstr(makeStr(largeStr, 10)))
setImmediate(cb)
},
function flattenedSeveralLargeConcatsTwice(cb) {
stream.write(flatstr(flatstr(makeStr(largeStr, 10))))
setImmediate(cb)
},
function flattenedSeveralLargeConcatsTriple(cb) {
stream.write(flatstr(flatstr(flatstr(makeStr(largeStr, 10)))))
setImmediate(cb)
},
function unflattenedExponentialSmallConcats(cb) {
stream.write(makeExpoStr('a', 12))
setImmediate(cb)
},
},
function flattenedExponentialSmallConcats(cb) {
stream.write(flatstr(makeExpoStr('a', 12)))
setImmediate(cb)
},
function flattenedExponentialSmallConcatsTwice(cb) {
stream.write(flatstr(flatstr(makeExpoStr('a', 12))))
setImmediate(cb)
},
function flattenedExponentialSmallConcatsTriple(cb) {
stream.write(flatstr(flatstr(flatstr(makeExpoStr('a', 12)))))
setImmediate(cb)
},
function unflattenedExponentialLargeConcats(cb) {
stream.write(makeExpoStr(largeStr, 7))
setImmediate(cb)
},
},
function flattenedExponentialLargeConcats(cb) {
stream.write(flatstr(makeExpoStr(largeStr, 7)))
setImmediate(cb)
},
function flattenedExponentialLargeConcatsTwice(cb) {
stream.write(flatstr(flatstr(makeExpoStr(largeStr, 7))))
setImmediate(cb)
},
function flattenedExponentialLargeConcatsTriple(cb) {
stream.write(flatstr(flatstr(flatstr(makeExpoStr(largeStr, 7)))))
setImmediate(cb)
}
], 10000)

Expand All @@ -57,4 +89,4 @@ function makeExpoStr(str, concats) {
s += s
}
return s
}
}
68 changes: 45 additions & 23 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Flattens the underlying C structures of a concatenated JavaScript string
## About

If you're doing lots of string concatenation and then writing that
string somewhere, you may find that passing your string through
string somewhere, you may find that passing your string through
`flatstr` vastly improves performance.

## Usage
Expand All @@ -17,47 +17,69 @@ flatstr(someHeavilyConcatenatedString)

## Benchmarks

Benchmarks test flat vs non-flat strings being written to
Benchmarks test flat vs non-flat strings being written to
an `fs.WriteStream`.

```
unflattenedManySmallConcats*10000: 147.540ms
flattenedManySmallConcats*10000: 105.994ms
unflattenedSeveralLargeConcats*10000: 287.901ms
flattenedSeveralLargeConcats*10000: 226.121ms
unflattenedExponentialSmallConcats*10000: 410.533ms
flattenedExponentialSmallConcats*10000: 219.973ms
unflattenedExponentialLargeConcats*10000: 2774.230ms
flattenedExponentialLargeConcats*10000: 1862.815ms
unflattenedManySmallConcats*10000: 204.155ms
flattenedManySmallConcats*10000: 159.666ms
flattenedManySmallConcatsTwice*10000: 163.746ms
flattenedManySmallConcatsTriple*10000: 160.908ms

unflattenedSeveralLargeConcats*10000: 451.027ms
flattenedSeveralLargeConcats*10000: 344.129ms
flattenedSeveralLargeConcatsTwice*10000: 350.910ms
flattenedSeveralLargeConcatsTriple*10000: 341.492ms

unflattenedExponentialSmallConcats*10000: 475.649ms
flattenedExponentialSmallConcats*10000: 190.150ms
flattenedExponentialSmallConcatsTwice*10000: 190.556ms
flattenedExponentialSmallConcatsTriple*10000: 188.864ms

unflattenedExponentialLargeConcats*10000: 4268.143ms
flattenedExponentialLargeConcats*10000: 3121.345ms
flattenedExponentialLargeConcatsTwice*10000: 3109.011ms
flattenedExponentialLargeConcatsTriple*10000: 3128.649ms
```

In each case, flattened strings win,
here's the performance gains from using `flatstr`
In each case, flattened strings win,
here's the performance gains from using `flatstr`.

```
ManySmallConcats: 28%
SeveralLargeConcats: 21%
ExponentialSmallConcats: 46%
ExponentialLargeConcats: 33%
ManySmallConcats: 22%
SeveralLargeConcats: 24%
ExponentialSmallConcats: 60%
ExponentialLargeConcats: 29%
```

As shown above, even applying `flatstr` multiple time there're benefits!
In fact comparing the unflattened string with the triple flattened ones the results become:

```
ManySmallConcats: 21%
SeveralLargeConcats: 24%
ExponentialSmallConcats: 60%
ExponentialLargeConcats: 27%

```

## How does it work

In the v8 C++ layer, JavaScript strings can be represented in two ways.
In the v8 C++ layer, JavaScript strings can be represented in two ways.

1. As an array
2. As a tree

When JavaScript strings are concatenated, tree structures are used
to represent them. For the concat operation, this is cheaper than
reallocating a larger array. However, performing other operations
reallocating a larger array. However, performing other operations
on the tree structures can become costly (particularly where lots of
concatenation has occurred).
concatenation has occurred).

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,
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.
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,
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.

`String::Flatten` is not exposed as a JavaScript function, but it can be triggered as a side effect.
`String::Flatten` is not exposed as a JavaScript function, but it can be triggered as a side effect.

There are several ways to indirectly call `String::Flatten` (see `alt-benchmark.js`), but coercion to a number appears to be (one of) the cheapest.

Expand All @@ -79,7 +101,7 @@ function wrapper adds negligible overhead.
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
was performant, v8 wouldn't be using trees in the first place). The best
place to use flatstr is just prior to passing it to an API that eventually
runs non-v8 code (such as `fs.WriteStream`, or perhaps `xhr` or DOM apis in the browser).
runs non-v8 code (such as `fs.WriteStream`, or perhaps `xhr` or DOM apis in the browser).


## Acknowledgements
Expand Down