Skip to content

Commit

Permalink
Performance and test improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
jwagner committed Jul 23, 2022
1 parent 8d3d284 commit 6608759
Show file tree
Hide file tree
Showing 17 changed files with 115 additions and 51 deletions.
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ and fairly fast (about 20 nanoseconds for a sample of 2d noise) and tree shakeab

## Demos

- [Synth Wave Demo](https://29a.ch/sandbox/2022/simplex-noise-synthwave/) shown in the header
- [Synthwave Demo](https://29a.ch/sandbox/2022/simplex-noise-synthwave/) shown in the header
- Simple 2D plasma on [codepen.io](http://codepen.io/jwagner/pen/BNmpdm/?editors=001).
- [3D voxel world generation](https://29a.ch/sandbox/2012/voxelworld/) example.
- Film grain in [analog film emulator](https://29a.ch/film-emulator/).
Expand Down Expand Up @@ -90,11 +90,9 @@ So ~20 nanoseconds per call.

```
$ node perf/index.js
27745787.933336906
init: 192,590 ops/sec ±1%
noise2D: 57,928,891 ops/sec ±1%
noise3D: 34,159,230 ops/sec ±0%
noise4D: 24,589,786 ops/sec ±0%
noise2D: 66,608,762 ops/sec ±0%
noise3D: 41,059,121 ops/sec ±0%
noise4D: 33,406,638 ops/sec ±0%
```

At least at a glance it also seems to be faster than 'fast-simplex-noise':
Expand Down Expand Up @@ -159,6 +157,8 @@ const simplex = {
When combined with tree-shaking this helps with build sizes.
- Removed the built in version of the alea PRNG to focus the library to do only one thing.
If you want to continue to use it you'll have to install and import it separately.
- Noise functions are a bit faster (~ 10-20%) due to using integers in some places
- Noise values can be different from previous versions especially for inputs > 2^31
- Test coverage is now at 100%

### 3.0.1
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,6 @@
"build": "./build.sh",
"docs": "typedoc --excludePrivate --out public/docs simplex-noise.ts",
"prepare": "npm run-script build",
"benchmark": "parcel build && node ./perf/benchmark.js"
"benchmark": "npm run build && cd perf && ./benchmark.sh"
}
}
9 changes: 5 additions & 4 deletions perf/benchmark.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/bin/sh
sudo cpufreq-set -g performance
# use core 0 + 4 (which is hyperthreading for core 0)
sudo nice -n -10 sudo -u $USER taskset -c 0,4 `which node` --random_seed=1 --hash_seed=1 --expose-gc index.js
sudo cpufreq-set -g powersave
# using powersave yields more consistent results
sudo cpupower frequency-set -g powersave
# use core 0 + 16 (which is hyperthreading for core 0)
sudo nice -n -10 sudo -u $USER taskset -c 0,16 `which node` --random_seed=1 --hash_seed=1 --expose-gc index.js
sudo cpufreq-set -g schedutil
12 changes: 6 additions & 6 deletions perf/index.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const Benchmark = require('benchmark');
const Alea = require('alea');
const {noiseFunction2D, noiseFunction3D, noiseFunction4D} = require('..');
const {createNoise2D, createNoise3D, createNoise4D} = require('..');

const invocationsPerRun = 8*8*8;
const rnd = () => new Alea('seed');
const noise2D = noiseFunction2D(rnd());
const noise3D = noiseFunction3D(rnd());
const noise4D = noiseFunction4D(rnd());
const noise2D = createNoise2D(rnd());
const noise3D = createNoise3D(rnd());
const noise4D = createNoise4D(rnd());

// prevent the compiler from optimizing away the calls
let sideEffect = 0.0;
Expand All @@ -23,7 +23,7 @@ new Benchmark.Suite('simplex-noise')
}
}
sideEffect += a;
})
}, {minTime: 30})
.add('noise3D', function() {
let a = 0.0;
for (let x = 0; x < 8; x++) {
Expand Down Expand Up @@ -58,4 +58,4 @@ new Benchmark.Suite('simplex-noise')
);
});
})
.run({delay: 10, minTime: 20, maxTime: 25});
.run({delay: 10, minTime: 50, maxTime: 120, minSamples: 10000});
82 changes: 58 additions & 24 deletions simplex-noise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,30 @@ const G3 = 1.0 / 6.0;
const F4 = (Math.sqrt(5.0) - 1.0) / 4.0;
const G4 = (5.0 - Math.sqrt(5.0)) / 20.0;

