diff --git a/benchmark/util/fixed-queue.js b/benchmark/util/fixed-queue.js new file mode 100644 index 00000000000000..5c9329351edbbe --- /dev/null +++ b/benchmark/util/fixed-queue.js @@ -0,0 +1,18 @@ +'use strict'; + +const common = require('../common'); + +const bench = common.createBenchmark(main, { + n: [1e5], +}, { flags: ['--expose-internals'] }); + +function main({ n }) { + const FixedQueue = require('internal/fixed_queue'); + const queue = new FixedQueue(); + bench.start(); + for (let i = 0; i < n; i++) + queue.push(i); + for (let i = 0; i < n; i++) + queue.shift(); + bench.end(n); +} diff --git a/lib/internal/fixed_queue.js b/lib/internal/fixed_queue.js index a6e00c716c5371..09a2dddea88bfa 100644 --- a/lib/internal/fixed_queue.js +++ b/lib/internal/fixed_queue.js @@ -49,6 +49,11 @@ const kMask = kSize - 1; // | undefined | | item | // +-----------+ +-----------+ // +// Implementation detail: To reduce allocations, a single spare +// FixedCircularBuffer may be kept for reuse. When a segment empties, it +// is stored as the spare, and when the head fills, the spare is linked +// in. This does not change the active list structure or queue semantics. +// // Adding a value means moving `top` forward by one, removing means // moving `bottom` forward by one. After reaching the end, the queue // wraps around. @@ -74,16 +79,18 @@ class FixedCircularBuffer { } push(data) { - this.list[this.top] = data; - this.top = (this.top + 1) & kMask; + const top = this.top; + this.list[top] = data; + this.top = (top + 1) & kMask; } shift() { - const nextItem = this.list[this.bottom]; + const bottom = this.bottom; + const nextItem = this.list[bottom]; if (nextItem === undefined) return null; - this.list[this.bottom] = undefined; - this.bottom = (this.bottom + 1) & kMask; + this.list[bottom] = undefined; + this.bottom = (bottom + 1) & kMask; return nextItem; } } @@ -91,6 +98,7 @@ class FixedCircularBuffer { module.exports = class FixedQueue { constructor() { this.head = this.tail = new FixedCircularBuffer(); + this._spare = null; } isEmpty() { @@ -98,12 +106,19 @@ module.exports = class FixedQueue { } push(data) { - if (this.head.isFull()) { - // Head is full: Creates a new queue, sets the old queue's `.next` to it, - // and sets it as the new main queue. - this.head = this.head.next = new FixedCircularBuffer(); + let head = this.head; + if (head.isFull()) { + // Head is full: reuse the spare buffer if available. + // Otherwise create a new one. Link it and advance head. + let nextBuf = this._spare; + if (nextBuf !== null) { + this._spare = null; + } else { + nextBuf = new FixedCircularBuffer(); + } + this.head = head = head.next = nextBuf; } - this.head.push(data); + head.push(data); } shift() { @@ -112,7 +127,8 @@ module.exports = class FixedQueue { if (tail.isEmpty() && tail.next !== null) { // If there is another queue, it forms the new tail. this.tail = tail.next; - tail.next = null; + // Overwrite any existing spare + this._spare = tail; } return next; }