Skip to content

Reduce bloat from wgsl::Error::as_parse_error - #9944

Open
bolshoytoster wants to merge 4 commits into
gfx-rs:trunkfrom
bolshoytoster:trunk
Open

Reduce bloat from wgsl::Error::as_parse_error#9944
bolshoytoster wants to merge 4 commits into
gfx-rs:trunkfrom
bolshoytoster:trunk

Conversation

@bolshoytoster

Copy link
Copy Markdown

Connections
#9941

Description
wgsl::Error::as_parse_error causes a lot of bloat in binary size and LLVM IR. This PR makes some changes to reduce the amount of bloat.

More info in #9941

Testing
I've checked that the LLVM IR and binary outputs are lower with llvm-lines and cargo-bloat
TODO: run all the tests

Squash or Rebase?

Squash (this is a draft)

Checklist

  • I self-reviewed and fully understand this PR.
  • WebGPU implementations built with wgpu may be affected behaviorally.
  • Validation and feature gates are in place to confine behavioral changes.
  • Tests demonstrate the validation and altered logic works.
  • CHANGELOG.md entries for the user-facing effects of this change are present.
  • The PR is minimal, and doesn't make sense to land as multiple PRs.
  • Commits are logically scoped and individually reviewable.
  • The PR description has enough context to understand the motivation and solution implemented.

@bolshoytoster

Copy link
Copy Markdown
Author

The current output size is 4302 lines of IR, 18.3KiB of output. Note that this is larger that what I claimed in the linked issue, since I was testing with 29.0.3 there, and this is aimed at main.

In addition to the changes discussed in the issue, this also reorders some of the fields when constructing ParserErrors. This results in less IR, due to the fact that the compiler was inserting Cow::drops even when it knew they were Cow::Borrowed, e.g.:

ParseError {
	// This creates a Cow::Borrowed
	message: "unexpected components".into(),
	// vec! can panic if allocating fails, so it needs to generate code to drop everything in scope.
	// It generates Cow::drop for message, even though it's not needed
	labels: vec![(bad_span, "unexpected components".into())],
	notes: vec![],
}

A better version:

ParseError {
	// This can still panic, but now it doesn't have to drop message
	labels: vec![(bad_span, "unexpected components".into())],
	message: "unexpected components".into(),
	notes: vec![],
}

@bolshoytoster

bolshoytoster commented Jul 25, 2026

Copy link
Copy Markdown
Author

I have been able to get it down to 3485 lines and 16.3KiB locally by using my own implementation of SmallVec (requires unsafe).

