Skip to content

Conversation

@faddiv
Copy link
Contributor

@faddiv faddiv commented Nov 15, 2025

Performance improvements: AttributeDataAccessor

Description

This PR showcases the performance improvements I had in mind:
The idea was to improve the AttributeDataAccessor since that is one of the most used parts of the code.
I replaced the reflection with a more direct approach. I also used caching on them to save some more work.
I also created a comparison benchmark for each step, so it can be compared to see how much benefit it can give.
My comments about the changes:

  • Some calls don't benefit from caching since it is called once or twice, others have lots of benefits. Regardless, I did it for each other for consistency.
  • I hoped for a little more benefit, but this is what I get: ~93 to ~82 ms on large compilation and nothing on small. I included the results, so you can see for yourself.
  • I have more ideas that could help, but I leave it for later.
  • Also, a few could return a read-only list directly, since it is ToList-ed. I didn't touch it for consistency.
  • There was a single static call, which I could separate from the rest. This way, the SymbolAccessor doesn't need to be passed around and checked for null. I think this makes this class cleaner.
  • Some places, not null is returned, I think the null check could be moved out, for more consistency.
  • Other places, if the processing fails, it returns null. I think it helps in stability. A half-typed-in attribute/method or whatever doesn't cause an exception this way.
  • Reverted: I reduced the number of different methods: TryGetAttributes, GetAttributes -> GetAttributes.
    • In tests, this doesn't cause any problem; however, there is a weird problem, which was there from the beginning: The GetBestTypeByMetadataName in WellKnownTypes won't find some types if it is not used in the code. It can occur when some tests are run alone. This could cause a problem, and I would look for some way to implement this differently. Or let's use TryGetAttributes instead.
    • In .NET 4.8, this causes problems, so I reverted on NotNullIfNotNull. I will look into other cases to revert to the original variant.
    • I also tried TryGetAttributes at other places, but it caused quite a lot of failures.
  • I created the NotNullIfNotNullConfiguration, since the NotNullIfNotNullAttribute is internal, but the AttributeDataAccessor is public.
  • I switched on the analyzer performance output on integration test <ReportAnalyzer>true</ReportAnalyzer> so I can observe how it is changed in practice. This reflected the gain in the benchmark.

Fixes # (issue)

Checklist

  • The existing code style is followed
  • The commit message follows our guidelines
  • Performed a self-review of my code
  • Hard-to-understand areas of my code are commented
  • The documentation is updated (as applicable)
  • Unit tests are added/updated
  • Integration tests are added/updated (as applicable, especially if feature/bug depends on roslyn or framework version in use)

Copy link
Contributor

@latonz latonz left a comment

Choose a reason for hiding this comment

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

Thank you for this contribution. I am still unsure whether it is worth adopting. I like the idea of moving away from reflection, but I am not yet fully convinced. What are your general thoughts on this?

Job=InProcess Toolchain=InProcessEmitToolchain
```
| Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated |
Copy link
Contributor

Choose a reason for hiding this comment

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

IMO we shouldn't checkin the generated results of the benchmarks...

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 checked in this only so I can show you my results. When I open this PR, I will remove this.
For now, I'm interested in whether you agree with this approach, considering the performance gains.

public class CachedAttributeDataAccessor(IAttributeDataAccessor attributeDataAccessor) : IAttributeDataAccessor
{
private readonly Dictionary<CacheKey, object?> _cache = new();
private readonly IAttributeDataAccessor _attributeDataAccessor = attributeDataAccessor;
Copy link
Contributor

Choose a reason for hiding this comment

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

This can be replaced with the primary ctor field, no?

Copy link
Contributor Author

@faddiv faddiv Nov 25, 2025

Choose a reason for hiding this comment

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

Done. BTW: Do you prefer using the primary ctor fields directly? I always disliked that it is not read-only.

Copy link
Contributor

Choose a reason for hiding this comment

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

IMO it has pros and cons. Often it saves quite a bit of boilerplate, but IMO it results in unnecessary diffs when there is a need to switch back to a regular ctor 🤷


namespace Riok.Mapperly.Configuration;

public class CachedAttributeDataAccessor(IAttributeDataAccessor attributeDataAccessor) : IAttributeDataAccessor
Copy link
Contributor

Choose a reason for hiding this comment

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

To what extent do the performance gains result from caching?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

After the AttributeDataAccessor, it is smaller. As mentioned, in some cases, it is pointless; in other cases, it provides benefits.
For me, the large compile had these results:

  • Baseline: 93.267 ms and 21185.55 KB
  • Without caching: 85.923 ms and 20239.85 KB
  • With caching: 82.853 ms and 19992.74 KB

private static TValue? GetSimpleValue<TValue>(AttributeData attrData, string propertyName)
where TValue : struct
{
if (TryGetTypedConstant(attrData, propertyName, out var typedConstant, out _))
Copy link
Contributor

Choose a reason for hiding this comment

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

Invert the if and return early.

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 used the conditional operator where it seemed to fit, and early return where it wasn't.

@latonz
Copy link
Contributor

latonz commented Dec 4, 2025

Hi @faddiv ,

Thank you for your effort on this and for providing the detailed performance benchmarks.
We have decided not to continue with this PR for now. While we appreciate the investigation into these optimizations, we are going to hold off on merging these changes at this time.

Thanks again for your contribution!

@latonz latonz closed this Dec 4, 2025
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