Skip to content

Use generational identifiers for tracked structs #864

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 5 commits into from
May 23, 2025

Conversation

ibraheemdev
Copy link
Contributor

@ibraheemdev ibraheemdev commented May 16, 2025

Pack a generation into input IDs.

The generation of a tracked struct is incremented after it is reused, allowing us to avoid read dependencies. Generational IDs were originally meant for #839, as adding the necessary read dependency on interned structs that may be reused introduced a large (~50%) regression to ty's incremental performance.

This increases the size of Id from a u32 to a u64. However, if the generation is restricted to u16, and ingredient indices are restricted to u16, this does not increase the size of DatabaseKeyIndex, so the memory usage effect is limited (~5% increase to ty's peak memory usage). However, this should allow us to implement garbage collection for interned values without significant performance concerns, so memory usage over time should benefit.

If the generation exceeds u16::MAX, we can fallback to adding read dependencies on tracked structs. An alternative would be to leak the slot, which would also allow us to remove the created_at field on tracked structs and may alleviate the memory usage concerns. This might be more feasible if the generation stole a few more bits from ingredient indices (as the number of ingredients is effectively static for a given salsa program).

This has a small (~4%) performance improvement on ty's benchmarks.

Copy link

netlify bot commented May 16, 2025

Deploy Preview for salsa-rs canceled.

Name Link
🔨 Latest commit 5a2c276
🔍 Latest deploy log https://app.netlify.com/projects/salsa-rs/deploys/6830801652a6d30008710190

Copy link

codspeed-hq bot commented May 16, 2025

CodSpeed Performance Report

Merging #864 will degrade performances by 15.82%

Comparing ibraheemdev:ibraheem/gen-ids (5a2c276) with master (a12bf31)

Summary

❌ 3 (👁 3) regressions
✅ 9 untouched benchmarks

Benchmarks breakdown

Benchmark BASE HEAD Change
👁 accumulator 3.9 ms 4.4 ms -12.1%
👁 new[SupertypeInput] 15.8 µs 16.4 µs -4.09%
👁 many_tracked_structs 33.9 µs 40.2 µs -15.82%

@MichaReiser
Copy link
Contributor

This has a small (~4%) performance improvement on ty's benchmarks.

This is huge! I'm leaning towards removing the created_at field and leaking memos. It's still an improvement over what we have today and we can explore the right solution once we hit that limit (which could as well be that we increase the id size further)

Copy link
Contributor

@davidbarsky davidbarsky left a comment

Choose a reason for hiding this comment

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

lgtm, merge whenever you'd like to?

@MichaReiser
Copy link
Contributor

I'd be interested to explore the memory overhead if we changed DatabaseKeyIndex to store three separate u32 (ingredient, generation, and id) as that would, IMO, eliminate the need for a report_read fallback.

@@ -152,6 +152,9 @@ fn revalidate_no_changes() {
"salsa_event(DidValidateMemoizedValue { database_key: read_value(Id(400)) })",
"salsa_event(DidReinternValue { key: query_d::interned_arguments(Id(800)), revision: R2 })",
"salsa_event(DidValidateMemoizedValue { database_key: query_d(Id(800)) })",
"salsa_event(DidValidateMemoizedValue { database_key: read_value(Id(401)) })",
Copy link
Contributor

Choose a reason for hiding this comment

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

Huh, this is interesting

Copy link
Contributor

Choose a reason for hiding this comment

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

Do you know why this test changed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure. Maybe the memos are being validated instead of the function being re-executing, if the function previously had a read dependency on a tracked struct (and that dependency no longer exists)?

@ibraheemdev
Copy link
Contributor Author

I updated the PR to store DatabaseKeyIndex as a u32 triple. This didn't seem to have a noticeable affect on memory usage, and allows us to remove the created_at field comfortably, which ends up being a net decrease compared to the previous version.

Unfortunately it looks like the benchmarks don't like this change...

@MichaReiser
Copy link
Contributor

Is there any perf difference on ty? (When running on codspeed)

@ibraheemdev
Copy link
Contributor Author

ibraheemdev commented May 20, 2025

No performance difference on ty benchmarks (astral-sh/ruff#18226).

@MichaReiser
Copy link
Contributor

I plan to review this tomorrow.

I'm okay with the regression given that it enables interned GC without a 50% incremental perf and memory regression. If you haven't done so already, maybe take an hour or two to see if you can spot the source of the regression in a local recorded profile (take the benchmark that regresses the most)

This is a substantial change where I'd like to get at least a thumbs up from r-a too, given that the performance is now regressing on micro benchmarks. Cc @Veykril

@ibraheemdev
Copy link
Contributor Author

I'm okay with the regression given that it enables interned GC without a 50% incremental perf and memory regression. If you haven't done so already, maybe take an hour or two to see if you can spot the source of the regression in a local recorded profile (take the benchmark that regresses the most)

I don't think there is a specific source, I think the regression is directly related to the size of DatabaseKeyIndex increasing (e.g. worse cache utilization, and it specifically no longer fits into a standard register). It makes sense that the microbenchmarks are regressing, I'm not sure there's much we can do about it.

let updated_id = self.update(zalsa, current_revision, id, &current_deps, fields);
if id != updated_id {
// Overwrite the previous ID if we are reusing the slot with new fields.
zalsa_local.store_tracked_struct_id(identity, updated_id);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This feels a little wrong, but I think it is correct. If the struct is recreated with the same fields, we want to return the latest generation for the tracked structs to align with the memos. If it is recreated with new fields, we have to create a new generation and invalidate the previous version.

Copy link
Contributor

@MichaReiser MichaReiser 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 awesome. I've a few smaller comments that should be addressed before landing (assuming r-a is on board with this change)

@@ -152,6 +152,9 @@ fn revalidate_no_changes() {
"salsa_event(DidValidateMemoizedValue { database_key: read_value(Id(400)) })",
"salsa_event(DidReinternValue { key: query_d::interned_arguments(Id(800)), revision: R2 })",
"salsa_event(DidValidateMemoizedValue { database_key: query_d(Id(800)) })",
"salsa_event(DidValidateMemoizedValue { database_key: read_value(Id(401)) })",
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you know why this test changed?

@Veykril
Copy link
Member

Veykril commented May 22, 2025

I would like to cut another release before merging this if possible. I expect this to have a noticable memory impact for rust-analyzer, having this be part of a separate release cycle would make that easier to check.

// the unlikely case that the ID is already at its maximum generation, we are forced to leak
// the previous slot and allocate a new value.
if id.generation() == u32::MAX {
return Err(());
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it would be good to log at least an info message that we leaked this node.

@ibraheemdev
Copy link
Contributor Author

ibraheemdev commented May 22, 2025

When rebased onto this branch, #839 is actually a ~5% improvement to ty's incremental performance.

@MichaReiser
Copy link
Contributor

@Veykril what's your timeline on the next release?

@Veykril
Copy link
Member

Veykril commented May 23, 2025

I'm fine with cutting a release now if you want to land this to try it out in ruff/ty asap

@MichaReiser
Copy link
Contributor

I'll land this once all feedback is addressed.

@MichaReiser MichaReiser enabled auto-merge May 23, 2025 14:05
@MichaReiser MichaReiser added this pull request to the merge queue May 23, 2025
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks May 23, 2025
@MichaReiser MichaReiser added this pull request to the merge queue May 23, 2025
Merged via the queue into salsa-rs:master with commit f7b0856 May 23, 2025
11 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