Last active
December 16, 2021 20:29
-
-
Save colinbendell/42eaacf3b0c76beb0326727da2061d12 to your computer and use it in GitHub Desktop.
Compare Javascript v8 .slice.pop() vs. .slice(-1) memory and timing performance
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const { PerformanceObserver, performance, constants } = require('perf_hooks'); | |
const MAX_TEST_SIZE = Math.pow(2,16); //65536 | |
// pre-allocate an array filled with objects (primatives like Number and Boolean have internal cpp optimizations) | |
const a = new Array(MAX_TEST_SIZE).map(v => ({})); | |
// pre-allocate an array for the results. this way we don't count the heap overhead from assigning the target variables | |
const b = new Array(MAX_TEST_SIZE); | |
let totalHeapUsed = 0; | |
let heapUsed = 0; | |
let lastHeapUsed = 0; | |
let duration = 0; | |
let gcCount = 0; | |
let gcTime = 0; | |
// Create a performance observer | |
const observer = new PerformanceObserver((list) => { | |
for (const entry of list.getEntries()) { | |
if (entry.entryType === 'measure' && entry.name === 'evaltime') { | |
duration += entry.duration; | |
} | |
else if (entry.entryType === 'measure' && entry.name === 'internal-time') { | |
duration -= entry.duration; | |
// console.log(entry.duration); | |
} | |
else if (entry.entryType === 'gc') { | |
// node 16 adopted UserTiming Level3. Accessing other property methods (like entry.kind for nod 15) is considered deprecated | |
if (entry.detail?.kind !== constants.NODE_PERFORMANCE_GC_MINOR) { | |
// for debugging, trying to minimize GC_MAJOR and TIME based GC events | |
console.log(entry.detail); | |
} | |
gcCount++; | |
gcTime += entry.duration; | |
} | |
} | |
let message = `duration: ${Math.round(duration)/1000}s used-heap: ${Math.round((totalHeapUsed)/1024/1024*10)/10}MB`; | |
if (process.argv.includes('--gc')) message += ` (gc-count: ${gcCount} gc-time: ${Math.round(gcTime)/1000}s)`; | |
console.log(message); | |
performance.clearMarks(); | |
}); | |
observer.observe({ entryTypes: ['gc', 'measure'], buffered: true }); | |
// calling process.memoryUsage() isn't free. we do an internal timing to net out at the end | |
performance.mark('internal-start') | |
for (const i in [...Array(MAX_TEST_SIZE)]) { | |
process.memoryUsage(); | |
} | |
performance.mark('internal-end') | |
performance.measure('internal-time', 'internal-start', 'internal-end'); | |
performance.mark('start'); | |
// zero out our total heap used so far | |
totalHeapUsed -= (lastHeapUsed = process.memoryUsage().heapUsed); | |
for (const i in [...Array(MAX_TEST_SIZE)]) { | |
if (process.argv.includes('--baseline')) { | |
// SCENARIO 1: vanillaJS that uses a.length | |
// const b = a.length > 0 ? a[a.length - 1] : null; | |
const c = a[a.length - 1]; | |
b[i] = c; | |
} | |
else if (process.argv.includes('--pop')) { | |
// SCENARIO 2: use .slice().pop() | |
// const b = a.slice().pop(); | |
const c = a.slice().pop(); | |
b[i] = c; | |
} | |
else { | |
// SCENARIO 3: use .slice(-1) | |
// const [b] = a.slice(-1); | |
const [c] = a.slice(-1); | |
b[i] = c; | |
} | |
// simple-person's heap tracking, but we have to account for GC threads | |
heapUsed = process.memoryUsage().heapUsed; | |
if (lastHeapUsed > heapUsed) { | |
totalHeapUsed += (lastHeapUsed - heapUsed); | |
// console.log("used-heap", Math.round((lastHeapUsed - heapUsed)/1024/1024*10)/10 + "MB"); | |
} | |
lastHeapUsed = heapUsed; | |
} | |
performance.mark('end'); | |
performance.measure('evaltime', 'start', 'end'); | |
totalHeapUsed += process.memoryUsage().heapUsed; | |
//observer is called at the end of execution since there isn't any other yielding |
Use Case: const value = arraylist.slice().pop()
$ repeat 10 { node --max-heap-size=4024 --initial-heap-size=4024 --predictable-gc-schedule --scavenge-task-trigger=100 --no-scavenge-task --huge-max-old-generation-size js-get-last-array-entry.js --pop }
duration: 23.869s used-heap: 28091.2MB
duration: 23.505s used-heap: 28091.2MB
duration: 24.786s used-heap: 28091.2MB
duration: 26.613s used-heap: 28091.2MB
duration: 25.420s used-heap: 28091.2MB
duration: 25.069s used-heap: 28091.2MB
duration: 25.915s used-heap: 28091.2MB
duration: 23.455s used-heap: 28091.2MB
duration: 23.662s used-heap: 28091.2MB
duration: 23.538s used-heap: 28091.2MB
Use Case: const value = arraylist[arraylist.length - 1]
$ repeat 10 { node --max-heap-size=4024 --initial-heap-size=4024 --predictable-gc-schedule --scavenge-task-trigger=100 --no-scavenge-task --huge-max-old-generation-size js-get-last-array-entry.js --baseline }
duration: 0.004s used-heap: 2.8MB
duration: 0.008s used-heap: 2.8MB
duration: 0.005s used-heap: 2.8MB
duration: 0.003s used-heap: 2.8MB
duration: 0.003s used-heap: 2.8MB
duration: 0.003s used-heap: 2.8MB
duration: 0.006s used-heap: 2.8MB
duration: 0.003s used-heap: 2.8MB
duration: 0.003s used-heap: 2.8MB
duration: 0.006s used-heap: 2.8MB
To summarize:
.slice(-1)
- 2.1x more memory, 4.2x slower than vanilla JS version.slice().pop()
- 10,000x more memory, 5,600x slower than vanilla JS.slice().pop()
- 4,800x more memory, 1,300x slower than.slice(-1)
I tried to run this and got a TypeError: Cannot read property 'kind' of undefined
from line 27
I tried to run this and got a
TypeError: Cannot read property 'kind' of undefined
from line 27
What version of node
are you running? I've only tested with 16.1.0
That was it. I was on 15.x there. Upgraded to 16.4.1 and all is happy.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Use Case:
const [value] = arraylist.slice(-1)