[XmlSerializer] Avoid IList/Array.GetValue path for array item serialization in reflection writer - #128504
Conversation
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/bb7ee09b-36ff-43d7-9d23-45bdf352277f Co-authored-by: StephenMolloy <19562826+StephenMolloy@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR updates the reflection-based XmlSerializer writer’s array-item serialization loop to treat Array inputs explicitly rather than flowing through the IList path, reducing per-element overhead associated with IList indexing for arrays.
Changes:
- Adds an
o is Arrayfast-path inReflectionXmlSerializationWriter.WriteArrayItemsthat iterates viaArray.GetEnumerator()and forwards items toWriteElements. - Hoists
choiceSources as Arrayinto a singleArray? choiceArrayand reuses it across array/list/enumerable paths.
|
Workflow state for the Holistic Review Orchestrator. {
"version": 5,
"last_dispatched_commit": "66cbf868c49a015be2cef62009c1955cd348a92c",
"last_dispatched_base_ref": "main",
"last_dispatched_base_sha": "3a1a86525ba8649767249acefa9cad7abeaa4fe4",
"last_reviewed_commit": "66cbf868c49a015be2cef62009c1955cd348a92c",
"last_reviewed_base_ref": "main",
"last_reviewed_base_sha": "3a1a86525ba8649767249acefa9cad7abeaa4fe4",
"last_recorded_worker_run_id": "29680699499",
"review_attempt_commit": "",
"review_attempt_base_ref": "",
"review_attempt_count": 0,
"max_review_attempts": 5,
"review_history_format": "holistic-review-disclosure-v1",
"review_history": [
{
"commit": "66cbf868c49a015be2cef62009c1955cd348a92c",
"review_id": 4730564956
}
]
} |
There was a problem hiding this comment.
Holistic Review
Motivation: WriteArrayItems in the reflection-based XmlSerializer writer treated arrays through the generic IList path, where element access (arr[i]) on a boxed primitive array can devolve into slower Array.GetValue behavior. The PR (fixing #67221) aims to keep array serialization on the direct SZ-array enumerator path to reduce per-element overhead in tight serialization loops.
Approach: An explicit if (o is Array array) branch is added ahead of the IList branch, iterating via array.GetEnumerator() (the optimized single-dimension array enumerator) and forwarding each element to WriteElements. The choiceSources cast is hoisted once into a local Array? choiceArray and reused across the array, list, and enumerable branches, removing the repeated per-iteration (Array?)choiceSources cast. The list branch replaces o as IList + null-check with an o is IList list pattern.
Summary: The change is small, well-scoped, and behavior-preserving. Ordering and choice-source indexing are unchanged: the array branch increments c in lockstep with element iteration exactly as the pre-existing enumerable branch does, so choiceArray?.GetValue(c++) yields the same values as the previous ((Array?)choiceSources)?.GetValue(i). Because arrays already implement IList, they would previously have taken the list path; routing them through the array branch first is functionally equivalent for single-dimension arrays (the only kind produced here). Multidimensional arrays are not a concern in this code path, and the fallback enumerable branch and list branch remain intact. No test changes accompany the PR, which is acceptable given existing System.Private.Xml serialization coverage exercises array members and output semantics are unchanged; CI results should confirm no regressions. The only nit is a typo in the new comment ("is know" → "is known"), noted inline. Verdict: LGTM.
Note
This review was generated by this repository's Holistic Review agentic workflow to complement the built-in Copilot review.
Generated by Holistic Review · 41.4 AIC · ⌖ 9.6 AIC · ⊞ 10K
|
Doing a copilot review shows that the optimization doesnt actually work. Can you please check if this analysis holds -- feels like it based on the source pointers below? The array special-case doesn''t actually avoid The premise is that
So per element both do bounds-check + The boxing is the real cost, and it''s unavoidable with this shape: Suggest dropping the |
…tion-based serializer.
|
The latest iteration of this change seems to have dropped the check if it's an array and then use |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationWriter.cs:166
- This method still treats arrays as
IList(IList? list = o as IList;), so for non-primitive arrays the per-element access can still devolve toArray.GetValuevia theIListindexer. The PR description mentions adding an explicito is Arraybranch to avoid that path, but the current implementation only adds a primitive fast-path. Either implement the array-specific iteration branch as described (to avoidIListindexing for arrays generally), or update the PR description to match the actual scope (primitive-only optimization).
bool isListText = text != null && text.IsList && elements.Length == 0;
IList? list = o as IList;
if (list is not null && list.Count == 0)
{
return;
}
if (TryWritePrimitiveItems(elements, text, choice, o))
src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationWriter.cs:209
- In the enumerable (non-IList) path,
cis incremented when fetchingchoiceSource(GetValue(c++)) before theisListTextwhitespace check. This causes a leading space to be written for the first item whenisListTextis true (sincecis already 1). The spacing check needs to use the pre-increment index (or increment after writing).
int c = 0;
while (e.MoveNext())
{
object ai = e.Current;
object? choiceSource = choiceArray?.GetValue(c++);
if (isListText && c > 0)
{
WriteValue(" ");
}
So, I'm not sure this is really an improvement, or even feasible? The reflection-based serializer has to support both NativeAOT and NO DynamicCode scenarios. The latter means we still need a non-MakeGenericMethod path, and the former means we would have to have a version of the code that statically roots the bound generics anyway. The big "if" block is ugly, but it only runs once per collection. We could cache the result of this if block on the |
|
Azure Pipelines: Successfully started running 3 pipeline(s). 13 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
|
/ba-g helix failures unrelated |
XmlSerializer’s reflection path was serializing arrays through
IList, which can devolve toArray.GetValueper element and add avoidable overhead in tight loops. This change introduces an explicit array path inReflectionXmlSerializationWriter.WriteArrayItemsto keep array iteration on the direct array/enumerator path.Problem
WriteArrayItemstreated arrays asIList, so element access usedarr[i]and could hit slowerArray.GetValuebehavior for primary-type arrays.Change
if (o is Array array)branch inReflectionXmlSerializationWriter.WriteArrayItems.WriteElements.choiceSourcescast to a singleArray? choiceArrayand reused it across array/list/enumerable paths.Behavioral scope
Fixes #67221
Warning
Firewall rules blocked me from connecting to one or more addresses (expand for details)
I tried to connect to the following addresses, but was blocked by firewall rules:
bla/home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet exec --runtimeconfig System.Private.Xml.Tests.runtimeconfig.json --depsfile System.Private.Xml.Tests.deps.json /home/REDACTED/.nuget/packages/microsoft.dotnet.xunitconsoleREDACTED/2.9.3-beta.26257.113/build/../tools/net/xunit.console.dll System.Private.Xml.Tests.dll -xml testResults.xml -nologo -notrait category=OuterLoop -notrait category=failing(dns block)/home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet exec --runtimeconfig System.Private.Xml.Tests.runtimeconfig.json --depsfile System.Private.Xml.Tests.deps.json /home/REDACTED/.nuget/packages/microsoft.dotnet.xunitconsoleREDACTED/2.9.3-beta.26257.113/build/../tools/net/xunit.console.dll System.Private.Xml.Tests.dll -xml testResults.xml -nologo -notrait category=OuterLoop -notrait category=failing urity.Cryptograp-DURTBLDENV_FRIENDLY=Debug c.c.o.d -o CMake/usr/bin/sh _ALIGNED SSUME_ALIGNED TARGET_UNIX -DUR-D_TIME_BITS=64(dns block)/home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet exec --runtimeconfig System.Private.Xml.Tests.runtimeconfig.json --depsfile System.Private.Xml.Tests.deps.json /home/REDACTED/.nuget/packages/microsoft.dotnet.xunitconsoleREDACTED/2.9.3-beta.26257.113/build/../tools/net/xunit.console.dll System.Private.Xml.Tests.dll -xml testResults.xml -nologo -notrait category=OuterLoop -notrait category=failing k/_temp/ghcca-node/node/bin/bash -DFALLBACK_OS_Igit d/lib -DDEBUG Native_EXPORTS -DTARGET_64BIT -D/home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-U0(dns block)foo/home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet exec --runtimeconfig System.Private.Xml.Tests.runtimeconfig.json --depsfile System.Private.Xml.Tests.deps.json /home/REDACTED/.nuget/packages/microsoft.dotnet.xunitconsoleREDACTED/2.9.3-beta.26257.113/build/../tools/net/xunit.console.dll System.Private.Xml.Tests.dll -xml testResults.xml -nologo -notrait category=OuterLoop -notrait category=failing(dns block)/home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet exec --runtimeconfig System.Private.Xml.Tests.runtimeconfig.json --depsfile System.Private.Xml.Tests.deps.json /home/REDACTED/.nuget/packages/microsoft.dotnet.xunitconsoleREDACTED/2.9.3-beta.26257.113/build/../tools/net/xunit.console.dll System.Private.Xml.Tests.dll -xml testResults.xml -nologo -notrait category=OuterLoop -notrait category=failing urity.Cryptograp-DURTBLDENV_FRIENDLY=Debug c.c.o.d -o CMake/usr/bin/sh _ALIGNED SSUME_ALIGNED TARGET_UNIX -DUR-D_TIME_BITS=64(dns block)/home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet exec --runtimeconfig System.Private.Xml.Tests.runtimeconfig.json --depsfile System.Private.Xml.Tests.deps.json /home/REDACTED/.nuget/packages/microsoft.dotnet.xunitconsoleREDACTED/2.9.3-beta.26257.113/build/../tools/net/xunit.console.dll System.Private.Xml.Tests.dll -xml testResults.xml -nologo -notrait category=OuterLoop -notrait category=failing k/_temp/ghcca-node/node/bin/bash -DFALLBACK_OS_Igit d/lib -DDEBUG Native_EXPORTS -DTARGET_64BIT -D/home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-U0(dns block)notfound.invalid.corp.microsoft.com/home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet exec --runtimeconfig System.Private.Xml.Tests.runtimeconfig.json --depsfile System.Private.Xml.Tests.deps.json /home/REDACTED/.nuget/packages/microsoft.dotnet.xunitconsoleREDACTED/2.9.3-beta.26257.113/build/../tools/net/xunit.console.dll System.Private.Xml.Tests.dll -xml testResults.xml -nologo -notrait category=OuterLoop -notrait category=failing(dns block)/home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet exec --runtimeconfig System.Private.Xml.Tests.runtimeconfig.json --depsfile System.Private.Xml.Tests.deps.json /home/REDACTED/.nuget/packages/microsoft.dotnet.xunitconsoleREDACTED/2.9.3-beta.26257.113/build/../tools/net/xunit.console.dll System.Private.Xml.Tests.dll -xml testResults.xml -nologo -notrait category=OuterLoop -notrait category=failing urity.Cryptograp-DURTBLDENV_FRIENDLY=Debug c.c.o.d -o CMake/usr/bin/sh _ALIGNED SSUME_ALIGNED TARGET_UNIX -DUR-D_TIME_BITS=64(dns block)/home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet exec --runtimeconfig System.Private.Xml.Tests.runtimeconfig.json --depsfile System.Private.Xml.Tests.deps.json /home/REDACTED/.nuget/packages/microsoft.dotnet.xunitconsoleREDACTED/2.9.3-beta.26257.113/build/../tools/net/xunit.console.dll System.Private.Xml.Tests.dll -xml testResults.xml -nologo -notrait category=OuterLoop -notrait category=failing k/_temp/ghcca-node/node/bin/bash -DFALLBACK_OS_Igit d/lib -DDEBUG Native_EXPORTS -DTARGET_64BIT -D/home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-U0(dns block)test.test/home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet exec --runtimeconfig System.Private.Xml.Tests.runtimeconfig.json --depsfile System.Private.Xml.Tests.deps.json /home/REDACTED/.nuget/packages/microsoft.dotnet.xunitconsoleREDACTED/2.9.3-beta.26257.113/build/../tools/net/xunit.console.dll System.Private.Xml.Tests.dll -xml testResults.xml -nologo -notrait category=OuterLoop -notrait category=failing(dns block)/home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet exec --runtimeconfig System.Private.Xml.Tests.runtimeconfig.json --depsfile System.Private.Xml.Tests.deps.json /home/REDACTED/.nuget/packages/microsoft.dotnet.xunitconsoleREDACTED/2.9.3-beta.26257.113/build/../tools/net/xunit.console.dll System.Private.Xml.Tests.dll -xml testResults.xml -nologo -notrait category=OuterLoop -notrait category=failing urity.Cryptograp-DURTBLDENV_FRIENDLY=Debug c.c.o.d -o CMake/usr/bin/sh _ALIGNED SSUME_ALIGNED TARGET_UNIX -DUR-D_TIME_BITS=64(dns block)/home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet /home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-linux-Debug-x64/dotnet exec --runtimeconfig System.Private.Xml.Tests.runtimeconfig.json --depsfile System.Private.Xml.Tests.deps.json /home/REDACTED/.nuget/packages/microsoft.dotnet.xunitconsoleREDACTED/2.9.3-beta.26257.113/build/../tools/net/xunit.console.dll System.Private.Xml.Tests.dll -xml testResults.xml -nologo -notrait category=OuterLoop -notrait category=failing k/_temp/ghcca-node/node/bin/bash -DFALLBACK_OS_Igit d/lib -DDEBUG Native_EXPORTS -DTARGET_64BIT -D/home/REDACTED/work/runtime/runtime/artifacts/bin/testhost/net11.0-U0(dns block)If you need me to access, download, or install something from one of these locations, you can either: