Skip to content

AVRO-3893: [csharp] Avoid per-call closure allocation in ObjectCreator.FindType - #3966

Open
iemejia wants to merge 1 commit into
apache:mainfrom
iemejia:AVRO-3893-objectcreator-lambda-capture
Open

AVRO-3893: [csharp] Avoid per-call closure allocation in ObjectCreator.FindType#3966
iemejia wants to merge 1 commit into
apache:mainfrom
iemejia:AVRO-3893-objectcreator-lambda-capture

Conversation

@iemejia

@iemejia iemejia commented Aug 24, 2026

Copy link
Copy Markdown
Member

What changes were proposed in this pull request?

ObjectCreator.FindType passed an inline lambda to ConcurrentDictionary.GetOrAdd:

return typeCacheByName.GetOrAdd(name, (_) =>
{
    ...
    if (TryGetIListItemTypeName(name, out var itemTypeName)) { ... }   // captures `name`
    ...
});

The lambda discards the key parameter ((_)) and instead captures the local name. Capturing a local forces the C# compiler to allocate a new display-class closure (and delegate) on every call — including cache hits, since the factory delegate is constructed as an argument regardless of whether GetOrAdd invokes it. As reported in AVRO-3893, this accounted for ~10% of allocations on a deserialization hot path using PreresolvingDatumReader.

How was this patch fixed?

  • Extract the value factory into a private FindTypeUncached(string name) method.
  • Store a single cached Func<string, Type> findTypeFactory delegate, created once in the constructor, and pass it to GetOrAdd.
  • The factory uses its name parameter (the cache key supplied by GetOrAdd) instead of a captured local, so no closure is allocated per lookup.

ObjectCreator is a shared singleton (ObjectCreator.Instance), so the factory delegate is effectively allocated once for the process. Behaviour is unchanged; the CA1031 suppression is retargeted from FindType to the extracted FindTypeUncached.

How was this patch tested?

  • dotnet build of Avro.main succeeds with 0 warnings (confirming the retargeted suppression).
  • Avro.test Specific/ObjectCreator suites pass: 96/96 across net6.0, net7.0, and net8.0.

…r.FindType

ObjectCreator.FindType passed an inline lambda to
ConcurrentDictionary.GetOrAdd that discarded the key parameter ((_)) and
captured the local `name` instead. Capturing a local forces the compiler to
allocate a new display-class closure and delegate on every call, even on
cache hits, which showed up as a significant share of allocations on the
deserialization hot path when using a PreresolvingDatumReader.

Extract the value factory into a FindTypeUncached(string) method and store a
single cached Func<string, Type> delegate (findTypeFactory), created once in
the constructor. The factory now uses its `name` parameter (the cache key
supplied by GetOrAdd) instead of a captured local, so no closure is allocated
per lookup. Behaviour is unchanged. The CA1031 suppression is retargeted to
the extracted method.
@github-actions github-actions Bot added the C# label Aug 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant