Skip to content

Improve transform speed #780

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 3, 2024
Merged

Improve transform speed #780

merged 2 commits into from
Feb 3, 2024

Conversation

ehmicky
Copy link
Collaborator

@ehmicky ehmicky commented Feb 3, 2024

This improves the speed of transforms when the transform function is synchronous, which I am assuming should be the vast majority of the cases.

The performance improvement is quite big. On my machine, using a child process with some big output (1MB), the transform runs 4x faster.

@@ -51,3 +60,45 @@ const generatorFinalChunks = async function * (final, index, generators) {
yield * transformChunk(finalChunk, generators, index + 1);
}
};

// Duplicate the code above but as synchronous functions.
// This is a performance optimization when the `transform`/`flush` function is synchronous, which is the common case.
Copy link
Collaborator Author

@ehmicky ehmicky Feb 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicating the code is unfortunate. All of the following lines are just copies of the lines above, but without async/await. Since we're iterating over possibly millions of chunks, unnecessarily waiting for millions of ticks ends up being very slow for synchronous functions.

I tried to think of a smart way to get the same benefit without duplicating the code, but could not find one. For example, I've tried the following instead:

const transformChunk = async function * (chunk, generators, index) {
	if (index === generators.length) {
		yield chunk;
		return;
	}

	const {transform, transformAsync} = generators[index];
	if (transformAsync) {
		for await (const transformedChunk of transform(chunk)) {
			yield * transformChunk(transformedChunk, generators, index + 1);
		}
	} else {
		for (const transformedChunk of transform(chunk)) {
			yield * transformChunk(transformedChunk, generators, index + 1);
		}
	}
};

But this does not give much performance benefits. This is because transformChunk still needs to be async, therefore the yield * statement iterates asynchronously, even when the for loop is synchronous.

So the only way to get the performance benefit is to make that function not async, which ends up requiring duplicating all the code.

The performance benefit is quite big, so I thought it was worth considering.

It turns out Node.js core team might actually have come to the same conclusion when implementing Readable.from(), which also involves bridging iterators and streams. They go even one step further and end up duplicating their code in 3 instances.

// There are a lot of duplication here, it's done on purpose for performance
// reasons - avoid await when not needed.

I wish JavaScript had a version of the await keyword that does not wait for one tick when the argument is not a promise, as a performance optimization (at the expense of not knowing whether a tick has passed or not).

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants