Skip to content

Commit 6608759

Browse files
committed
Performance and test improvements
1 parent 8d3d284 commit 6608759

17 files changed

+115
-51
lines changed

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ and fairly fast (about 20 nanoseconds for a sample of 2d noise) and tree shakeab
1212

1313
## Demos
1414

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

9191
```
9292
$ node perf/index.js
93-
27745787.933336906
94-
init: 192,590 ops/sec ±1%
95-
noise2D: 57,928,891 ops/sec ±1%
96-
noise3D: 34,159,230 ops/sec ±0%
97-
noise4D: 24,589,786 ops/sec ±0%
93+
noise2D: 66,608,762 ops/sec ±0%
94+
noise3D: 41,059,121 ops/sec ±0%
95+
noise4D: 33,406,638 ops/sec ±0%
9896
```
9997

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

164164
### 3.0.1

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,6 @@
7777
"build": "./build.sh",
7878
"docs": "typedoc --excludePrivate --out public/docs simplex-noise.ts",
7979
"prepare": "npm run-script build",
80-
"benchmark": "parcel build && node ./perf/benchmark.js"
80+
"benchmark": "npm run build && cd perf && ./benchmark.sh"
8181
}
8282
}

perf/benchmark.sh

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#!/bin/sh
2-
sudo cpufreq-set -g performance
3-
# use core 0 + 4 (which is hyperthreading for core 0)
4-
sudo nice -n -10 sudo -u $USER taskset -c 0,4 `which node` --random_seed=1 --hash_seed=1 --expose-gc index.js
5-
sudo cpufreq-set -g powersave
2+
# using powersave yields more consistent results
3+
sudo cpupower frequency-set -g powersave
4+
# use core 0 + 16 (which is hyperthreading for core 0)
5+
sudo nice -n -10 sudo -u $USER taskset -c 0,16 `which node` --random_seed=1 --hash_seed=1 --expose-gc index.js
6+
sudo cpufreq-set -g schedutil

perf/index.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
/* eslint-disable @typescript-eslint/no-var-requires */
22
const Benchmark = require('benchmark');
33
const Alea = require('alea');
4-
const {noiseFunction2D, noiseFunction3D, noiseFunction4D} = require('..');
4+
const {createNoise2D, createNoise3D, createNoise4D} = require('..');
55

66
const invocationsPerRun = 8*8*8;
77
const rnd = () => new Alea('seed');
8-
const noise2D = noiseFunction2D(rnd());
9-
const noise3D = noiseFunction3D(rnd());
10-
const noise4D = noiseFunction4D(rnd());
8+
const noise2D = createNoise2D(rnd());
9+
const noise3D = createNoise3D(rnd());
10+
const noise4D = createNoise4D(rnd());
1111

1212
// prevent the compiler from optimizing away the calls
1313
let sideEffect = 0.0;
@@ -23,7 +23,7 @@ new Benchmark.Suite('simplex-noise')
2323
}
2424
}
2525
sideEffect += a;
26-
})
26+
}, {minTime: 30})
2727
.add('noise3D', function() {
2828
let a = 0.0;
2929
for (let x = 0; x < 8; x++) {
@@ -58,4 +58,4 @@ new Benchmark.Suite('simplex-noise')
5858
);
5959
});
6060
})
61-
.run({delay: 10, minTime: 20, maxTime: 25});
61+
.run({delay: 10, minTime: 50, maxTime: 120, minSamples: 10000});

simplex-noise.ts

Lines changed: 58 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,30 @@ const G3 = 1.0 / 6.0;
3434
const F4 = (Math.sqrt(5.0) - 1.0) / 4.0;
3535
const G4 = (5.0 - Math.sqrt(5.0)) / 20.0;
3636

