Skip to content

Free of syn#66

Merged
alice-i-cecile merged 22 commits intobevyengine:mainfrom
janhohenheim:unsynn
Apr 30, 2026
Merged

Free of syn#66
alice-i-cecile merged 22 commits intobevyengine:mainfrom
janhohenheim:unsynn

Conversation

@janhohenheim
Copy link
Copy Markdown
Member

@janhohenheim janhohenheim commented Apr 29, 2026

Objective

syn is hella slow: https://fasterthanli.me/articles/the-virtue-of-unsynn

Solution

switch to unsynn, which is what facet uses

Testing

  • cargo test runs
  • running a cargo build on my machine with a heavily customized setup reports the following timing:
    • main: 2.16 s
    • unsynn: 0.56 s

Disclaimer: I'm not a metaprogramming expert or anything, so please take a critical look at this PR before merging :)

Followup

Add to https://github.com/fasterthanlime/free-of-syn!

@github-actions
Copy link
Copy Markdown

Welcome, new contributor!

Please make sure you've read our contributing guide and we look forward to reviewing your pull request shortly ✨

Comment thread deny.toml
"MIT-0",
"Unlicense",
"Zlib",
"Unicode-3.0",
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This just allows unicode symbols to be redistributed freely. Seems alright :)

Comment thread Cargo.toml
[dependencies]
syn = "2.0"
quote = "1.0"
proc-macro2 = "1.0"
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

proc-macro2 is re-exported by unsynn

Comment thread Cargo.toml
license = "MIT OR Apache-2.0"
keywords = ["bevy", "variadics", "docs"]
rust-version = "1.81.0"
rust-version = "1.83.0"
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Looked it up, this is the minimal version that supports rust-lang/rust#128183, which unsynn uses

@alice-i-cecile alice-i-cecile requested a review from atlv24 April 30, 2026 00:00
Comment thread src/lib.rs
#![allow(missing_docs, reason = "Not all docs are written yet, see #3492.")]
#![cfg_attr(any(docsrs, docsrs_dep), feature(doc_cfg, rustdoc_internals))]
#![cfg_attr(any(docsrs, docsrs_dep), feature(doc_cfg))]
#![expect(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Surely we should move this to the actual error enum, not the crate itself.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

it triggers for something inside the unsynn! macro. I don't know how to move the expect there :/

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Comment to this effect please :)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

And issue upstream ideally.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done... I think? I'm very confused by Radicle (their git forge). Locally it's showing me the issue, but not on the webpage. Since the page is self-hosted, I assume they just didn't run a sync yet.

Can't link the issue since I don't know the hyperlink to it yet.

Comment thread src/lib.rs
@janhohenheim
Copy link
Copy Markdown
Member Author

Note: this regresses the error messages a bit
Before:
image

After:
image

I'm sure this can be fixed by emitting a custom error message with quote_spanned! + compile_error!, but I'm not sure how to do that exactly

@janhohenheim
Copy link
Copy Markdown
Member Author

Update: diagnostics should now look the same :)

Comment thread src/lib.rs
})
}

fn pretty_print_error(err: Error) -> TokenStream {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Link this in that issue please to help them out, and leave this link as a comment in the code please!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done... I think? I'm very confused by Radicle (their git forge). Locally it's showing me the issue, but not on the webpage. Since the page is self-hosted, I assume they just didn't run a sync yet.

Comment thread src/lib.rs Outdated
Co-authored-by: atlv <email@atlasdostal.com>
Copy link
Copy Markdown
Contributor

@atlv24 atlv24 left a comment

Choose a reason for hiding this comment

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

nice work Jan!!

Copy link
Copy Markdown
Member

@alice-i-cecile alice-i-cecile left a comment

Choose a reason for hiding this comment

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

Remember to write release notes!

Comment thread Cargo.toml
syn = "2.0"
quote = "1.0"
proc-macro2 = "1.0"
unsynn = "0.3"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The primary complaint here is that we're increasing supply-chain risk by doing this: single maintainer, relatively small name, new-ish crate.

That said, given the counterfactual and meaningfully improved compile time benchmarks I'm happy to take this trade!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah I'm primarily basing my own trust on Amos (fasterthanlime), since he is a big name and is heavily pushing this crate.

Copy link
Copy Markdown
Member

@alice-i-cecile alice-i-cecile left a comment

Choose a reason for hiding this comment

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

Broadly on board, but needs a bit of cleanup now :)

@janhohenheim
Copy link
Copy Markdown
Member Author

@alice-i-cecile done!

Copy link
Copy Markdown
Member

@cart cart left a comment

Choose a reason for hiding this comment

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

This is likely to actually regress Bevy compile times, as we are compiling syn for a million other reasons already, meaning this actually adds an additional crate to compile.

The only way this could be faster is if the cost of compiling a whole additional AST parsing library is offset by the unsynn macro logic being faster across all use cases. And for all I know unsynn could be slower to execute!

Lets benchmark

@janhohenheim
Copy link
Copy Markdown
Member Author

janhohenheim commented Apr 30, 2026

I use variadics_please in non-Bevy projects and don’t really plan on unsynning all of Bevy. I can check the compile time differences on Bevy later, but I personally want to look at this PR in isolation, since that is how variadics_please markets itself.

Update: I checked out Bevy.
This is using my trusty config.

syn:

hhh@hhh-fedora ~/g/bevy (main)> hyperfine 'cargo build' --prepare 'cargo clean'
Benchmark 1: cargo build
  Time (mean ± σ):     45.751 s ±  1.311 s    [User: 525.875 s, System: 81.569 s]
  Range (min … max):   44.392 s … 48.276 s    10 runs

unsynn:

hhh@hhh-fedora ~/g/bevy (main)> hyperfine 'cargo build' --prepare 'cargo clean'
Benchmark 1: cargo build
  Time (mean ± σ):     44.810 s ±  0.677 s    [User: 521.251 s, System: 80.650 s]
  Range (min … max):   44.058 s … 45.896 s    10 runs

At first glance it looks like the mean is down for unsynn, but the standard deviation is high enough that one should not take that as a giveaway.
The stastical read is "we have no evidence for the compile times being different"

On the other hand, given that there's no obvious downside, I'm happy with this result, since it matters for non-syn consumers

@cart
Copy link
Copy Markdown
Member

cart commented Apr 30, 2026

Thank you for the diligence! I would expect this PR in particular to be a regression somewhere in the range of 0 to 0.5 seconds (as all_tuples! is a relatively "low cost" macro ... not a lot of performance to claw back there) , and we know that we're compiling an additional crate that costs about ~0.5. In my mind these numbers make perfect sense: a potential slight regression hidden in the noise of 0.677 second standard deviation.

That being said, I do find the promise of unsynn and incremental improvements to the (rather sorry) state of proc macros to be compelling. I'm content to start running this experiment. It would be most interesting to port one of the "higher parsed token volume" Bevy macros that read all of the struct fields, across many structs in the Bevy codebase, to actually justify the cost of unsynn in the tree / actually prove the theory here.

That being said, given that macro_rules derives and "const-reflect driven proc macros" are both on the horizon, investing in an ecosystem-wide porting effort could end up being very wasteful, as those each have pretty significant advantages (and I expect us to want to take advantage if / when they land).

@alice-i-cecile alice-i-cecile merged commit 77b1fae into bevyengine:main Apr 30, 2026
27 checks passed
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.

4 participants