Skip to content

Commit

Permalink
Expand aggressive inliner (#8)
Browse files Browse the repository at this point in the history
- Upgrade operation inliner to support larger function bodies
  - Only bailout scenarios involve conditional or multiple `return` statements
- Support destructuring parameters
- Improve documentation to show code style to avoid bailouts
- Add tests for more use cases
  • Loading branch information
planttheidea authored Jun 17, 2019
1 parent 4e63cf3 commit 8502c19
Show file tree
Hide file tree
Showing 54 changed files with 1,130 additions and 556 deletions.
84 changes: 83 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Iteration helpers that inline to native loops for performance
- [Usage](#usage)
- [Methods](#methods)
- [How it works](#how-it-works)
- [Aggressive inlining](#aggressive-inlining)
- [Bailout scenarios](#bailout-scenarios)
- [Gotchas](#gotchas)
- [`*Object` methods do not perform `hasOwnProperty` check](#object-methods-do-not-perform-hasownproperty-check)
- [`findIndex` vs `findKey`](#findindex-vs-findkey)
Expand Down Expand Up @@ -92,7 +94,9 @@ const foo = _result;
If you are passing uncached values as the array or the handler, it will store those values as local variables and execute the same loop based on those variables.
One extra performance boost is that `inline-loops` will try to inline operations when possible. For example:
### Aggressive inlining
One extra performance boost is that `inline-loops` will try to inline the callback operations when possible. For example:
```javascript
// this
Expand Down Expand Up @@ -141,10 +145,88 @@ for (let _key = 0, _length = array.length, _value; _key < _length; ++_key) {
const isAllTuples = _result;
```
### Bailout scenarios
Inevitably not everything can be inlined, so there are known bailout scenarios:
- When using a cached function reference (we can only inline functions that are statically declared in the macro scope)
- When there are multiple `return` statements (as there is no scope to return from, the conversion of the logic would be highly complex)
- When the `return` statement is not top-level (same reason as with multiple `return`s)
That means if you are cranking every last ounce of performance out of this macro, you want to get cozy with ternaries.
```js
import { map } from 'inline-loops.macro';

// this will bail out to storing the function and calling it in the loop
const deopted = map(array, value => {
if (value % 2 === 0) {
return 'even';
}

return 'odd';
});

// this will inline the operation and avoid function calls
const inlined = map(array, value => (value % 2 === 0 ? 'even' : 'odd'));
```
## Gotchas
Some aspects of implementing this macro that you should be aware of:
### Conditionals do not delay execution
If you do something like this with standard JS:
```js
return isFoo ? array.map(v => v * 2) : array;
```
The `array` is only mapped over if `isFoo` is true. However, because we are inlining these calls into `for` loops in the scope they operate in, this conditional calling does not apply with this macro.
```js
// this
return isFoo ? map(array, v => v * 2) : array;

// turns into this
let _result = [];

for (let _key = 0, _length = array.length, _value; _key < _length; ++_key) {
_value = array[_key];
_result[_key] = _value * 2;
}

return isFoo ? _result : array;
```
Notice the mapping occurs whether the condition is met or not. If you want to ensure this conditionality is maintained, you should use an `if` block instead:
```js
// this
if (isFoo) {
return map(array, v => v * 2);
}

return array;

// turns into this
if (isFoo) {
let _result = [];

for (let _key = 0, _length = array.length, _value; _key < _length; ++_key) {
_value = array[_key];
_result[_key] = _value * 2;
}

return _result;
}

return array;
```
This will ensure the potentially expensive computation only occurs when necessary.
### `*Object` methods do not perform `hasOwnProperty` check
The object methods will do operations in `for-in` loop, but will not guard via a `hasOwnProperty` check. For example:
Expand Down
7 changes: 7 additions & 0 deletions __tests__/__fixtures__/bailout/early-return/code.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { filter } from '../../../../src/inline-loops.macro';

const result = filter([1, 2, 3], (value) => {
if (value === 2) {
return true;
}
});
16 changes: 16 additions & 0 deletions __tests__/__fixtures__/bailout/early-return/output.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const _iterable = [1, 2, 3];

const _fn = value => {
if (value === 2) {
return true;
}
};

let _result = [];

for (let _key = 0, _length = _iterable.length, _value; _key < _length; ++_key) {
_value = _iterable[_key];
if (_fn(_value, _key, _iterable)) _result.push(_value);
}

const result = _result;
9 changes: 9 additions & 0 deletions __tests__/__fixtures__/bailout/multiple-returns/code.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { map } from '../../../../src/inline-loops.macro';

const result = map([1, 2, 3], (value) => {
if (value === 2) {
return 82;
}

return value;
});
18 changes: 18 additions & 0 deletions __tests__/__fixtures__/bailout/multiple-returns/output.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const _iterable = [1, 2, 3];

const _fn = value => {
if (value === 2) {
return 82;
}

return value;
};

let _result = [];

for (let _key = 0, _length = _iterable.length, _value; _key < _length; ++_key) {
_value = _iterable[_key];
_result[_key] = _fn(_value, _key, _iterable);
}

const result = _result;
9 changes: 9 additions & 0 deletions __tests__/__fixtures__/complex/conditional/code.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { map } from '../../../../src/inline-loops.macro';

function getStuff() {
if (foo === 'bar') {
return map(array, v => v * 2);
}

return array;
}
14 changes: 14 additions & 0 deletions __tests__/__fixtures__/complex/conditional/output.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
function getStuff() {
if (foo === 'bar') {
let _result = [];

for (let _key = 0, _length = array.length, _value; _key < _length; ++_key) {
_value = array[_key];
_result[_key] = _value * 2;
}

return _result;
}

return array;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { forEach } from '../../../../src/inline-loops.macro';

forEach([], ([a, b]) => {
forEach([], ([a, [b]]) => {
console.log(a, b);
});
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
const _iterable = [];

const _fn = ([a, b]) => {
console.log(a, b);
};

for (let _key = 0, _length = _iterable.length, _value; _key < _length; ++_key) {
_value = _iterable[_key];

_fn(_value, _key, _iterable);
const [_a, [_b]] = _value;
console.log(_a, _b);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { forEach } from '../../../../src/inline-loops.macro';

forEach([], ({ a, b: { c } }) => {
console.log(a, c);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const _iterable = [];

for (let _key = 0, _length = _iterable.length, _value; _key < _length; ++_key) {
_value = _iterable[_key];
const {
a: _a,
b: {
c: _c
}
} = _value;
console.log(_a, _c);
}
30 changes: 30 additions & 0 deletions __tests__/__fixtures__/complex/inlined-large-function/code.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { filter, map, reduceObject } from '../../../../src/inline-loops.macro';

const result = filter(array, (value, index) => {
// usage inside
const mapped = map(array, value => value * 2);

// custom for loop with let
for (let i = 0; i < mapped.length; i++) {
mapped[i] = mapped[i] ** 2;
}

// custom for loop with var
for (var i = 0; i < mapped.length; i++) {
mapped[i] = mapped[i] ** 2;
}

// another iteration, using the mapped values
const reduced = reduceObject(object, value => ({
[value]: mapped,
}));

// custom for-in
for (var key in reduced) {
if (reduced[key] < 0) {
delete reduced[key];
}
}

return reduced[100];
});
54 changes: 54 additions & 0 deletions __tests__/__fixtures__/complex/inlined-large-function/output.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
let _result = [];

for (let _key = 0, _length = array.length, _value; _key < _length; ++_key) {
_value = array[_key];
let _result2 = [];

for (let _key3 = 0, _length2 = array.length, _value2; _key3 < _length2; ++_key3) {
_value2 = array[_key3];
_result2[_key3] = _value2 * 2;
}

// usage inside
const _mapped = _result2; // custom for loop with let

for (let i = 0; i < _mapped.length; i++) {
_mapped[i] = _mapped[i] ** 2;
} // custom for loop with var


for (var _i2 = 0; _i2 < _mapped.length; _i2++) {
_mapped[_i2] = _mapped[_i2] ** 2;
} // another iteration, using the mapped values


let _hasInitialValue = false;

let _value3;

let _result3;

for (let _key4 in object) {
if (_hasInitialValue) {
_value3 = object[_key4];
_result3 = {
[_result3]: _mapped
};
} else {
_hasInitialValue = true;
_result3 = object[_key4];
}
}

const _reduced = _result3; // custom for-in

for (var _key2 in _reduced) {
if (_reduced[_key2] < 0) {
delete _reduced[_key2];
}
}

if (_reduced[100]) _result.push(_value);
}

const result = _result;
7 changes: 7 additions & 0 deletions __tests__/__fixtures__/complex/this/code.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { map } from '../../../../src/inline-loops.macro';

function foo(array) {
return map(array, function (value) {
return this && this.foo ? value : null;
});
}
10 changes: 10 additions & 0 deletions __tests__/__fixtures__/complex/this/output.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
function foo(array) {
let _result = [];

for (let _key = 0, _length = array.length, _value; _key < _length; ++_key) {
_value = array[_key];
_result[_key] = this && this.foo ? _value : null;
}

return _result;
}
10 changes: 3 additions & 7 deletions __tests__/__fixtures__/uncached/every/output.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
const _iterable = [1, 2, 3, 4];

const _fn = value => {
const isValueEven = value % 2 === 0;
return isValueEven;
};

let _result = true;

for (let _key = 0, _length = _iterable.length, _value; _key < _length; ++_key) {
_value = _iterable[_key];

if (!_fn(_value, _key, _iterable)) {
const _isValueEven = _value % 2 === 0;

if (!_isValueEven) {
_result = false;
break;
}
Expand Down
10 changes: 3 additions & 7 deletions __tests__/__fixtures__/uncached/everyObject/output.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,16 @@ const _iterable = {
three: 3,
four: 4
};

const _fn = value => {
const isValueEven = value % 2 === 0;
return isValueEven;
};

let _result = true;

let _value;

for (let _key in _iterable) {
_value = _iterable[_key];

if (!_fn(_value, _key, _iterable)) {
const _isValueEven = _value % 2 === 0;

if (!_isValueEven) {
_result = false;
break;
}
Expand Down
8 changes: 6 additions & 2 deletions __tests__/__fixtures__/uncached/everyRight/code.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { everyRight } from "../../../../src/inline-loops.macro";
import { everyRight } from '../../../../src/inline-loops.macro';

const areAllEven = everyRight(array, value => value % 2 === 0);
const areAllEven = everyRight([1, 2, 3, 4], (value) => {
const isValueEven = value % 2 === 0;

return isValueEven;
});
Loading

0 comments on commit 8502c19

Please sign in to comment.