const grad3 = new Float32Array([1, 1, 0,
// I'm really not sure why this | 0 (basically a coercion to int)
// is making this faster but I get ~5 million ops/sec more on the
// benchmarks across the board or a ~10% speedup.
const fastFloor = (x: number) => Math.floor(x) | 0;

const grad2 = new Float64Array([1, 1,
-1, 1,
1, -1,

-1, -1,
1, 0,
-1, 0,

1, 0,
-1, 0,
0, 1,

0, -1,
0, 1,
0, -1]);

// double seems to be faster than single or int's
// probably because most operations are in double precision
const grad3 = new Float64Array([1, 1, 0,
-1, 1, 0,
1, -1, 0,

Expand All @@ -50,7 +73,8 @@ const grad3 = new Float32Array([1, 1, 0,
0, 1, -1,
0, -1, -1]);

const grad4 = new Float32Array([0, 1, 1, 1, 0, 1, 1, -1, 0, 1, -1, 1, 0, 1, -1, -1,
// double is a bit quicker here as well
const grad4 = new Float64Array([0, 1, 1, 1, 0, 1, 1, -1, 0, 1, -1, 1, 0, 1, -1, -1,
0, -1, 1, 1, 0, -1, 1, -1, 0, -1, -1, 1, 0, -1, -1, -1,
1, 0, 1, 1, 1, 0, 1, -1, 1, 0, -1, 1, 1, 0, -1, -1,
-1, 0, 1, 1, -1, 0, 1, -1, -1, 0, -1, 1, -1, 0, -1, -1,
Expand Down Expand Up @@ -79,15 +103,18 @@ export type NoiseFunction2D = (x: number, y: number) => number;
*/
export function createNoise2D(random: RandomFn = Math.random): NoiseFunction2D {
const perm = buildPermutationTable(random);
const permMod12 = perm.map(v => v % 12);
// precalculating this yields a little ~3% performance improvement.
const permGrad2x = new Float64Array(perm).map(v => grad2[(v % 12) * 2]);
const permGrad2y = new Float64Array(perm).map(v => grad2[(v % 12) * 2 + 1]);
return function noise2D(x: number, y: number): number {
// if(!isFinite(x) || !isFinite(y)) return 0;
let n0 = 0; // Noise contributions from the three corners
let n1 = 0;
let n2 = 0;
// Skew the input space to determine which simplex cell we're in
const s = (x + y) * F2; // Hairy factor for 2D
const i = Math.floor(x + s);
const j = Math.floor(y + s);
const i = fastFloor(x + s);
const j = fastFloor(y + s);
const t = (i + j) * G2;
const X0 = i - t; // Unskew the cell origin back to (x,y) space
const Y0 = j - t;
Expand Down Expand Up @@ -117,21 +144,30 @@ export function createNoise2D(random: RandomFn = Math.random): NoiseFunction2D {
// Calculate the contribution from the three corners
let t0 = 0.5 - x0 * x0 - y0 * y0;
if (t0 >= 0) {
const gi0 = permMod12[ii + perm[jj]] * 3;
const gi0 = ii + perm[jj];
const g0x = permGrad2x[gi0];
const g0y = permGrad2y[gi0];
t0 *= t0;
n0 = t0 * t0 * (grad3[gi0] * x0 + grad3[gi0 + 1] * y0); // (x,y) of grad3 used for 2D gradient
// n0 = t0 * t0 * (grad2[gi0] * x0 + grad2[gi0 + 1] * y0); // (x,y) of grad3 used for 2D gradient
n0 = t0 * t0 * (g0x * x0 + g0y * y0);
}
let t1 = 0.5 - x1 * x1 - y1 * y1;
if (t1 >= 0) {
const gi1 = permMod12[ii + i1 + perm[jj + j1]] * 3;
const gi1 = ii + i1 + perm[jj + j1];
const g1x = permGrad2x[gi1];
const g1y = permGrad2y[gi1];
t1 *= t1;
n1 = t1 * t1 * (grad3[gi1] * x1 + grad3[gi1 + 1] * y1);
// n1 = t1 * t1 * (grad2[gi1] * x1 + grad2[gi1 + 1] * y1);
n1 = t1 * t1 * (g1x * x1 + g1y * y1);
}
let t2 = 0.5 - x2 * x2 - y2 * y2;
if (t2 >= 0) {
const gi2 = permMod12[ii + 1 + perm[jj + 1]] * 3;
const gi2 = ii + 1 + perm[jj + 1];
const g2x = permGrad2x[gi2];
const g2y = permGrad2y[gi2];
t2 *= t2;
n2 = t2 * t2 * (grad3[gi2] * x2 + grad3[gi2 + 1] * y2);
// n2 = t2 * t2 * (grad2[gi2] * x2 + grad2[gi2 + 1] * y2);
n2 = t2 * t2 * (g2x * x2 + g2y * y2);
}
// Add contributions from each corner to get the final noise value.
// The result is scaled to return values in the interval [-1,1].
Expand All @@ -155,15 +191,13 @@ export type NoiseFunction3D = (x: number, y: number, z: number) => number;
*/
export function createNoise3D(random: RandomFn = Math.random): NoiseFunction3D {
const perm = buildPermutationTable(random);
const permMod12 = perm.map(v => v % 12);

return function noise3D(x: number, y: number, z: number): number {
let n0, n1, n2, n3; // Noise contributions from the four corners
// Skew the input space to determine which simplex cell we're in
const s = (x + y + z) * F3; // Very nice and simple skew factor for 3D
const i = Math.floor(x + s);
const j = Math.floor(y + s);
const k = Math.floor(z + s);
const i = fastFloor(x + s);
const j = fastFloor(y + s);
const k = fastFloor(z + s);
const t = (i + j + k) * G3;
const X0 = i - t; // Unskew the cell origin back to (x,y,z) space
const Y0 = j - t;
Expand Down Expand Up @@ -248,28 +282,28 @@ export function createNoise3D(random: RandomFn = Math.random): NoiseFunction3D {
let t0 = 0.6 - x0 * x0 - y0 * y0 - z0 * z0;
if (t0 < 0) n0 = 0.0;
else {
const gi0 = permMod12[ii + perm[jj + perm[kk]]] * 3;
const gi0 = (perm[ii + perm[jj + perm[kk]]] % 12) * 3;
t0 *= t0;
n0 = t0 * t0 * (grad3[gi0] * x0 + grad3[gi0 + 1] * y0 + grad3[gi0 + 2] * z0);
}
let t1 = 0.6 - x1 * x1 - y1 * y1 - z1 * z1;
if (t1 < 0) n1 = 0.0;
else {
const gi1 = permMod12[ii + i1 + perm[jj + j1 + perm[kk + k1]]] * 3;
const gi1 = (perm[ii + i1 + perm[jj + j1 + perm[kk + k1]]] % 12) * 3;
t1 *= t1;
n1 = t1 * t1 * (grad3[gi1] * x1 + grad3[gi1 + 1] * y1 + grad3[gi1 + 2] * z1);
}
let t2 = 0.6 - x2 * x2 - y2 * y2 - z2 * z2;
if (t2 < 0) n2 = 0.0;
else {
const gi2 = permMod12[ii + i2 + perm[jj + j2 + perm[kk + k2]]] * 3;
const gi2 = (perm[ii + i2 + perm[jj + j2 + perm[kk + k2]]] % 12) * 3;
t2 *= t2;
n2 = t2 * t2 * (grad3[gi2] * x2 + grad3[gi2 + 1] * y2 + grad3[gi2 + 2] * z2);
}
let t3 = 0.6 - x3 * x3 - y3 * y3 - z3 * z3;
if (t3 < 0) n3 = 0.0;
else {
const gi3 = permMod12[ii + 1 + perm[jj + 1 + perm[kk + 1]]] * 3;
const gi3 = (perm[ii + 1 + perm[jj + 1 + perm[kk + 1]]] % 12) * 3;
t3 *= t3;
n3 = t3 * t3 * (grad3[gi3] * x3 + grad3[gi3 + 1] * y3 + grad3[gi3 + 2] * z3);
}
Expand Down Expand Up @@ -300,10 +334,10 @@ export function createNoise4D(random: RandomFn = Math.random) {
let n0, n1, n2, n3, n4; // Noise contributions from the five corners
// Skew the (x,y,z,w) space to determine which cell of 24 simplices we're in
const s = (x + y + z + w) * F4; // Factor for 4D skewing
const i = Math.floor(x + s);
const j = Math.floor(y + s);
const k = Math.floor(z + s);
const l = Math.floor(w + s);
const i = fastFloor(x + s);
const j = fastFloor(y + s);
const k = fastFloor(z + s);
const l = fastFloor(w + s);
const t = (i + j + k + l) * G4; // Factor for 4D unskewing
const X0 = i - t; // Unskew the cell origin back to (x,y,z,w) space
const Y0 = j - t;
Expand Down
Binary file removed snapshots/noise2D.png
Binary file not shown.
Binary file added snapshots/noise2Dgiga.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added snapshots/noise2Dlarge.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added snapshots/noise2Dsmall.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed snapshots/noise3D.png
Binary file not shown.
Binary file added snapshots/noise3Dlarge.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added snapshots/noise3Dsmall.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed snapshots/noise4D.png
Binary file not shown.
Binary file added snapshots/noise4Dlarge.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added snapshots/noise4Dsmall.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions test/matches-snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,10 @@ export function sampleFunctionToImageData(f: SampleFunction, width: number, heig
}
}
return imageData;
}

// same as sampleFunctionToImageData but x and y go from -1 .. 1 instead of 0 .. width
// output is scaled from -1 .. 1 to 0 .. 255
export function sampleFunctionToImageDataOne(f: SampleFunction, width: number, height: number): ImageDataLike {
return sampleFunctionToImageData((x, y) => f(x / width * 2 - 1, y / height * 2 - 1) * 128 + 127, width, height);
}
43 changes: 33 additions & 10 deletions test/simplex-noise-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import alea from 'alea';
import { assert } from 'chai';


import { assertMatchesImage, sampleFunctionToImageData } from './matches-snapshot';
import { assertMatchesImage, sampleFunctionToImageDataOne } from './matches-snapshot';

function getRandom(seed = 'seed') {
return alea(seed);
Expand Down Expand Up @@ -91,9 +91,20 @@ describe('createNoise2D', () => {
it('should return similar values for similar inputs', function () {
assert(Math.abs(noise2D(0.1, 0.2) - noise2D(0.101, 0.202)) < 0.1);
});
it('should match snapshot', function () {
const actual = sampleFunctionToImageData((x, y) => noise2D(x / 10, y / 10) * 128 + 128, 64, 64);
assertMatchesImage(actual, 'noise2D.png');
it('should match snapshot for small inputs', function () {
const size = 64;
const actual = sampleFunctionToImageDataOne((x, y) => noise2D(x * 2, y * 2), size, size);
assertMatchesImage(actual, 'noise2Dsmall.png');
});
it('should match snapshot for large inputs', function () {
const size = 64;
const actual = sampleFunctionToImageDataOne((x, y) => noise2D(x * 1000, y * 1000), size, size);
assertMatchesImage(actual, 'noise2Dlarge.png');
});
it('should match snapshot for gigantic inputs', function () {
const size = 64;
const actual = sampleFunctionToImageDataOne((x, y) => noise2D(x * 5e9, y * 5e9), size, size);
assertMatchesImage(actual, 'noise2Dgiga.png');
});
});
});
Expand Down Expand Up @@ -131,9 +142,15 @@ describe('createNoise3D', () => {
it('should return similar values for similar inputs', function () {
assert(Math.abs(noise3D(0.1, 0.2, 0.3) - noise3D(0.101, 0.202, 0.303)) < 0.1);
});
it('should match snapshot', function () {
const actual = sampleFunctionToImageData((x, y) => noise3D(x / 10, y / 10, (x + y) / 2) * 128 + 128, 64, 64);
assertMatchesImage(actual, 'noise3D.png');
it('should match snapshot for small inputs', function () {
const size = 64;
const actual = sampleFunctionToImageDataOne((x, y) => noise3D(x * 2, y * 2, (x + y)), size, size);
assertMatchesImage(actual, 'noise3Dsmall.png');
});
it('should match snapshot for large inputs', function () {
const size = 64;
const actual = sampleFunctionToImageDataOne((x, y) => noise3D(x * 1000, y * 1000, (x + y) * 500), size, size);
assertMatchesImage(actual, 'noise3Dlarge.png');
});
});
});
Expand Down Expand Up @@ -171,9 +188,15 @@ describe('createNoise4D', () => {
it('should return similar values for similar inputs', function () {
assert(Math.abs(noise4D(0.1, 0.2, 0.3, 0.4) - noise4D(0.101, 0.202, 0.303, 0.404)) < 0.1);
});
it('should match snapshot', function () {
const actual = sampleFunctionToImageData((x, y) => noise4D(x / 10, y / 10, x / 4, y / 3) * 128 + 128, 64, 64);
assertMatchesImage(actual, 'noise4D.png');
it('should match snapshot for small inputs', function () {
const size = 64;
const actual = sampleFunctionToImageDataOne((x, y) => noise4D(x * 2, y * 2, x + y, x - y), size, size);
assertMatchesImage(actual, 'noise4Dsmall.png');
});
it('should match snapshot for large inputs', function () {
const size = 64;
const actual = sampleFunctionToImageDataOne((x, y) => noise4D(x * 1000, y * 1000, (x + y) * 500, (x - y) * 500), size, size);
assertMatchesImage(actual, 'noise4Dlarge.png');
});
});
});

0 comments on commit 6608759

Please sign in to comment.