Skip to content

Commit

Permalink
Merge pull request #69673 from sharwell/trim-builder
Browse files Browse the repository at this point in the history
Avoid static overhead for switch-over-string optimization
  • Loading branch information
sharwell authored Sep 20, 2023
2 parents 8b35705 + 98f1be6 commit 1e1f73d
Show file tree
Hide file tree
Showing 10 changed files with 170 additions and 9 deletions.
17 changes: 17 additions & 0 deletions src/Compilers/CSharp/Portable/Binder/DecisionDagBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1754,6 +1754,23 @@ private readonly struct FrozenArrayBuilder<T>
public FrozenArrayBuilder(ArrayBuilder<T> arrayBuilder)
{
Debug.Assert(arrayBuilder != null);
if (arrayBuilder.Capacity >= ArrayBuilder<T>.PooledArrayLengthLimitExclusive
&& arrayBuilder.Count < ArrayBuilder<T>.PooledArrayLengthLimitExclusive
&& arrayBuilder.Capacity >= arrayBuilder.Count * 2)
{
// An ArrayBuilder<T> meeting these conditions will satisfy the following:
//
// 1. The current backing array is too large to be returned to the pool (i.e. it will be garbage
// collected whenever no longer used).
// 2. The resized backing array from trimming is small enough to be returned to the pool (i.e. it
// will not be wasted after this builder is freed).
// 3. At least half of the storage of the current builder is unused.
//
// If we can save half the space without wasting an array that would fit in the pool, go ahead and
// do so by trimming the array builder.
arrayBuilder.Capacity = arrayBuilder.Count;
}

_arrayBuilder = arrayBuilder;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3309,7 +3309,7 @@ internal bool HasImplicitPointerConversion(TypeSymbol? source, TypeSymbol? desti
}

if (sourceSig.CallingConvention == Cci.CallingConvention.Unmanaged &&
!sourceSig.GetCallingConventionModifiers().SetEquals(destinationSig.GetCallingConventionModifiers()))
!sourceSig.GetCallingConventionModifiers().SetEqualsWithoutIntermediateHashSet(destinationSig.GetCallingConventionModifiers()))
{
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1827,7 +1827,7 @@ private static bool FunctionPointerCallingConventionsEqual(FunctionPointerMethod
return (sourceSignature.GetCallingConventionModifiers(), targetSignature.GetCallingConventionModifiers()) switch
{
(null, null) => true,
({ } sourceModifiers, { } targetModifiers) when sourceModifiers.SetEquals(targetModifiers) => true,
({ } sourceModifiers, { } targetModifiers) when sourceModifiers.SetEqualsWithoutIntermediateHashSet(targetModifiers) => true,
_ => false
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -734,7 +734,7 @@ private bool EqualsNoParameters(FunctionPointerMethodSymbol other, TypeCompareKi
if ((compareKind & TypeCompareKind.IgnoreCustomModifiersAndArraySizesAndLowerBounds) != 0)
{
if (CallingConvention.IsCallingConvention(CallingConvention.Unmanaged)
&& !GetCallingConventionModifiers().SetEquals(other.GetCallingConventionModifiers()))
&& !GetCallingConventionModifiers().SetEqualsWithoutIntermediateHashSet(other.GetCallingConventionModifiers()))
{
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ internal static partial class ValueSetFactory
/// relational operators for it; such a set can be formed only by including explicitly mentioned
/// members (or the inverse, excluding them, by complementing the set).
/// </summary>
private sealed class EnumeratedValueSet<T, TTC> : IValueSet<T> where TTC : struct, IEquatableValueTC<T> where T : notnull
private sealed class EnumeratedValueSet<T, TTC> : IValueSet<T>
where TTC : struct, IEquatableValueTC<T>
where T : notnull
{
/// <summary>
/// In <see cref="_included"/>, then members are listed by inclusion. Otherwise all members
Expand Down Expand Up @@ -150,8 +152,14 @@ public IValueSet<T> Union(IValueSet<T> o)

IValueSet IValueSet.Union(IValueSet other) => Union((IValueSet<T>)other);

public override bool Equals(object? obj) => obj is EnumeratedValueSet<T, TTC> other &&
this._included == other._included && this._membersIncludedOrExcluded.SetEquals(other._membersIncludedOrExcluded);
public override bool Equals(object? obj)
{
if (obj is not EnumeratedValueSet<T, TTC> other)
return false;

return this._included == other._included
&& this._membersIncludedOrExcluded.SetEqualsWithoutIntermediateHashSet(other._membersIncludedOrExcluded);
}

public override int GetHashCode() => Hash.Combine(this._included.GetHashCode(), this._membersIncludedOrExcluded.GetHashCode());

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;

namespace Microsoft.CodeAnalysis;

internal static class ImmutableHashSetExtensions
{
/// <summary>
/// Performs a <see cref="ImmutableHashSet{T}.SetEquals"/> comparison without allocating an intermediate
/// <see cref="HashSet{T}"/>.
/// </summary>
/// <seealso href="https://github.com/dotnet/runtime/issues/90986"/>
public static bool SetEqualsWithoutIntermediateHashSet<T>(this ImmutableHashSet<T> set, ImmutableHashSet<T> other)
{
if (set is null)
throw new ArgumentNullException(nameof(set));
if (other is null)
throw new ArgumentNullException(nameof(other));

if (ReferenceEquals(set, other))
return true;

var otherSet = other.WithComparer(set.KeyComparer);
if (set.Count != otherSet.Count)
return false;

foreach (var item in other)
{
if (!set.Contains(item))
return false;
}

return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public bool Equals(ProgrammaticSuppressionInfo? other)
}

return other != null &&
this.Suppressions.SetEquals(other.Suppressions);
this.Suppressions.SetEqualsWithoutIntermediateHashSet(other.Suppressions);
}

public override bool Equals(object? obj)
Expand Down
20 changes: 19 additions & 1 deletion src/Dependencies/PooledObjects/ArrayBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ namespace Microsoft.CodeAnalysis.PooledObjects
[DebuggerTypeProxy(typeof(ArrayBuilder<>.DebuggerProxy))]
internal sealed partial class ArrayBuilder<T> : IReadOnlyCollection<T>, IReadOnlyList<T>
{
/// <summary>
/// See <see cref="Free()"/> for an explanation of this constant value.
/// </summary>
public const int PooledArrayLengthLimitExclusive = 128;

#region DebuggerProxy

private sealed class DebuggerProxy
Expand Down Expand Up @@ -108,6 +113,19 @@ public int Count
}
}

public int Capacity
{
get
{
return _builder.Capacity;
}

set
{
_builder.Capacity = value;
}
}

public T this[int index]
{
get
Expand Down Expand Up @@ -379,7 +397,7 @@ public void Free()
// while the chance that we will need their size is diminishingly small.
// It makes sense to constrain the size to some "not too small" number.
// Overall perf does not seem to be very sensitive to this number, so I picked 128 as a limit.
if (_builder.Capacity < 128)
if (_builder.Capacity < PooledArrayLengthLimitExclusive)
{
if (this.Count != 0)
{
Expand Down
2 changes: 1 addition & 1 deletion src/Tools/IdeCoreBenchmarks/IdeCoreBenchmarks.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<OutputType>Exe</OutputType>
<TargetFrameworks>net7.0;net472</TargetFrameworks>
<IsShipping>false</IsShipping>
<LangVersion>9</LangVersion>
<LangVersion>11</LangVersion>
<ProduceReferenceAssembly>False</ProduceReferenceAssembly>
</PropertyGroup>

Expand Down
78 changes: 78 additions & 0 deletions src/Tools/IdeCoreBenchmarks/SwitchStatementBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Linq;
using System.Text;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Engines;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Test.Utilities;

namespace IdeCoreBenchmarks;

[MemoryDiagnoser]
[SimpleJob(RunStrategy.Throughput, invocationCount: 1)]
public class SwitchStatementBenchmarks
{
[Params(100, 1000, 5000, 10000)]
public int SwitchCount
{
get;
set;
}

private static string CreateSourceFile(int switchCount)
{
var builder = new StringBuilder();
builder.AppendLine(
"""
class TestClass
{
int TestMethod(string arg)
{
switch (arg)
{
""");

for (var i = 0; i < switchCount; i++)
{
builder.AppendLine(
$"""
case "Text{i}": return {i};
""");
}

builder.AppendLine(
"""
default: return 0;
}
}
}
""");

return builder.ToString();
}

[Benchmark]
public object BindFile()
{
var projectId = ProjectId.CreateNewId();
var documentId = DocumentId.CreateNewId(projectId);

using var workspace = new AdhocWorkspace();

var solution = workspace.CurrentSolution
.AddProject(projectId, "ProjectName", "AssemblyName", LanguageNames.CSharp)
.AddDocument(documentId, "DocumentName", CreateSourceFile(SwitchCount));

solution = solution.AddMetadataReference(projectId, MetadataReference.CreateFromFile(typeof(object).Assembly.Location));
solution = solution.WithProjectCompilationOptions(projectId, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

var project = solution.Projects.First();
var compilation = project.GetCompilationAsync().Result;
return compilation.EmitToStream();
}
}

0 comments on commit 1e1f73d

Please sign in to comment.