Cache lookups to IEnumerable<IYamlTypeConverter>#956
Cache lookups to IEnumerable<IYamlTypeConverter>#956EdwardCooke merged 3 commits intoaaubry:masterfrom
Conversation
|
Note that this PR uses the same benchmark introduced in #953 and #954. So if / when one of these PRs merge, the other should be rebased on top of latest main to remove the duplicate commit. I'm happy to do this housekeeping. I just mention it for clarity, and so that each PR can be reviewed independently, rather than forcing them to all be chained together. |
|
Alternatively, there aren't many I didn't go that route initially because:
If you have a strong preference of either path let me know and I'm happy to make updates here. |
97fbcfb to
657c784
Compare
|
I'm happier with no breaking changes, even in the constructors, but yes, this is public so it can be extended. In fact I saw a consumer of yamldotnet the other day that inherited some of these low level classes and made minor changes. Like the kubernetes client. |
| internal sealed class TypeConverterCache | ||
| { | ||
| private readonly IYamlTypeConverter[] typeConverters; | ||
| private readonly Dictionary<Type, (bool HasMatch, IYamlTypeConverter TypeConverter)> cache = new(); |
There was a problem hiding this comment.
I'm going to make a PR to fix this, but this should have been a concurrent dictionary. It's failing on my laptop because it's fast enough to catch it where the build system is slow enought that it didn't. Found it while testing your other PR for the build properties.
There was a problem hiding this comment.
You're correct, sorry that I missed this.
Several types accept an
IEnumerable<IYamlTypeConverter>, and then repeatedly traverse the list searching for a converter that accepts the type to be serialized. Traversing an IEnumerable with LINQ results in an enumerator (to walk theIEnumerable) and a Func (to filter the enumerable) allocation per visitor per object serialized. In the benchmark setup this results in 12 allocations per object serialized.To avoid the repeated lookups and allocations, I introduced an internal
TypeConverterCachethat accepts the collection of type converters and maps the type to convert to the firstIYamlTypeConverterthat accepts the type (the same as the previous behavior). This eliminates all 12 allocations and has a small CPU time win as well.To avoid a breaking change, I left the
protected IEnumerable<IYamlTypeConverter>and added aprivate TypeConverterCacheonPreProcessingPhaseObjectGraphVisitorSkeleton. If / when we want to remove cruft and can accept breaking changes we can remove this field.Note that each type that previously was doing list traversal has its own cache. If it's OK to change the constructor signature on these types, we could instead construct a single cache in the builder (e.g. in
BuilderSkeleton.BuildTypeConverters) and pass it in. However, I assume the builder is designed for public extension, so I avoided that in this change.In the provided benchmark, this results in a 15% - 25% reduction in allocations:
Before
After