Skip to content

Commit 67688cc

Browse files
anonrigaduh95
authored andcommitted
stream: hoist repeated loads in readable paths
fromList() re-read state.length and each chunk's length several times per call and re-loaded buffer[idx] around every copy, and read() loaded state.length three times in its read(0) check; the engine cannot fold these loads across the intervening copy and slice calls. Cache them in locals instead. Ported from Bun's fork of the same functions (src/js/internal/streams/readable.ts fromList/read), which carries these hoists on top of the shared readable-stream lineage. benchmark/compare.js against the unmodified baseline (60-run capture plus an independent 30-run repeat, Welch t-test): streams/readable-unevenread +0.65% (p=4.3e-4) / +0.59% (p=5.9e-3) and streams/pipe.js +0.74% (p=2.0e-4) / +0.52% (p=8.8e-3), with no significant regression across the captured streams benchmarks. Refs: https://github.com/oven-sh/bun/blob/main/src/js/internal/streams/readable.ts Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com> Assisted-by: Grok (Grok Build) PR-URL: #64312 Reviewed-By: Robert Nagy <ronagy@icloud.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 6851b8d commit 67688cc

1 file changed

Lines changed: 61 additions & 46 deletions

File tree

lib/internal/streams/readable.js

Lines changed: 61 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -673,15 +673,17 @@ Readable.prototype.read = function(n) {
673673

674674
// If we're doing read(0) to trigger a readable event, but we
675675
// already have a bunch of data in the buffer, then just trigger
676-
// the 'readable' event and move on.
676+
// the 'readable' event and move on. `state.length` cannot change
677+
// within this block, so it is loaded once instead of three times.
678+
const stateLength = state.length;
677679
if (n === 0 &&
678680
(state[kState] & kNeedReadable) !== 0 &&
679681
((state.highWaterMark !== 0 ?
680-
state.length >= state.highWaterMark :
681-
state.length > 0) ||
682+
stateLength >= state.highWaterMark :
683+
stateLength > 0) ||
682684
(state[kState] & kEnded) !== 0)) {
683685
debug('read: emitReadable');
684-
if (state.length === 0 && (state[kState] & kEnded) !== 0)
686+
if (stateLength === 0 && (state[kState] & kEnded) !== 0)
685687
endReadable(this);
686688
else
687689
emitReadable(this);
@@ -1624,8 +1626,14 @@ Readable._fromList = fromList;
16241626
// This function is designed to be inlinable, so please take care when making
16251627
// changes to the function body.
16261628
function fromList(n, state) {
1629+
// `state.length` cannot change while this function runs (only the
1630+
// caller updates it, after this returns) and the chunk lengths feeding
1631+
// the copy loops cannot change across the copy calls, so every
1632+
// repeated property load below is hoisted into a local.
1633+
const stateLength = state.length;
1634+
16271635
// nothing buffered.
1628-
if (state.length === 0)
1636+
if (stateLength === 0)
16291637
return null;
16301638

16311639
let idx = state.bufferIndex;
@@ -1637,7 +1645,7 @@ function fromList(n, state) {
16371645
if ((state[kState] & kObjectMode) !== 0) {
16381646
ret = buf[idx];
16391647
buf[idx++] = null;
1640-
} else if (!n || n >= state.length) {
1648+
} else if (!n || n >= stateLength) {
16411649
// Read it all, truncate the list.
16421650
if ((state[kState] & kDecoder) !== 0) {
16431651
ret = '';
@@ -1651,61 +1659,68 @@ function fromList(n, state) {
16511659
ret = buf[idx];
16521660
buf[idx++] = null;
16531661
} else {
1654-
ret = Buffer.allocUnsafe(state.length);
1662+
ret = Buffer.allocUnsafe(stateLength);
16551663

16561664
let i = 0;
16571665
while (idx < len) {
1658-
TypedArrayPrototypeSet(ret, buf[idx], i);
1659-
i += buf[idx].length;
1666+
const data = buf[idx];
1667+
TypedArrayPrototypeSet(ret, data, i);
1668+
i += data.length;
16601669
buf[idx++] = null;
16611670
}
16621671
}
1663-
} else if (n < buf[idx].length) {
1664-
// `slice` is the same for buffers and strings.
1665-
ret = buf[idx].slice(0, n);
1666-
buf[idx] = buf[idx].slice(n);
1667-
} else if (n === buf[idx].length) {
1668-
// First chunk is a perfect match.
1669-
ret = buf[idx];
1670-
buf[idx++] = null;
1671-
} else if ((state[kState] & kDecoder) !== 0) {
1672-
ret = '';
1673-
while (idx < len) {
1674-
const str = buf[idx];
1675-
if (n > str.length) {
1676-
ret += str;
1677-
n -= str.length;
1678-
buf[idx++] = null;
1679-
} else {
1680-
if (n === str.length) {
1672+
} else {
1673+
const first = buf[idx];
1674+
const firstLength = first.length;
1675+
if (n < firstLength) {
1676+
// `slice` is the same for buffers and strings.
1677+
ret = first.slice(0, n);
1678+
buf[idx] = first.slice(n);
1679+
} else if (n === firstLength) {
1680+
// First chunk is a perfect match.
1681+
ret = first;
1682+
buf[idx++] = null;
1683+
} else if ((state[kState] & kDecoder) !== 0) {
1684+
ret = '';
1685+
while (idx < len) {
1686+
const str = buf[idx];
1687+
const strLength = str.length;
1688+
if (n > strLength) {
16811689
ret += str;
1690+
n -= strLength;
16821691
buf[idx++] = null;
16831692
} else {
1684-
ret += str.slice(0, n);
1685-
buf[idx] = str.slice(n);
1693+
if (n === strLength) {
1694+
ret += str;
1695+
buf[idx++] = null;
1696+
} else {
1697+
ret += str.slice(0, n);
1698+
buf[idx] = str.slice(n);
1699+
}
1700+
break;
16861701
}
1687-
break;
16881702
}
1689-
}
1690-
} else {
1691-
ret = Buffer.allocUnsafe(n);
1692-
1693-
const retLen = n;
1694-
while (idx < len) {
1695-
const data = buf[idx];
1696-
if (n > data.length) {
1697-
TypedArrayPrototypeSet(ret, data, retLen - n);
1698-
n -= data.length;
1699-
buf[idx++] = null;
1700-
} else {
1701-
if (n === data.length) {
1703+
} else {
1704+
ret = Buffer.allocUnsafe(n);
1705+
1706+
const retLen = n;
1707+
while (idx < len) {
1708+
const data = buf[idx];
1709+
const dataLength = data.length;
1710+
if (n > dataLength) {
17021711
TypedArrayPrototypeSet(ret, data, retLen - n);
1712+
n -= dataLength;
17031713
buf[idx++] = null;
17041714
} else {
1705-
TypedArrayPrototypeSet(ret, new FastBuffer(data.buffer, data.byteOffset, n), retLen - n);
1706-
buf[idx] = new FastBuffer(data.buffer, data.byteOffset + n, data.length - n);
1715+
if (n === dataLength) {
1716+
TypedArrayPrototypeSet(ret, data, retLen - n);
1717+
buf[idx++] = null;
1718+
} else {
1719+
TypedArrayPrototypeSet(ret, new FastBuffer(data.buffer, data.byteOffset, n), retLen - n);
1720+
buf[idx] = new FastBuffer(data.buffer, data.byteOffset + n, dataLength - n);
1721+
}
1722+
break;
17071723
}
1708-
break;
17091724
}
17101725
}
17111726
}

0 commit comments

Comments
 (0)