Skip to content

Replace char[] array in CompletionRequiresQuotes with cached SearchValues<char> #24907

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged

Conversation

ArmaanMcleod
Copy link
Contributor

@ArmaanMcleod ArmaanMcleod commented Jan 31, 2025

PR Summary

Similar to below PRs:

#24896
#24880
#24879

I've removed the char[] array allocation in CompletionRequiresQuotes to reduce allocations every time this method is called which is referenced in 15 different places in CompletionCompleters class.

Instead I have made the default chars & escape chars of type SearchValues<char> which is cached in the class and use a method ContainsCharsToCheck. This method replaces the previous IndexOfAny calls with the char[] array and instead uses the efficient ContainsAny<T>(Span<T>, SearchValues<T>)).

I have done some benchmarks and it seems to bring a performance benefit with execution count and reduced allocation.

Benchmark

Details

Test Code

using System.Buffers;
using System.Text;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Order;
using BenchmarkDotNet.Running;

namespace BenchMark;

[MemoryDiagnoser]
[RankColumn]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByParams)]
public class SearchValuesBenchmark2
{
    public static readonly List<string> Strings = [];

    private static readonly SearchValues<char> s_defaultCharsToCheck = SearchValues.Create("$`");
    private static readonly SearchValues<char> s_escapeCharsToCheck = SearchValues.Create("$[]`");

    private static bool ContainsCharsToCheck(ReadOnlySpan<char> text, bool escape)
        => text.ContainsAny(escape ? s_escapeCharsToCheck : s_defaultCharsToCheck);

    [Params(10, 100, 1000)]
    public int StringCount;