The reason I did this was to find out the maximum size that ould be saved by SmallVec. Unfortunately, using the actual SmallVec increases the output size, seemingly because the smallvec! macro is inefficient (for the inline case, it's implemented by creating an empty one and repeatedly pushing to it).

I'm going to test to see if it's better with smallvec 2.0.0-alpha.12, and if not, I'll look into contributing there to make the macro more efficient. (I don't think naga allows unsafe code, so I can't just use my minimal implementation here).

Edit: I've got it to 3389 and 16.0KiB by using SmallVec::from_buf, which can only be used like this in 2.0.0. So it can't really be used unless wgpu decides to use an 'alpha' version of smallvec, or if smallvec finally releases v2 (unlikely to be soon, since the v2 branch hasn't been touched for 5 months)

Comment thread naga/src/front/wgsl/error.rs Outdated
Comment on lines 537 to 539
Token::Attribute => "@".to_string(),
Token::Number(_) => "number".to_string(),
Token::Word(s) => s.to_string(),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Does making expected_str a Cow so it doesn’t have many separate to_string() calls help?

In general, when trying to reduce binary size, I’ve found that it’s useful to push as many things as possible into being literals instead of repeated function calls, so I think expressing these as Cow::Borrowed("@") is worth trying (or even const { Cow::Borrowed("@") }, if necessary).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

For some reason when I tried that here it actually made the output bigger. I'll try using const {}.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Using const {} helped, but the output is still bigger than without Cow :(

@bolshoytoster

Copy link
Copy Markdown
Author

I think I've gotten it as low as I can (3775, 17.4KiB). If I'm allowed to update smallvec to 2.0.0-alpha.12 (probably not), then it could be 3206 and 14.2KiB. Unfortunately, using the current version of smallvec increases the size to 3960 21.1KiB.

The latest commit separates part of the function out and merges some of the format!s and .to_string()s. It also avoids dereferencing &self in as_parse_error, because that helps for some reason.

It also merges some duplicate &source[span]s, since the compiler wasn't merging them automatically.

Notes in case anyone wants to try to squeeze it further:
Most of the generated code is drop glue inserted due to functions that can panic:

  • 71 from format!, since it allocates a String, and allocation can fail (maybe you could replace them with write!ing into a preallocated (probably on the stack, and with enough capacity to handle pretty much any string) buffer, and truncates/aborts if it overruns)
  • 12 from &source[span]

@bolshoytoster

bolshoytoster commented Jul 27, 2026

Copy link
Copy Markdown
Author

Possibly related to #9052 and #9075: since you sometimes can't precompile shaders (if you don't know what API you might be using at compile time) would it be worth adding a feature to strip down things like this (i.e. in this case, the feature could cause as_parse_error to just return a generic error, telling you that you need to enable the feature to see errors).

@bolshoytoster
bolshoytoster marked this pull request as ready for review July 28, 2026 08:55

@kpreid kpreid left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Overall feedback: this needs more comments. Any time one writes code weirdly for the sake of a performance characteristic, one should document the reason for the weirdness and how the code should be maintained under this constraint.

Comment thread naga/src/front/wgsl/error.rs Outdated
}

impl ExpectedToken<'_> {
fn as_string(&self) -> String {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Since this constructs a string, it should be named to_string(). But relatedly, have you tried implementing Display instead? I wouldn’t be surprised if that’s worse, but it’s worth checking (and commenting).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I originally considered implementing Display, but opted not to. I named it as_string to avoid the clippy lint telling me to implement Display.

I've implemented Display now, and the output is smaller now that I can avoid calling to_string.

Comment thread naga/src/front/wgsl/error.rs Outdated
FormatChar(&'static str, char, &'static str),
FormatStr([&'a str; 3]),
}
match match self {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Does this have to be a match match? I would expect that adding a let doesn’t significantly affect the output, and will be clearer.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I've moved out one of the matched into a variable.

Comment thread naga/src/front/wgsl/error.rs Outdated
message: format!("invalid field accessor `{}`", &source[accessor_span],),
labels: vec![(accessor_span, "invalid accessor".into())],
notes: vec![],
// Other variants with a static message, single label and no notes

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This comment is inaccurate — not all of the messages are static.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Whoops, I think I meant 'Other variants with a single static label and no notes'

Comment thread naga/src/front/wgsl/error.rs Outdated
}

impl ExpectedToken<'_> {
fn as_string(&self) -> String {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This function needs a comment (not a doc-comment but an // comment) explaining why it is written so non-straightforwardly, and perhaps how to validate future changes to it.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I've added comments here and to as_parse_error, but I'm not sure how well they explain things.

message: format!("no definition in scope for identifier: `{ident}`"),
labels: vec![(ident_span, "unknown identifier".into())],
Error::BadNumber(bad_span, ref err) => ParseError {
message: format!("{}: `{}`", err, &source[*bad_span]).into(),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Personally, I would write Cow::Borrowed (or Borrowed with use Cow::Borrowed;) instead of using .into() for these conversions — it’s more explicit (doesn’t leave the reader wondering “why are we making a string and immediately converting it?") and might be faster to compile (I'm just guessing, but it doesn’t require a trait implementation lookup).

But this is likely not a big deal either way.

@bolshoytoster bolshoytoster Jul 30, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I used .into() because the original code was using it, I think having Cow::Borrowed might be a bit verbose.

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.

2 participants