Skip to content

Commit b4882a8

Browse files
RoosterDragonabcdefg30
authored andcommitted
Avoid some allocations in MiniYaml.Merge.
During the merge operation, it is quite common to be dealing with a node that has no child nodes. When there are no such nodes, we can return early from some functions to avoid allocating new collections that will not be used. In the MergePartial operation, reuse a dictionary as scratch space when checking for conflicts. We introduce a IntoDictionaryWithConflictLog helper to allow this. This avoids allocating a new dictionary for the conflict log that gets thrown away at each check.
1 parent 03dd996 commit b4882a8

File tree

2 files changed

+42
-15
lines changed

2 files changed

+42
-15
lines changed

OpenRA.Game/Exts.cs

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
using System.Globalization;
1515
using System.Linq;
1616
using System.Reflection;
17+
using System.Text;
1718
using OpenRA.Primitives;
1819
using OpenRA.Support;
1920
using OpenRA.Traits;
@@ -432,15 +433,26 @@ public static Dictionary<TKey, TSource> ToDictionaryWithConflictLog<TSource, TKe
432433
public static Dictionary<TKey, TElement> ToDictionaryWithConflictLog<TSource, TKey, TElement>(
433434
this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector,
434435
string debugName, Func<TKey, string> logKey = null, Func<TElement, string> logValue = null)
436+
{
437+
var output = new Dictionary<TKey, TElement>();
438+
IntoDictionaryWithConflictLog(source, keySelector, elementSelector, debugName, output, logKey, logValue);
439+
return output;
440+
}
441+
442+
public static void IntoDictionaryWithConflictLog<TSource, TKey, TElement>(
443+
this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector,
444+
string debugName, Dictionary<TKey, TElement> output,
445+
Func<TKey, string> logKey = null, Func<TElement, string> logValue = null)
435446
{
436447
// Fall back on ToString() if null functions are provided:
437448
logKey ??= s => s.ToString();
438449
logValue ??= s => s.ToString();
439450

440451
// Try to build a dictionary and log all duplicates found (if any):
441-
var dupKeys = new Dictionary<TKey, List<string>>();
452+
Dictionary<TKey, List<string>> dupKeys = null;
442453
var capacity = source is ICollection<TSource> collection ? collection.Count : 0;
443-
var d = new Dictionary<TKey, TElement>(capacity);
454+
output.Clear();
455+
output.EnsureCapacity(capacity);
444456
foreach (var item in source)
445457
{
446458
var key = keySelector(item);
@@ -451,14 +463,15 @@ public static Dictionary<TKey, TElement> ToDictionaryWithConflictLog<TSource, TK
451463
continue;
452464

453465
// Check for a key conflict:
454-
if (!d.TryAdd(key, element))
466+
if (!output.TryAdd(key, element))
455467
{
468+
dupKeys ??= new Dictionary<TKey, List<string>>();
456469
if (!dupKeys.TryGetValue(key, out var dupKeyMessages))
457470
{
458471
// Log the initial conflicting value already inserted:
459472
dupKeyMessages = new List<string>
460473
{
461-
logValue(d[key])
474+
logValue(output[key])
462475
};
463476
dupKeys.Add(key, dupKeyMessages);
464477
}
@@ -469,15 +482,14 @@ public static Dictionary<TKey, TElement> ToDictionaryWithConflictLog<TSource, TK
469482
}
470483

471484
// If any duplicates were found, throw a descriptive error
472-
if (dupKeys.Count > 0)
485+
if (dupKeys != null)
473486
{
474-
var badKeysFormatted = string.Join(", ", dupKeys.Select(p => $"{logKey(p.Key)}: [{string.Join(",", p.Value)}]"));
475-
var msg = $"{debugName}, duplicate values found for the following keys: {badKeysFormatted}";
476-
throw new ArgumentException(msg);
487+
var badKeysFormatted = new StringBuilder(
488+
$"{debugName}, duplicate values found for the following keys: ");
489+
foreach (var p in dupKeys)
490+
badKeysFormatted.Append($"{logKey(p.Key)}: [{string.Join(",", p.Value)}]");
491+
throw new ArgumentException(badKeysFormatted.ToString());
477492
}
478-
479-
// Return the dictionary we built:
480-
return d;
481493
}
482494

483495
public static Color ColorLerp(float t, Color c1, Color c2)

OpenRA.Game/MiniYaml.cs

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ public sealed class MiniYaml
115115
const int SpacesPerLevel = 4;
116116
static readonly Func<string, string> StringIdentity = s => s;
117117
static readonly Func<MiniYaml, MiniYaml> MiniYamlIdentity = my => my;
118+
static readonly Dictionary<string, MiniYamlNode> ConflictScratch = new();
118119

119120
public readonly string Value;
120121
public readonly ImmutableArray<MiniYamlNode> Nodes;
@@ -419,7 +420,8 @@ public static List<MiniYamlNode> Merge(IEnumerable<IReadOnlyCollection<MiniYamlN
419420

420421
// Resolve any top-level removals (e.g. removing whole actor blocks)
421422
var nodes = new MiniYaml("", resolved.Select(kv => new MiniYamlNode(kv.Key, kv.Value)));
422-
return ResolveInherits(nodes, tree, ImmutableDictionary<string, MiniYamlNode.SourceLocation>.Empty);
423+
var result = ResolveInherits(nodes, tree, ImmutableDictionary<string, MiniYamlNode.SourceLocation>.Empty);
424+
return result as List<MiniYamlNode> ?? result.ToList();
423425
}
424426

425427
static void MergeIntoResolved(MiniYamlNode overrideNode, List<MiniYamlNode> existingNodes, HashSet<string> existingNodeKeys,
@@ -444,9 +446,12 @@ static void MergeIntoResolved(MiniYamlNode overrideNode, List<MiniYamlNode> exis
444446
existingNodes.Add(overrideNode.WithValue(value));
445447
}
446448

447-
static List<MiniYamlNode> ResolveInherits(
449+
static IReadOnlyCollection<MiniYamlNode> ResolveInherits(
448450
MiniYaml node, Dictionary<string, MiniYaml> tree, ImmutableDictionary<string, MiniYamlNode.SourceLocation> inherited)
449451
{
452+
if (node.Nodes.Length == 0)
453+
return node.Nodes;
454+
450455
var resolved = new List<MiniYamlNode>(node.Nodes.Length);
451456
var resolvedKeys = new HashSet<string>(node.Nodes.Length);
452457

@@ -491,6 +496,9 @@ static List<MiniYamlNode> ResolveInherits(
491496
/// </summary>
492497
static IReadOnlyCollection<MiniYamlNode> MergeSelfPartial(IReadOnlyCollection<MiniYamlNode> existingNodes)
493498
{
499+
if (existingNodes.Count == 0)
500+
return existingNodes;
501+
494502
var keys = new HashSet<string>(existingNodes.Count);
495503
var ret = new List<MiniYamlNode>(existingNodes.Count);
496504
foreach (var n in existingNodes)
@@ -511,8 +519,15 @@ static IReadOnlyCollection<MiniYamlNode> MergeSelfPartial(IReadOnlyCollection<Mi
511519

512520
static MiniYaml MergePartial(MiniYaml existingNodes, MiniYaml overrideNodes)
513521
{
514-
existingNodes?.Nodes.ToDictionaryWithConflictLog(x => x.Key, "MiniYaml.Merge", null, x => $"{x.Key} (at {x.Location})");
515-
overrideNodes?.Nodes.ToDictionaryWithConflictLog(x => x.Key, "MiniYaml.Merge", null, x => $"{x.Key} (at {x.Location})");
522+
lock (ConflictScratch)
523+
{
524+
// PERF: Reuse ConflictScratch for all conflict checks to avoid allocations.
525+
existingNodes?.Nodes.IntoDictionaryWithConflictLog(
526+
n => n.Key, n => n, "MiniYaml.Merge", ConflictScratch, k => k, n => $"{n.Key} (at {n.Location})");
527+
overrideNodes?.Nodes.IntoDictionaryWithConflictLog(
528+
n => n.Key, n => n, "MiniYaml.Merge", ConflictScratch, k => k, n => $"{n.Key} (at {n.Location})");
529+
ConflictScratch.Clear();
530+
}
516531

517532
if (existingNodes == null)
518533
return overrideNodes;

0 commit comments

Comments
 (0)