37-
const grad3 = new Float32Array([1, 1, 0,
37+
// I'm really not sure why this | 0 (basically a coercion to int)
38+
// is making this faster but I get ~5 million ops/sec more on the
39+
// benchmarks across the board or a ~10% speedup.
40+
const fastFloor = (x: number) => Math.floor(x) | 0;
41+
42+
const grad2 = new Float64Array([1, 1,
43+
-1, 1,
44+
1, -1,
45+
46+
-1, -1,
47+
1, 0,
48+
-1, 0,
49+
50+
1, 0,
51+
-1, 0,
52+
0, 1,
53+
54+
0, -1,
55+
0, 1,
56+
0, -1]);
57+
58+
// double seems to be faster than single or int's
59+
// probably because most operations are in double precision
60+
const grad3 = new Float64Array([1, 1, 0,
3861
-1, 1, 0,
3962
1, -1, 0,
4063

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

53-
const grad4 = new Float32Array([0, 1, 1, 1, 0, 1, 1, -1, 0, 1, -1, 1, 0, 1, -1, -1,
76+
// double is a bit quicker here as well
77+
const grad4 = new Float64Array([0, 1, 1, 1, 0, 1, 1, -1, 0, 1, -1, 1, 0, 1, -1, -1,
5478
0, -1, 1, 1, 0, -1, 1, -1, 0, -1, -1, 1, 0, -1, -1, -1,
5579
1, 0, 1, 1, 1, 0, 1, -1, 1, 0, -1, 1, 1, 0, -1, -1,
5680
-1, 0, 1, 1, -1, 0, 1, -1, -1, 0, -1, 1, -1, 0, -1, -1,
@@ -79,15 +103,18 @@ export type NoiseFunction2D = (x: number, y: number) => number;
79103
*/
80104
export function createNoise2D(random: RandomFn = Math.random): NoiseFunction2D {
81105
const perm = buildPermutationTable(random);
82-
const permMod12 = perm.map(v => v % 12);
106+
// precalculating this yields a little ~3% performance improvement.
107+
const permGrad2x = new Float64Array(perm).map(v => grad2[(v % 12) * 2]);
108+
const permGrad2y = new Float64Array(perm).map(v => grad2[(v % 12) * 2 + 1]);
83109
return function noise2D(x: number, y: number): number {
110+
// if(!isFinite(x) || !isFinite(y)) return 0;
84111
let n0 = 0; // Noise contributions from the three corners
85112
let n1 = 0;
86113
let n2 = 0;
87114
// Skew the input space to determine which simplex cell we're in
88115
const s = (x + y) * F2; // Hairy factor for 2D
89-
const i = Math.floor(x + s);
90-
const j = Math.floor(y + s);
116+
const i = fastFloor(x + s);
117+
const j = fastFloor(y + s);
91118
const t = (i + j) * G2;
92119
const X0 = i - t; // Unskew the cell origin back to (x,y) space
93120
const Y0 = j - t;
@@ -117,21 +144,30 @@ export function createNoise2D(random: RandomFn = Math.random): NoiseFunction2D {
117144
// Calculate the contribution from the three corners
118145
let t0 = 0.5 - x0 * x0 - y0 * y0;
119146
if (t0 >= 0) {
120-
const gi0 = permMod12[ii + perm[jj]] * 3;
147+
const gi0 = ii + perm[jj];
148+
const g0x = permGrad2x[gi0];
149+
const g0y = permGrad2y[gi0];
121150
t0 *= t0;
122-
n0 = t0 * t0 * (grad3[gi0] * x0 + grad3[gi0 + 1] * y0); // (x,y) of grad3 used for 2D gradient
151+
// n0 = t0 * t0 * (grad2[gi0] * x0 + grad2[gi0 + 1] * y0); // (x,y) of grad3 used for 2D gradient
152+
n0 = t0 * t0 * (g0x * x0 + g0y * y0);
123153
}
124154
let t1 = 0.5 - x1 * x1 - y1 * y1;
125155
if (t1 >= 0) {
126-
const gi1 = permMod12[ii + i1 + perm[jj + j1]] * 3;
156+
const gi1 = ii + i1 + perm[jj + j1];
157+
const g1x = permGrad2x[gi1];
158+
const g1y = permGrad2y[gi1];
127159
t1 *= t1;
128-
n1 = t1 * t1 * (grad3[gi1] * x1 + grad3[gi1 + 1] * y1);
160+
// n1 = t1 * t1 * (grad2[gi1] * x1 + grad2[gi1 + 1] * y1);
161+
n1 = t1 * t1 * (g1x * x1 + g1y * y1);
129162
}
130163
let t2 = 0.5 - x2 * x2 - y2 * y2;
131164
if (t2 >= 0) {
132-
const gi2 = permMod12[ii + 1 + perm[jj + 1]] * 3;
165+
const gi2 = ii + 1 + perm[jj + 1];
166+
const g2x = permGrad2x[gi2];
167+
const g2y = permGrad2y[gi2];
133168
t2 *= t2;
134-
n2 = t2 * t2 * (grad3[gi2] * x2 + grad3[gi2 + 1] * y2);
169+
// n2 = t2 * t2 * (grad2[gi2] * x2 + grad2[gi2 + 1] * y2);
170+
n2 = t2 * t2 * (g2x * x2 + g2y * y2);
135171
}
136172
// Add contributions from each corner to get the final noise value.
137173
// The result is scaled to return values in the interval [-1,1].
@@ -155,15 +191,13 @@ export type NoiseFunction3D = (x: number, y: number, z: number) => number;
155191
*/
156192
export function createNoise3D(random: RandomFn = Math.random): NoiseFunction3D {
157193
const perm = buildPermutationTable(random);
158-
const permMod12 = perm.map(v => v % 12);
159-
160194
return function noise3D(x: number, y: number, z: number): number {
161195
let n0, n1, n2, n3; // Noise contributions from the four corners
162196
// Skew the input space to determine which simplex cell we're in
163197
const s = (x + y + z) * F3; // Very nice and simple skew factor for 3D
164-
const i = Math.floor(x + s);
165-
const j = Math.floor(y + s);
166-
const k = Math.floor(z + s);
198+
const i = fastFloor(x + s);
199+
const j = fastFloor(y + s);
200+
const k = fastFloor(z + s);
167201
const t = (i + j + k) * G3;
168202
const X0 = i - t; // Unskew the cell origin back to (x,y,z) space
169203
const Y0 = j - t;
@@ -248,28 +282,28 @@ export function createNoise3D(random: RandomFn = Math.random): NoiseFunction3D {
248282
let t0 = 0.6 - x0 * x0 - y0 * y0 - z0 * z0;
249283
if (t0 < 0) n0 = 0.0;
250284
else {
251-
const gi0 = permMod12[ii + perm[jj + perm[kk]]] * 3;
285+
const gi0 = (perm[ii + perm[jj + perm[kk]]] % 12) * 3;
252286
t0 *= t0;
253287
n0 = t0 * t0 * (grad3[gi0] * x0 + grad3[gi0 + 1] * y0 + grad3[gi0 + 2] * z0);
254288
}
255289
let t1 = 0.6 - x1 * x1 - y1 * y1 - z1 * z1;
256290
if (t1 < 0) n1 = 0.0;
257291
else {
258-
const gi1 = permMod12[ii + i1 + perm[jj + j1 + perm[kk + k1]]] * 3;
292+
const gi1 = (perm[ii + i1 + perm[jj + j1 + perm[kk + k1]]] % 12) * 3;
259293
t1 *= t1;
260294
n1 = t1 * t1 * (grad3[gi1] * x1 + grad3[gi1 + 1] * y1 + grad3[gi1 + 2] * z1);
261295
}
262296
let t2 = 0.6 - x2 * x2 - y2 * y2 - z2 * z2;
263297
if (t2 < 0) n2 = 0.0;
264298
else {
265-
const gi2 = permMod12[ii + i2 + perm[jj + j2 + perm[kk + k2]]] * 3;
299+
const gi2 = (perm[ii + i2 + perm[jj + j2 + perm[kk + k2]]] % 12) * 3;
266300
t2 *= t2;
267301
n2 = t2 * t2 * (grad3[gi2] * x2 + grad3[gi2 + 1] * y2 + grad3[gi2 + 2] * z2);
268302
}
269303
let t3 = 0.6 - x3 * x3 - y3 * y3 - z3 * z3;
270304
if (t3 < 0) n3 = 0.0;
271305
else {
272-
const gi3 = permMod12[ii + 1 + perm[jj + 1 + perm[kk + 1]]] * 3;
306+
const gi3 = (perm[ii + 1 + perm[jj + 1 + perm[kk + 1]]] % 12) * 3;
273307
t3 *= t3;
274308
n3 = t3 * t3 * (grad3[gi3] * x3 + grad3[gi3 + 1] * y3 + grad3[gi3 + 2] * z3);
275309
}
@@ -300,10 +334,10 @@ export function createNoise4D(random: RandomFn = Math.random) {
300334
let n0, n1, n2, n3, n4; // Noise contributions from the five corners
301335
// Skew the (x,y,z,w) space to determine which cell of 24 simplices we're in
302336
const s = (x + y + z + w) * F4; // Factor for 4D skewing
303-
const i = Math.floor(x + s);
304-
const j = Math.floor(y + s);
305-
const k = Math.floor(z + s);
306-
const l = Math.floor(w + s);
337+
const i = fastFloor(x + s);
338+
const j = fastFloor(y + s);
339+
const k = fastFloor(z + s);
340+
const l = fastFloor(w + s);
307341
const t = (i + j + k + l) * G4; // Factor for 4D unskewing
308342
const X0 = i - t; // Unskew the cell origin back to (x,y,z,w) space
309343
const Y0 = j - t;

snapshots/noise2D.png

-3.17 KB
Binary file not shown.

snapshots/noise2Dgiga.png

781 Bytes
Loading

snapshots/noise2Dlarge.png

4.13 KB
Loading

snapshots/noise2Dsmall.png

2.7 KB
Loading

snapshots/noise3D.png

-4.13 KB
Binary file not shown.

0 commit comments

Comments
 (0)