    private class RandomGenerator
    {
        private static readonly Random random = new();
        private const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789$[]`";

        public static string GenerateRandomTextWithSpecialChars(int minLength, int maxLength)
        {
            int length = random.Next(minLength, maxLength + 1);
            var stringBuilder = new StringBuilder(length);
            for (int i = 0; i < length; i++)
            {
                stringBuilder.Append(chars[random.Next(chars.Length)]);
            }
            return stringBuilder.ToString();
        }

        public static bool GetRandomBool() => random.Next(2) == 1;
    }


    [GlobalSetup]
    public void Setup()
    {
        for (int i = 0; i < StringCount; i++)
        {
            Strings.Add(RandomGenerator.GenerateRandomTextWithSpecialChars(10, 100));
        }
    }

    [Benchmark(Baseline = true)]
    public int WithIndexofAnyAndCharArray()
    {
        int checkCount = 0;

        foreach (var str in Strings)
        {
            var escape = RandomGenerator.GetRandomBool();
            char[] charToCheck = escape ? ['$', '[', ']', '`'] : ['$', '`'];

            if (str.IndexOfAny(charToCheck) != 1)
            {
                checkCount++;
            }
        }

        return checkCount;
    }

    [Benchmark]
    public int WithSearchValuesContains()
    {
        int checkCount = 0;

        foreach (var str in Strings)
        {
            var escape = RandomGenerator.GetRandomBool();

            if (ContainsCharsToCheck(str, escape))
            {
                checkCount++;
            }
        }

        return checkCount;
    }
}

public class Program
{
    public static void Main(string[] args)
    {
        BenchmarkRunner.Run<SearchValuesBenchmark2>();
    }
}

Results

// * Detailed results *
SearchValuesBenchmark2.WithSearchValuesContains: DefaultJob [StringCount=10]
Runtime = .NET 9.0.1 (9.0.124.61010), X64 RyuJIT AVX2; GC = Concurrent Workstation
Mean = 116.001 ns, StdErr = 0.096 ns (0.08%), N = 15, StdDev = 0.374 ns
Min = 115.315 ns, Q1 = 115.717 ns, Median = 116.086 ns, Q3 = 116.240 ns, Max = 116.490 ns
IQR = 0.523 ns, LowerFence = 114.931 ns, UpperFence = 117.025 ns
ConfidenceInterval = [115.601 ns; 116.400 ns] (CI 99.9%), Margin = 0.399 ns (0.34% of Mean)
Skewness = -0.47, Kurtosis = 1.75, MValue = 2
-------------------- Histogram --------------------
[115.116 ns ; 116.689 ns) | @@@@@@@@@@@@@@@
---------------------------------------------------

SearchValuesBenchmark2.WithIndexofAnyAndCharArray: DefaultJob [StringCount=10]
Runtime = .NET 9.0.1 (9.0.124.61010), X64 RyuJIT AVX2; GC = Concurrent Workstation
Mean = 165.199 ns, StdErr = 0.465 ns (0.28%), N = 14, StdDev = 1.741 ns
Min = 161.502 ns, Q1 = 164.679 ns, Median = 165.128 ns, Q3 = 166.057 ns, Max = 168.138 ns
IQR = 1.378 ns, LowerFence = 162.612 ns, UpperFence = 168.124 ns
ConfidenceInterval = [163.235 ns; 167.163 ns] (CI 99.9%), Margin = 1.964 ns (1.19% of Mean)
Skewness = -0.37, Kurtosis = 2.59, MValue = 2
-------------------- Histogram --------------------
[160.554 ns ; 169.086 ns) | @@@@@@@@@@@@@@
---------------------------------------------------

SearchValuesBenchmark2.WithSearchValuesContains: DefaultJob [StringCount=100]
Runtime = .NET 9.0.1 (9.0.124.61010), X64 RyuJIT AVX2; GC = Concurrent Workstation
Mean = 1.401 us, StdErr = 0.002 us (0.12%), N = 15, StdDev = 0.007 us
Min = 1.390 us, Q1 = 1.397 us, Median = 1.401 us, Q3 = 1.404 us, Max = 1.412 us
IQR = 0.007 us, LowerFence = 1.386 us, UpperFence = 1.415 us
ConfidenceInterval = [1.394 us; 1.408 us] (CI 99.9%), Margin = 0.007 us (0.51% of Mean)
Skewness = -0.05, Kurtosis = 1.92, MValue = 2
-------------------- Histogram --------------------
[1.386 us ; 1.415 us) | @@@@@@@@@@@@@@@
---------------------------------------------------

SearchValuesBenchmark2.WithIndexofAnyAndCharArray: DefaultJob [StringCount=100]
Runtime = .NET 9.0.1 (9.0.124.61010), X64 RyuJIT AVX2; GC = Concurrent Workstation
Mean = 1.871 us, StdErr = 0.009 us (0.47%), N = 16, StdDev = 0.035 us
Min = 1.823 us, Q1 = 1.845 us, Median = 1.862 us, Q3 = 1.895 us, Max = 1.936 us
IQR = 0.050 us, LowerFence = 1.771 us, UpperFence = 1.969 us
ConfidenceInterval = [1.835 us; 1.907 us] (CI 99.9%), Margin = 0.036 us (1.92% of Mean)
Skewness = 0.55, Kurtosis = 1.89, MValue = 2
-------------------- Histogram --------------------
[1.804 us ; 1.871 us) | @@@@@@@@@@
[1.871 us ; 1.955 us) | @@@@@@
---------------------------------------------------

SearchValuesBenchmark2.WithSearchValuesContains: DefaultJob [StringCount=1000]
Runtime = .NET 9.0.1 (9.0.124.61010), X64 RyuJIT AVX2; GC = Concurrent Workstation
Mean = 16.237 us, StdErr = 0.022 us (0.13%), N = 15, StdDev = 0.083 us
Min = 16.087 us, Q1 = 16.187 us, Median = 16.252 us, Q3 = 16.306 us, Max = 16.352 us
IQR = 0.119 us, LowerFence = 16.009 us, UpperFence = 16.484 us
ConfidenceInterval = [16.148 us; 16.326 us] (CI 99.9%), Margin = 0.089 us (0.55% of Mean)
Skewness = -0.38, Kurtosis = 1.67, MValue = 2
-------------------- Histogram --------------------
[16.042 us ; 16.397 us) | @@@@@@@@@@@@@@@
---------------------------------------------------

SearchValuesBenchmark2.WithIndexofAnyAndCharArray: DefaultJob [StringCount=1000]
Runtime = .NET 9.0.1 (9.0.124.61010), X64 RyuJIT AVX2; GC = Concurrent Workstation
Mean = 24.508 us, StdErr = 0.074 us (0.30%), N = 15, StdDev = 0.285 us
Min = 24.142 us, Q1 = 24.260 us, Median = 24.549 us, Q3 = 24.693 us, Max = 25.046 us
IQR = 0.434 us, LowerFence = 23.609 us, UpperFence = 25.344 us
ConfidenceInterval = [24.204 us; 24.813 us] (CI 99.9%), Margin = 0.304 us (1.24% of Mean)
Skewness = 0.28, Kurtosis = 1.77, MValue = 2
-------------------- Histogram --------------------
[24.036 us ; 25.197 us) | @@@@@@@@@@@@@@@
---------------------------------------------------

// * Summary *

BenchmarkDotNet v0.14.0, Windows 11 (10.0.22631.4602/23H2/2023Update/SunValley3)
AMD Ryzen Threadripper 3960X, 1 CPU, 48 logical and 24 physical cores
.NET SDK 9.0.102
  [Host]     : .NET 9.0.1 (9.0.124.61010), X64 RyuJIT AVX2
  DefaultJob : .NET 9.0.1 (9.0.124.61010), X64 RyuJIT AVX2


| Method                     | StringCount | Mean        | Error     | StdDev    | Ratio | RatioSD | Rank | Gen0   | Allocated | Alloc Ratio |
|--------------------------- |------------ |------------:|----------:|----------:|------:|--------:|-----:|-------:|----------:|------------:|
| WithSearchValuesContains   | 10          |    116.0 ns |   0.40 ns |   0.37 ns |  0.70 |    0.01 |    1 |      - |         - |        0.00 |
| WithIndexofAnyAndCharArray | 10          |    165.2 ns |   1.96 ns |   1.74 ns |  1.00 |    0.01 |    2 | 0.0381 |     320 B |        1.00 |
|                            |             |             |           |           |       |         |      |        |           |             |
| WithSearchValuesContains   | 100         |  1,400.7 ns |   7.11 ns |   6.65 ns |  0.75 |    0.01 |    1 |      - |         - |        0.00 |
| WithIndexofAnyAndCharArray | 100         |  1,871.4 ns |  36.00 ns |  35.36 ns |  1.00 |    0.03 |    2 | 0.3815 |    3200 B |        1.00 |
|                            |             |             |           |           |       |         |      |        |           |             |
| WithSearchValuesContains   | 1000        | 16,236.8 ns |  89.07 ns |  83.32 ns |  0.66 |    0.01 |    1 |      - |         - |        0.00 |
| WithIndexofAnyAndCharArray | 1000        | 24,508.3 ns | 304.39 ns | 284.73 ns |  1.00 |    0.02 |    2 | 3.8147 |   32000 B |        1.00 |

PR Context

PR Checklist

…Values<char> with ContainsCharToCheck method
@ArmaanMcleod ArmaanMcleod changed the title Remove char[] array in CompletionRequiresQuotes and use cached SearchValues<char> with ContainsCharToCheck method Remove char[] array in CompletionRequiresQuotes and use cached SearchValues<char> with ContainsCharToCheck method Jan 31, 2025
@iSazonov
Copy link
Collaborator

/azp run

Copy link

Azure Pipelines successfully started running 4 pipeline(s).

@iSazonov

This comment was marked as outdated.

This comment was marked as outdated.

@iSazonov iSazonov added the CL-CodeCleanup Indicates that a PR should be marked as a Code Cleanup change in the Change Log label Feb 1, 2025
@iSazonov iSazonov closed this Feb 1, 2025
@iSazonov iSazonov reopened this Feb 1, 2025
@iSazonov iSazonov closed this Feb 1, 2025
@iSazonov iSazonov reopened this Feb 1, 2025
@iSazonov
Copy link
Collaborator

iSazonov commented Feb 1, 2025

/azp run

Copy link

Azure Pipelines successfully started running 3 pipeline(s).

@iSazonov iSazonov self-assigned this Feb 1, 2025
@iSazonov iSazonov changed the title Remove char[] array in CompletionRequiresQuotes and use cached SearchValues<char> with ContainsCharToCheck method Replace char[] array in CompletionRequiresQuotes with cached SearchValues<char> Feb 1, 2025
@iSazonov iSazonov merged commit ed982b4 into PowerShell:master Feb 1, 2025
60 of 62 checks passed
Copy link
Contributor

microsoft-github-policy-service bot commented Feb 1, 2025

📣 Hey @ArmaanMcleod, how did we do? We would love to hear your feedback with the link below! 🗣️

🔗 https://aka.ms/PSRepoFeedback

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CL-CodeCleanup Indicates that a PR should be marked as a Code Cleanup change in the Change Log
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy