Skip to content

Commit 3f8cb2b

Browse files
authored
Avoid 1 element array alloc per child in open xml element.copy children (#2080)
Append(element.CloneNode(deep)) was resolving to the Append(params OpenXmlElement[]) overload, which allocates a fresh 1-element array per cloned child. Switching to AppendChild eliminates that array. Adds a CloneNode benchmark covering several element shapes; deep clones now allocate ~20% fewer bytes: Run_Deep 952 → 792 B (-17%) Paragraph_Simple_Deep 832 → 640 B (-23%) Paragraph_Formatted_Deep 2,176 → 1,728 B (-21%) Body_TenParagraphs_Deep 11,129 → 8,889 B (-20%) Body_HundredParagraphs_Deep 110,495 → 88,094 B (-20%, -8% time)
1 parent d055488 commit 3f8cb2b

2 files changed

Lines changed: 102 additions & 1 deletion

File tree

src/DocumentFormat.OpenXml.Framework/OpenXmlElement.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1936,9 +1936,13 @@ private static MarkupCompatibilityAttributes CloneMCAttributes(MarkupCompatibili
19361936
// Copy child elements from the container.
19371937
internal void CopyChildren(OpenXmlElement container, bool deep)
19381938
{
1939+
// Use AppendChild rather than Append: with a single OpenXmlElement argument the
1940+
// Append(params OpenXmlElement[]) overload allocates a 1-element array per child.
1941+
// For deep clones over a wide subtree those add up to a meaningful share of the
1942+
// CloneNode(true) allocation profile.
19391943
foreach (var element in container.ChildElements)
19401944
{
1941-
Append(element.CloneNode(deep));
1945+
AppendChild(element.CloneNode(deep));
19421946
}
19431947
}
19441948

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using BenchmarkDotNet.Attributes;
5+
using DocumentFormat.OpenXml.Wordprocessing;
6+
7+
namespace DocumentFormat.OpenXml.Benchmarks;
8+
9+
/// <summary>
10+
/// Benchmarks <see cref="OpenXmlElement.CloneNode(bool)"/> across several representative element
11+
/// shapes — leaf text, formatted run, simple paragraph, paragraph with properties, and a wider
12+
/// body. Loop-driven docx renderers (e.g. Parchment) call <c>CloneNode(true)</c> once per loop
13+
/// iteration so each iteration's allocation profile dominates the render's GC cost.
14+
/// </summary>
15+
public class CloneNodeTests
16+
{
17+
private Text _text = null!;
18+
private Run _runWithFormatting = null!;
19+
private Paragraph _simpleParagraph = null!;
20+
private Paragraph _formattedParagraph = null!;
21+
private Body _bodyOfTen = null!;
22+
private Body _bodyOfHundred = null!;
23+
24+
[GlobalSetup]
25+
public void Setup()
26+
{
27+
_text = new Text("Hello world");
28+
29+
_runWithFormatting = new Run(
30+
new RunProperties(
31+
new RunFonts { Ascii = "Calibri" },
32+
new FontSize { Val = "22" },
33+
new Bold()),
34+
new Text("Some formatted text"));
35+
36+
_simpleParagraph = new Paragraph(
37+
new Run(new Text("First run")),
38+
new Run(new Text(" second run")),
39+
new Run(new Text(" third run")));
40+
41+
_formattedParagraph = new Paragraph(
42+
new ParagraphProperties(
43+
new ParagraphStyleId { Val = "Normal" },
44+
new Justification { Val = JustificationValues.Left },
45+
new SpacingBetweenLines { Before = "0", After = "200" }),
46+
new Run(
47+
new RunProperties(new Bold()),
48+
new Text("Bold lead-in")),
49+
new Run(new Text(" body text continues")),
50+
new Run(
51+
new RunProperties(new Italic()),
52+
new Text(" italic tail")));
53+
54+
_bodyOfTen = BuildBody(10);
55+
_bodyOfHundred = BuildBody(100);
56+
}
57+
58+
[Benchmark]
59+
public OpenXmlElement Text_Shallow() => _text.CloneNode(false);
60+
61+
[Benchmark]
62+
public OpenXmlElement Text_Deep() => _text.CloneNode(true);
63+
64+
[Benchmark]
65+
public OpenXmlElement Run_Shallow() => _runWithFormatting.CloneNode(false);
66+
67+
[Benchmark]
68+
public OpenXmlElement Run_Deep() => _runWithFormatting.CloneNode(true);
69+
70+
[Benchmark]
71+
public OpenXmlElement Paragraph_Simple_Deep() => _simpleParagraph.CloneNode(true);
72+
73+
[Benchmark]
74+
public OpenXmlElement Paragraph_Formatted_Deep() => _formattedParagraph.CloneNode(true);
75+
76+
[Benchmark]
77+
public OpenXmlElement Body_TenParagraphs_Deep() => _bodyOfTen.CloneNode(true);
78+
79+
[Benchmark]
80+
public OpenXmlElement Body_HundredParagraphs_Deep() => _bodyOfHundred.CloneNode(true);
81+
82+
private static Body BuildBody(int paragraphCount)
83+
{
84+
var body = new Body();
85+
for (var i = 0; i < paragraphCount; i++)
86+
{
87+
body.AppendChild(new Paragraph(
88+
new ParagraphProperties(
89+
new ParagraphStyleId { Val = "Normal" }),
90+
new Run(
91+
new RunProperties(new RunFonts { Ascii = "Calibri" }),
92+
new Text($"Paragraph {i} text content"))));
93+
}
94+
95+
return body;
96+
}
97+
}

0 commit comments

Comments
 (0)