Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Perf: ReasonPhrases as Array instead of Dictionary #56304

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
merged 8 commits into from
Jan 15, 2025

Conversation

WhatzGames
Copy link
Contributor

@WhatzGames WhatzGames commented Jun 18, 2024

ReasonPhrases as Array instead of Dictionary

  • You've read the Contributor Guide and Code of Conduct.
  • You've included unit or integration tests for your change, where applicable.
  • You've included inline docs for your change, where applicable.
  • There's an open issue for the PR that you are making. If you'd like to propose a new feature or change, please open an issue to discuss the change or find an existing issue.

Summary of the changes (Less than 80 chars)
Perf improvement ReasonPhrase

Description

Replaced the Dictionary based implementation for an Array.

Benchmarks show an overall speed-improvement:

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3737/23H2/2023Update/SunValley3)
AMD Ryzen 5 2600X, 1 CPU, 12 logical and 6 physical cores
.NET SDK 9.0.100-preview.5.24307.3
  [Host]     : .NET 9.0.0 (9.0.24.30607), X64 RyuJIT AVX2
  DefaultJob : .NET 9.0.0 (9.0.24.30607), X64 RyuJIT AVX2

UnchangedBenchmarks

Method Mean Error StdDev Ratio
Reasoning_Dict 42.563 ns 0.2700 ns 0.2255 ns 1.00
Reasoning_FrDict 24.774 ns 0.4166 ns 0.3693 ns 0.58
Reasoning_Switch 35.713 ns 0.3052 ns 0.2855 ns 0.84
Reasoning_Array 7.391 ns 0.0836 ns 0.0782 ns 0.17

PreparedBenchmarks

Method Mean Error StdDev Ratio
Reasoning_Dict 43.230 ns 0.4669 ns 0.4139 ns 1.00
Reasoning_FrDict 23.568 ns 0.3186 ns 0.2980 ns 0.55
Reasoning_Switch 36.178 ns 0.4161 ns 0.3689 ns 0.84
Reasoning_Array 7.674 ns 0.0841 ns 0.0703 ns 0.18

UnpreparedBenchmarks

Method Mean Error StdDev Ratio
Default 144.68 ns 0.828 ns 0.775 ns 1.00
FrozenDictionary 132.87 ns 1.394 ns 1.304 ns 0.92
Switch 205.88 ns 0.979 ns 0.868 ns 1.42
Reasoning_Array 89.85 ns 0.773 ns 0.723 ns 0.62

StraightBenchmarks

Method StatusCode Mean Error StdDev Median Ratio RatioSD
Reasoning_Dict 0 3.4723 ns 0.0506 ns 0.0449 ns 3.4748 ns 1.00 0.00
Reasongin_FrDict 0 1.6297 ns 0.0185 ns 0.0173 ns 1.6282 ns 0.47 0.01
Reasoning_Switch 0 1.8411 ns 0.0337 ns 0.0315 ns 1.8298 ns 0.53 0.01
Reasoning_Array 0 0.5079 ns 0.0420 ns 0.0392 ns 0.4980 ns 0.15 0.01
Reasoning_Dict 100 3.7815 ns 0.0619 ns 0.0579 ns 3.7654 ns 1.00 0.00
Reasongin_FrDict 100 1.7862 ns 0.0479 ns 0.0448 ns 1.8067 ns 0.47 0.02
Reasoning_Switch 100 0.7871 ns 0.0166 ns 0.0130 ns 0.7872 ns 0.21 0.00
Reasoning_Array 100 0.8080 ns 0.0158 ns 0.0140 ns 0.8076 ns 0.21 0.01
Reasoning_Dict 102 3.7176 ns 0.0398 ns 0.0333 ns 3.7167 ns 1.00 0.00
Reasongin_FrDict 102 1.7751 ns 0.0270 ns 0.0226 ns 1.7662 ns 0.48 0.01
Reasoning_Switch 102 0.7865 ns 0.0056 ns 0.0053 ns 0.7865 ns 0.21 0.00
Reasoning_Array 102 0.8378 ns 0.0332 ns 0.0310 ns 0.8271 ns 0.23 0.01
Reasoning_Dict 200 3.7126 ns 0.0265 ns 0.0207 ns 3.7176 ns 1.00 0.00
Reasongin_FrDict 200 1.7646 ns 0.0142 ns 0.0118 ns 1.7649 ns 0.47 0.00
Reasoning_Switch 200 1.5469 ns 0.0085 ns 0.0066 ns 1.5469 ns 0.42 0.00
Reasoning_Array 200 0.8134 ns 0.0233 ns 0.0206 ns 0.8102 ns 0.22 0.01
Reasoning_Dict 226 4.7948 ns 0.1199 ns 0.1121 ns 4.7392 ns 1.00 0.00
Reasongin_FrDict 226 1.7394 ns 0.0296 ns 0.0263 ns 1.7299 ns 0.36 0.01
Reasoning_Switch 226 1.8112 ns 0.0230 ns 0.0192 ns 1.8049 ns 0.38 0.01
Reasoning_Array 226 0.8106 ns 0.0238 ns 0.0223 ns 0.8013 ns 0.17 0.01
Reasoning_Dict 300 3.8204 ns 0.1160 ns 0.1548 ns 3.8146 ns 1.00 0.00
Reasongin_FrDict 300 2.2715 ns 0.0330 ns 0.0293 ns 2.2635 ns 0.59 0.03
Reasoning_Switch 300 0.7976 ns 0.0246 ns 0.0230 ns 0.7943 ns 0.21 0.01
Reasoning_Array 300 0.8120 ns 0.0344 ns 0.0322 ns 0.7936 ns 0.21 0.01
Reasoning_Dict 308 3.7417 ns 0.0167 ns 0.0130 ns 3.7447 ns 1.00 0.00
Reasongin_FrDict 308 1.7781 ns 0.0235 ns 0.0196 ns 1.7719 ns 0.48 0.01
Reasoning_Switch 308 0.8056 ns 0.0068 ns 0.0060 ns 0.8064 ns 0.22 0.00
Reasoning_Array 308 0.8743 ns 0.0727 ns 0.0866 ns 0.8093 ns 0.23 0.02
Reasoning_Dict 499 3.7423 ns 0.0429 ns 0.0359 ns 3.7348 ns 1.00 0.00
Reasongin_FrDict 499 1.7812 ns 0.0426 ns 0.0378 ns 1.7664 ns 0.48 0.01
Reasoning_Switch 499 2.0517 ns 0.0138 ns 0.0129 ns 2.0481 ns 0.55 0.01
Reasoning_Array 499 0.8138 ns 0.0283 ns 0.0251 ns 0.8177 ns 0.22 0.01
Reasoning_Dict 500 3.7223 ns 0.0179 ns 0.0149 ns 3.7173 ns 1.00 0.00
Reasongin_FrDict 500 1.7743 ns 0.0316 ns 0.0296 ns 1.7621 ns 0.48 0.01
Reasoning_Switch 500 1.6978 ns 0.0165 ns 0.0146 ns 1.6946 ns 0.46 0.00
Reasoning_Array 500 0.8439 ns 0.0604 ns 0.0565 ns 0.8200 ns 0.23 0.02
Reasoning_Dict 511 3.8137 ns 0.0504 ns 0.0447 ns 3.7935 ns 1.00 0.00
Reasongin_FrDict 511 1.7733 ns 0.0152 ns 0.0142 ns 1.7725 ns 0.47 0.01
Reasoning_Switch 511 2.0656 ns 0.0061 ns 0.0057 ns 2.0656 ns 0.54 0.01
Reasoning_Array 511 0.8160 ns 0.0138 ns 0.0122 ns 0.8159 ns 0.21 0.00

Edit: updated benchmarks to show results according to #56304 (comment)
Edit2: updated benchmarks to show results according to #56304 (comment)
Edit3: updated benchmarks to show results according to 344293d

@dotnet-issue-labeler dotnet-issue-labeler bot added the area-networking Includes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions label Jun 18, 2024
@dotnet-policy-service dotnet-policy-service bot added the community-contribution Indicates that the PR has been added by a community member label Jun 18, 2024
@WhatzGames
Copy link
Contributor Author

@dotnet-policy-service agree

@WhatzGames WhatzGames changed the title ReasonPhrases as switch instead of Dictionary Perf: ReasonPhrases as switch instead of Dictionary Jun 19, 2024
@gfoidl
Copy link
Member

gfoidl commented Jun 20, 2024

@WhatzGames can you please show the benchmark code?

If the benchmarks have predictable input, then the CPU's branch predictor will do a good job (i.e. it predicts which branch to take) so that can bias the results.

It would be interesting to see these improvements w/ random status codes too.
Maybe instead of the Dictionary a FrozenDictionary should be tried too.

@gfoidl
Copy link
Member

gfoidl commented Jun 21, 2024

Try a benchmark like the following one, w/ more random inputs. On my machine (x64) the FrozenDictionary is best.

Benchmark code
//#define SIMPLE_BENCH

using System.Collections.Frozen;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

BenchmarkRunner.Run<Bench>();

public class Bench
{
#if SIMPLE_BENCH
    public int StatusCode { get; set; } = 200;

    [Benchmark(Baseline = true)]
    public string Default() => ReasonPhrases_Default.GetReasonPhrase(this.StatusCode);

    [Benchmark]
    public string FrozenDictionary() => ReasonPhrases_Frozen.GetReasonPhrase(this.StatusCode);

    [Benchmark]
    public string Switch() => ReasonPhrases_Switch.GetReasonPhrase(this.StatusCode);
#else
    private readonly int[] _statusCodes = [0, 100, 102, 200, 226, 300, 308, 499, 500, 511];

    [Benchmark(Baseline = true)]
    public string Default()
    {
        Random.Shared.Shuffle(_statusCodes);

        string phrase = "";

        foreach (int statusCode in _statusCodes)
        {
            phrase = ReasonPhrases_Default.GetReasonPhrase(statusCode);
        }

        return phrase;
    }

    [Benchmark]
    public string FrozenDictionary()
    {
        Random.Shared.Shuffle(_statusCodes);

        string phrase = "";

        foreach (int statusCode in _statusCodes)
        {
            phrase = ReasonPhrases_Frozen.GetReasonPhrase(statusCode);
        }

        return phrase;
    }

    [Benchmark]
    public string Switch()
    {
        Random.Shared.Shuffle(_statusCodes);

        string phrase = "";

        foreach (int statusCode in _statusCodes)
        {
            phrase = ReasonPhrases_Switch.GetReasonPhrase(statusCode);
        }

        return phrase;
    }
#endif
}

public static class ReasonPhrases_Default
{
    private static readonly Dictionary<int, string> s_phrases = new()
    {
        { 100, "Continue" },
        { 101, "Switching Protocols" },
        { 102, "Processing" },

        { 200, "OK" },
        { 201, "Created" },
        { 202, "Accepted" },
        { 203, "Non-Authoritative Information" },
        { 204, "No Content" },
        { 205, "Reset Content" },
        { 206, "Partial Content" },
        { 207, "Multi-Status" },
        { 208, "Already Reported" },
        { 226, "IM Used" },

        { 300, "Multiple Choices" },
        { 301, "Moved Permanently" },
        { 302, "Found" },
        { 303, "See Other" },
        { 304, "Not Modified" },
        { 305, "Use Proxy" },
        { 306, "Switch Proxy" },
        { 307, "Temporary Redirect" },
        { 308, "Permanent Redirect" },

        { 400, "Bad Request" },
        { 401, "Unauthorized" },
        { 402, "Payment Required" },
        { 403, "Forbidden" },
        { 404, "Not Found" },
        { 405, "Method Not Allowed" },
        { 406, "Not Acceptable" },
        { 407, "Proxy Authentication Required" },
        { 408, "Request Timeout" },
        { 409, "Conflict" },
        { 410, "Gone" },
        { 411, "Length Required" },
        { 412, "Precondition Failed" },
        { 413, "Payload Too Large" },
        { 414, "URI Too Long" },
        { 415, "Unsupported Media Type" },
        { 416, "Range Not Satisfiable" },
        { 417, "Expectation Failed" },
        { 418, "I'm a teapot" },
        { 419, "Authentication Timeout" },
        { 421, "Misdirected Request" },
        { 422, "Unprocessable Entity" },
        { 423, "Locked" },
        { 424, "Failed Dependency" },
        { 426, "Upgrade Required" },
        { 428, "Precondition Required" },
        { 429, "Too Many Requests" },
        { 431, "Request Header Fields Too Large" },
        { 451, "Unavailable For Legal Reasons" },
        { 499, "Client Closed Request" },

        { 500, "Internal Server Error" },
        { 501, "Not Implemented" },
        { 502, "Bad Gateway" },
        { 503, "Service Unavailable" },
        { 504, "Gateway Timeout" },
        { 505, "HTTP Version Not Supported" },
        { 506, "Variant Also Negotiates" },
        { 507, "Insufficient Storage" },
        { 508, "Loop Detected" },
        { 510, "Not Extended" },
        { 511, "Network Authentication Required" },
    };

    /// <summary>
    /// Gets the reason phrase for the specified status code.
    /// </summary>
    /// <param name="statusCode">The status code.</param>
    /// <returns>The reason phrase, or <see cref="string.Empty"/> if the status code is unknown.</returns>
    public static string GetReasonPhrase(int statusCode)
    {
        return s_phrases.TryGetValue(statusCode, out string? phrase) ? phrase : string.Empty;
    }
}

public static class ReasonPhrases_Frozen
{
    private static readonly FrozenDictionary<int, string> s_phrases = new Dictionary<int, string>()
    {
        { 100, "Continue" },
        { 101, "Switching Protocols" },
        { 102, "Processing" },

        { 200, "OK" },
        { 201, "Created" },
        { 202, "Accepted" },
        { 203, "Non-Authoritative Information" },
        { 204, "No Content" },
        { 205, "Reset Content" },
        { 206, "Partial Content" },
        { 207, "Multi-Status" },
        { 208, "Already Reported" },
        { 226, "IM Used" },

        { 300, "Multiple Choices" },
        { 301, "Moved Permanently" },
        { 302, "Found" },
        { 303, "See Other" },
        { 304, "Not Modified" },
        { 305, "Use Proxy" },
        { 306, "Switch Proxy" },
        { 307, "Temporary Redirect" },
        { 308, "Permanent Redirect" },

        { 400, "Bad Request" },
        { 401, "Unauthorized" },
        { 402, "Payment Required" },
        { 403, "Forbidden" },
        { 404, "Not Found" },
        { 405, "Method Not Allowed" },
        { 406, "Not Acceptable" },
        { 407, "Proxy Authentication Required" },
        { 408, "Request Timeout" },
        { 409, "Conflict" },
        { 410, "Gone" },
        { 411, "Length Required" },
        { 412, "Precondition Failed" },
        { 413, "Payload Too Large" },
        { 414, "URI Too Long" },
        { 415, "Unsupported Media Type" },
        { 416, "Range Not Satisfiable" },
        { 417, "Expectation Failed" },
        { 418, "I'm a teapot" },
        { 419, "Authentication Timeout" },
        { 421, "Misdirected Request" },
        { 422, "Unprocessable Entity" },
        { 423, "Locked" },
        { 424, "Failed Dependency" },
        { 426, "Upgrade Required" },
        { 428, "Precondition Required" },
        { 429, "Too Many Requests" },
        { 431, "Request Header Fields Too Large" },
        { 451, "Unavailable For Legal Reasons" },
        { 499, "Client Closed Request" },

        { 500, "Internal Server Error" },
        { 501, "Not Implemented" },
        { 502, "Bad Gateway" },
        { 503, "Service Unavailable" },
        { 504, "Gateway Timeout" },
        { 505, "HTTP Version Not Supported" },
        { 506, "Variant Also Negotiates" },
        { 507, "Insufficient Storage" },
        { 508, "Loop Detected" },
        { 510, "Not Extended" },
        { 511, "Network Authentication Required" },
    }
    .ToFrozenDictionary();

    /// <summary>
    /// Gets the reason phrase for the specified status code.
    /// </summary>
    /// <param name="statusCode">The status code.</param>
    /// <returns>The reason phrase, or <see cref="string.Empty"/> if the status code is unknown.</returns>
    public static string GetReasonPhrase(int statusCode)
    {
        return s_phrases.TryGetValue(statusCode, out string? phrase) ? phrase : string.Empty;
    }
}

public static class ReasonPhrases_Switch
{
    public static string GetReasonPhrase(int statusCode) => statusCode switch
    {
        // Status Codes listed at http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
        100 => "Continue",
        101 => "Switching Protocols",
        102 => "Processing",

        200 => "OK",
        201 => "Created",
        202 => "Accepted",
        203 => "Non-Authoritative Information",
        204 => "No Content",
        205 => "Reset Content",
        206 => "Partial Content",
        207 => "Multi-Status",
        208 => "Already Reported",
        226 => "IM Used",

        300 => "Multiple Choices",
        301 => "Moved Permanently",
        302 => "Found",
        303 => "See Other",
        304 => "Not Modified",
        305 => "Use Proxy",
        306 => "Switch Proxy",
        307 => "Temporary Redirect",
        308 => "Permanent Redirect",

        400 => "Bad Request",
        401 => "Unauthorized",
        402 => "Payment Required",
        403 => "Forbidden",
        404 => "Not Found",
        405 => "Method Not Allowed",
        406 => "Not Acceptable",
        407 => "Proxy Authentication Required",
        408 => "Request Timeout",
        409 => "Conflict",
        410 => "Gone",
        411 => "Length Required",
        412 => "Precondition Failed",
        413 => "Payload Too Large",
        414 => "URI Too Long",
        415 => "Unsupported Media Type",
        416 => "Range Not Satisfiable",
        417 => "Expectation Failed",
        418 => "I'm a teapot",
        419 => "Authentication Timeout",
        421 => "Misdirected Request",
        422 => "Unprocessable Entity",
        423 => "Locked",
        424 => "Failed Dependency",
        426 => "Upgrade Required",
        428 => "Precondition Required",
        429 => "Too Many Requests",
        431 => "Request Header Fields Too Large",
        451 => "Unavailable For Legal Reasons",
        499 => "Client Closed Request",

        500 => "Internal Server Error",
        501 => "Not Implemented",
        502 => "Bad Gateway",
        503 => "Service Unavailable",
        504 => "Gateway Timeout",
        505 => "HTTP Version Not Supported",
        506 => "Variant Also Negotiates",
        507 => "Insufficient Storage",
        508 => "Loop Detected",
        510 => "Not Extended",
        511 => "Network Authentication Required",

        _ => string.Empty
    };
}

@WhatzGames
Copy link
Contributor Author

WhatzGames commented Jun 21, 2024

I concur.
After running your Benchmarks, I end up with the same result.

Though I have to say, that I'm not too sure whether running Shuffle inside of the individual Benchmarks might have had a measurable impact.
And after adding my extra Benchmarks I do ask myself in hindsight, whether PreparedBenchmarks and UnchangedBenchmarks were probably just doing the same in the end.
Nevertheless, my results came to the same conclusion.

So if the given context requires it, I'll change the PR to use a FrozenDictionary instead.

Benchmark Code
using System.Collections.Frozen;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run();

//my initial Benchmark
public class StraightBenchmarks{
    
    [Params(0, 100, 102, 200, 226, 300, 308, 499, 500, 511)]
    public int StatusCode;

    [Benchmark(Baseline = true)]
    public string Reasoning_Dict() => ReasonPhrases_Default.GetReasonPhrase(StatusCode);

    [Benchmark]
    public string Reasongin_FrDict() => ReasonPhrases_Frozen.GetReasonPhrase(StatusCode);

    [Benchmark]
    public string Reasoning_Switch() => ReasonPhrases_Switch.GetReasonPhrase(StatusCode);
}
public class UnchangedBenchmarks
{
    private readonly int[] _statusCodes = [0, 100, 102, 200, 226, 300, 308, 499, 500, 511];

     [Benchmark(Baseline = true)]
    public string Reasoning_Dict() {
        string values = string.Empty;
        foreach (var statusCode in _statusCodes)
        {
            ReasonPhrases_Default.GetReasonPhrase(statusCode);
        }
        return values;
    }

    [Benchmark]
    public string Reasoning_FrDict() {
        string values = string.Empty;
        foreach (var statusCode in _statusCodes)
        {
            ReasonPhrases_Frozen.GetReasonPhrase(statusCode);
        }
        return values;
    }

    [Benchmark]
    public string Reasoning_Switch() {
        string values = string.Empty;
        foreach (var statusCode in _statusCodes)
        {
            ReasonPhrases_Switch.GetReasonPhrase(statusCode);
        }
        return values;
    }
}
public class PreparedBenchmarks{

    private readonly int[] _statusCodes = [0, 100, 102, 200, 226, 300, 308, 499, 500, 511];

    [GlobalSetup]
    public void Setup() => Random.Shared.Shuffle(_statusCodes);

     [Benchmark(Baseline = true)]
    public string Reasoning_Dict() {
        string values = string.Empty;
        foreach (var statusCode in _statusCodes)
        {
            ReasonPhrases_Default.GetReasonPhrase(statusCode);
        }
        return values;
    }

    [Benchmark]
    public string Reasoning_FrDict() {
        string values = string.Empty;
        foreach (var statusCode in _statusCodes)
        {
            ReasonPhrases_Frozen.GetReasonPhrase(statusCode);
        }
        return values;
    }

    [Benchmark]
    public string Reasoning_Switch() {
        string values = string.Empty;
        foreach (var statusCode in _statusCodes)
        {
            ReasonPhrases_Switch.GetReasonPhrase(statusCode);
        }
        return values;
    }
}

public class UnpreparedBenchmarks{
    private readonly int[] _statusCodes = [0, 100, 102, 200, 226, 300, 308, 499, 500, 511];

    [Benchmark(Baseline = true)]
    public string Default()
    {
        Random.Shared.Shuffle(_statusCodes);

        string phrase = "";

        foreach (int statusCode in _statusCodes)
        {
            phrase = ReasonPhrases_Default.GetReasonPhrase(statusCode);
        }

        return phrase;
    }

    [Benchmark]
    public string FrozenDictionary()
    {
        Random.Shared.Shuffle(_statusCodes);

        string phrase = "";

        foreach (int statusCode in _statusCodes)
        {
            phrase = ReasonPhrases_Frozen.GetReasonPhrase(statusCode);
        }

        return phrase;
    }

    [Benchmark]
    public string Switch()
    {
        Random.Shared.Shuffle(_statusCodes);

        string phrase = "";

        foreach (int statusCode in _statusCodes)
        {
            phrase = ReasonPhrases_Switch.GetReasonPhrase(statusCode);
        }

        return phrase;
    }
}

public static class ReasonPhrases_Default
{
    private static readonly Dictionary<int, string> s_phrases = new()
    {
        { 100, "Continue" },
        { 101, "Switching Protocols" },
        { 102, "Processing" },

        { 200, "OK" },
        { 201, "Created" },
        { 202, "Accepted" },
        { 203, "Non-Authoritative Information" },
        { 204, "No Content" },
        { 205, "Reset Content" },
        { 206, "Partial Content" },
        { 207, "Multi-Status" },
        { 208, "Already Reported" },
        { 226, "IM Used" },

        { 300, "Multiple Choices" },
        { 301, "Moved Permanently" },
        { 302, "Found" },
        { 303, "See Other" },
        { 304, "Not Modified" },
        { 305, "Use Proxy" },
        { 306, "Switch Proxy" },
        { 307, "Temporary Redirect" },
        { 308, "Permanent Redirect" },

        { 400, "Bad Request" },
        { 401, "Unauthorized" },
        { 402, "Payment Required" },
        { 403, "Forbidden" },
        { 404, "Not Found" },
        { 405, "Method Not Allowed" },
        { 406, "Not Acceptable" },
        { 407, "Proxy Authentication Required" },
        { 408, "Request Timeout" },
        { 409, "Conflict" },
        { 410, "Gone" },
        { 411, "Length Required" },
        { 412, "Precondition Failed" },
        { 413, "Payload Too Large" },
        { 414, "URI Too Long" },
        { 415, "Unsupported Media Type" },
        { 416, "Range Not Satisfiable" },
        { 417, "Expectation Failed" },
        { 418, "I'm a teapot" },
        { 419, "Authentication Timeout" },
        { 421, "Misdirected Request" },
        { 422, "Unprocessable Entity" },
        { 423, "Locked" },
        { 424, "Failed Dependency" },
        { 426, "Upgrade Required" },
        { 428, "Precondition Required" },
        { 429, "Too Many Requests" },
        { 431, "Request Header Fields Too Large" },
        { 451, "Unavailable For Legal Reasons" },
        { 499, "Client Closed Request" },

        { 500, "Internal Server Error" },
        { 501, "Not Implemented" },
        { 502, "Bad Gateway" },
        { 503, "Service Unavailable" },
        { 504, "Gateway Timeout" },
        { 505, "HTTP Version Not Supported" },
        { 506, "Variant Also Negotiates" },
        { 507, "Insufficient Storage" },
        { 508, "Loop Detected" },
        { 510, "Not Extended" },
        { 511, "Network Authentication Required" },
    };

    /// <summary>
    /// Gets the reason phrase for the specified status code.
    /// </summary>
    /// <param name="statusCode">The status code.</param>
    /// <returns>The reason phrase, or <see cref="string.Empty"/> if the status code is unknown.</returns>
    public static string GetReasonPhrase(int statusCode)
    {
        return s_phrases.TryGetValue(statusCode, out string? phrase) ? phrase : string.Empty;
    }
}

public static class ReasonPhrases_Frozen
{
    private static readonly FrozenDictionary<int, string> s_phrases = new Dictionary<int, string>()
    {
        { 100, "Continue" },
        { 101, "Switching Protocols" },
        { 102, "Processing" },

        { 200, "OK" },
        { 201, "Created" },
        { 202, "Accepted" },
        { 203, "Non-Authoritative Information" },
        { 204, "No Content" },
        { 205, "Reset Content" },
        { 206, "Partial Content" },
        { 207, "Multi-Status" },
        { 208, "Already Reported" },
        { 226, "IM Used" },

        { 300, "Multiple Choices" },
        { 301, "Moved Permanently" },
        { 302, "Found" },
        { 303, "See Other" },
        { 304, "Not Modified" },
        { 305, "Use Proxy" },
        { 306, "Switch Proxy" },
        { 307, "Temporary Redirect" },
        { 308, "Permanent Redirect" },

        { 400, "Bad Request" },
        { 401, "Unauthorized" },
        { 402, "Payment Required" },
        { 403, "Forbidden" },
        { 404, "Not Found" },
        { 405, "Method Not Allowed" },
        { 406, "Not Acceptable" },
        { 407, "Proxy Authentication Required" },
        { 408, "Request Timeout" },
        { 409, "Conflict" },
        { 410, "Gone" },
        { 411, "Length Required" },
        { 412, "Precondition Failed" },
        { 413, "Payload Too Large" },
        { 414, "URI Too Long" },
        { 415, "Unsupported Media Type" },
        { 416, "Range Not Satisfiable" },
        { 417, "Expectation Failed" },
        { 418, "I'm a teapot" },
        { 419, "Authentication Timeout" },
        { 421, "Misdirected Request" },
        { 422, "Unprocessable Entity" },
        { 423, "Locked" },
        { 424, "Failed Dependency" },
        { 426, "Upgrade Required" },
        { 428, "Precondition Required" },
        { 429, "Too Many Requests" },
        { 431, "Request Header Fields Too Large" },
        { 451, "Unavailable For Legal Reasons" },
        { 499, "Client Closed Request" },

        { 500, "Internal Server Error" },
        { 501, "Not Implemented" },
        { 502, "Bad Gateway" },
        { 503, "Service Unavailable" },
        { 504, "Gateway Timeout" },
        { 505, "HTTP Version Not Supported" },
        { 506, "Variant Also Negotiates" },
        { 507, "Insufficient Storage" },
        { 508, "Loop Detected" },
        { 510, "Not Extended" },
        { 511, "Network Authentication Required" },
    }
    .ToFrozenDictionary();

    /// <summary>
    /// Gets the reason phrase for the specified status code.
    /// </summary>
    /// <param name="statusCode">The status code.</param>
    /// <returns>The reason phrase, or <see cref="string.Empty"/> if the status code is unknown.</returns>
    public static string GetReasonPhrase(int statusCode)
    {
        return s_phrases.TryGetValue(statusCode, out string? phrase) ? phrase : string.Empty;
    }
}

public static class ReasonPhrases_Switch
{
    public static string GetReasonPhrase(int statusCode) => statusCode switch
    {
        // Status Codes listed at http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
        100 => "Continue",
        101 => "Switching Protocols",
        102 => "Processing",

        200 => "OK",
        201 => "Created",
        202 => "Accepted",
        203 => "Non-Authoritative Information",
        204 => "No Content",
        205 => "Reset Content",
        206 => "Partial Content",
        207 => "Multi-Status",
        208 => "Already Reported",
        226 => "IM Used",

        300 => "Multiple Choices",
        301 => "Moved Permanently",
        302 => "Found",
        303 => "See Other",
        304 => "Not Modified",
        305 => "Use Proxy",
        306 => "Switch Proxy",
        307 => "Temporary Redirect",
        308 => "Permanent Redirect",

        400 => "Bad Request",
        401 => "Unauthorized",
        402 => "Payment Required",
        403 => "Forbidden",
        404 => "Not Found",
        405 => "Method Not Allowed",
        406 => "Not Acceptable",
        407 => "Proxy Authentication Required",
        408 => "Request Timeout",
        409 => "Conflict",
        410 => "Gone",
        411 => "Length Required",
        412 => "Precondition Failed",
        413 => "Payload Too Large",
        414 => "URI Too Long",
        415 => "Unsupported Media Type",
        416 => "Range Not Satisfiable",
        417 => "Expectation Failed",
        418 => "I'm a teapot",
        419 => "Authentication Timeout",
        421 => "Misdirected Request",
        422 => "Unprocessable Entity",
        423 => "Locked",
        424 => "Failed Dependency",
        426 => "Upgrade Required",
        428 => "Precondition Required",
        429 => "Too Many Requests",
        431 => "Request Header Fields Too Large",
        451 => "Unavailable For Legal Reasons",
        499 => "Client Closed Request",

        500 => "Internal Server Error",
        501 => "Not Implemented",
        502 => "Bad Gateway",
        503 => "Service Unavailable",
        504 => "Gateway Timeout",
        505 => "HTTP Version Not Supported",
        506 => "Variant Also Negotiates",
        507 => "Insufficient Storage",
        508 => "Loop Detected",
        510 => "Not Extended",
        511 => "Network Authentication Required",

        _ => string.Empty
    };
}

@gfoidl
Copy link
Member

gfoidl commented Jun 24, 2024

change the PR to use a FrozenDictionary instead.

Please do so, and thanks for checking the bench-results too.

@WhatzGames WhatzGames changed the title Perf: ReasonPhrases as switch instead of Dictionary Perf: ReasonPhrases as FrozenDictionary instead of Dictionary Jun 24, 2024
@captainsafia
Copy link
Member

@gfoidl Thanks for providing guidance here!

@WhatzGames Can you update the benchmarks in the PR description to reflect the latest numbers you are seeing with FrozenDictionary?

@WhatzGames
Copy link
Contributor Author

Done.
Also updated some of the description to reflect this PRs changed approach.

@WhatzGames
Copy link
Contributor Author

WhatzGames commented Jun 25, 2024

mmmh... I just found a different approach and created a Benchmark for that one.
In comparison to the FrozenDictionary it reduces the time even further.

Method Mean Error StdDev Ratio Allocated Alloc Ratio
ReasonPhrases_Array 12.62 ns 0.181 ns 0.161 ns 0.49 - NA
ReasonPhrases_FrozenDict 26.00 ns 0.336 ns 0.315 ns 1.00 - NA

results after adding a Shuffle like @gfoidl used:

Method Mean Error StdDev Ratio
ReasonPhrases_Array 133.0 ns 1.80 ns 1.60 ns 0.91
ReasonPhrases_FrozenDict 146.4 ns 0.48 ns 0.45 ns 1.00
Benchmarks
using System.Collections.Frozen;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;


BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run();
[MemoryDiagnoser]
public class Benchmark
{
    public int[] statusCode = [0, 100, 102, 200, 226, 300, 308, 499, 500, 511];

    [Benchmark]
    public string? ReasonPhrases_Array()
    {
        string? value = string.Empty;
        foreach (var item in statusCode)
        {
            value = Reasons_Array.Get(item);
        }
        return value;
    }

    [Benchmark(Baseline = true)]
    public string? ReasonPhrases_FrozenDict()
    {
        string? value = string.Empty;
        foreach (var item in statusCode)
        {
            value = Reasons_Frozen.Get(item);
        }
        return value;
    }

}

public class ShuffledBenchmark
{
    public int[] statusCode = [0, 100, 102, 200, 226, 300, 308, 499, 500, 511];

    [Benchmark]
    public string? ReasonPhrases_Array()
    {
        Random.Shared.Shuffle(statusCode);
        string? value = string.Empty;
        foreach (var item in statusCode)
        {
            value = Reasons_Array.Get(item);
        }
        return value;
    }

    [Benchmark(Baseline = true)]
    public string? ReasonPhrases_FrozenDict()
    {
        Random.Shared.Shuffle(statusCode);
        string? value = string.Empty;
        foreach (var item in statusCode)
        {
            value = Reasons_Frozen.Get(item);
        }
        return value;
    }

}


static class Reasons_Frozen
{
    private static readonly FrozenDictionary<int, string?> Reasons = new Dictionary<int, string?>{
        {100, "Continue"},
        {101, "Switching Protocols"},
        {200,"OK"},
        {201,"Created"},
        {202,"Accepted"},
        {203,"Non-Authoritative Information"},
        {204,"No Content"},
        {205,"Reset Content"},
        {206,"Partial Content"},
        {207,"Multi-Status"},
        {300,"Multiple Choices"},
        {301,"Moved Permanently"},
        {302,"Found"},
        {303,"See Other"},
        {304,"Not Modified"},
        {305,"Use Proxy"},
        {306, null},
        {307,"Temporary Redirect"},
        {400, "Bad Request"},
        {401, "Unauthorized"},
        {402, "Payment Required"},
        {403, "Forbidden"},
        {404, "Not Found"},
        {405, "Method Not Allowed"},
        {406, "Not Acceptable"},
        {407, "Proxy Authentication Required"},
        {408, "Request Timeout"},
        {409, "Conflict"},
        {410, "Gone"},
        {411, "Length Required"},
        {412, "Precondition Failed"},
        {413, "Request Entity Too Large"},
        {414, "Request-Uri Too Long"},
        {415, "Unsupported Media Type"},
        {416, "Requested Range Not Satisfiable"},
        {417, "Expectation Failed"},
        {418, null},
        {419, null},
        {420, null},
        {421, null},
        {422, "Unprocessable Entity"},
        {423, "Locked"},
        {424, "Failed Dependency"},
        {425, null},
        {426, "Upgrade Required"},
        {500, "Internal Server Error"},
        {501, "Not Implemented"},
        {502, "Bad Gateway"},
        {503, "Service Unavailable"},
        {504, "Gateway Timeout"},
        {505, "Http Version Not Supported"},
        {506, null},
        {507, "Insufficient Storage"},
    }
    .ToFrozenDictionary();

    internal static string? Get(int statusCode){
        return Reasons.TryGetValue(statusCode, out string? reason) ? reason : null; 
    }
}


static class Reasons_Array
{
    private static readonly string?[]?[] HttpReasonPhrases = new string?[]?[]
    {
            null,

            [
                /* 100 */ "Continue",
                /* 101 */ "Switching Protocols",
                /* 102 */ "Processing"
            ],

            [
                /* 200 */ "OK",
                /* 201 */ "Created",
                /* 202 */ "Accepted",
                /* 203 */ "Non-Authoritative Information",
                /* 204 */ "No Content",
                /* 205 */ "Reset Content",
                /* 206 */ "Partial Content",
                /* 207 */ "Multi-Status"
            ],

            [
                /* 300 */ "Multiple Choices",
                /* 301 */ "Moved Permanently",
                /* 302 */ "Found",
                /* 303 */ "See Other",
                /* 304 */ "Not Modified",
                /* 305 */ "Use Proxy",
                /* 306 */ null,
                /* 307 */ "Temporary Redirect"
            ],

            [
                /* 400 */ "Bad Request",
                /* 401 */ "Unauthorized",
                /* 402 */ "Payment Required",
                /* 403 */ "Forbidden",
                /* 404 */ "Not Found",
                /* 405 */ "Method Not Allowed",
                /* 406 */ "Not Acceptable",
                /* 407 */ "Proxy Authentication Required",
                /* 408 */ "Request Timeout",
                /* 409 */ "Conflict",
                /* 410 */ "Gone",
                /* 411 */ "Length Required",
                /* 412 */ "Precondition Failed",
                /* 413 */ "Request Entity Too Large",
                /* 414 */ "Request-Uri Too Long",
                /* 415 */ "Unsupported Media Type",
                /* 416 */ "Requested Range Not Satisfiable",
                /* 417 */ "Expectation Failed",
                /* 418 */ null,
                /* 419 */ null,
                /* 420 */ null,
                /* 421 */ null,
                /* 422 */ "Unprocessable Entity",
                /* 423 */ "Locked",
                /* 424 */ "Failed Dependency",
                /* 425 */ null,
                /* 426 */ "Upgrade Required", // RFC 2817
            ],

            [
                /* 500 */ "Internal Server Error",
                /* 501 */ "Not Implemented",
                /* 502 */ "Bad Gateway",
                /* 503 */ "Service Unavailable",
                /* 504 */ "Gateway Timeout",
                /* 505 */ "Http Version Not Supported",
                /* 506 */ null,
                /* 507 */ "Insufficient Storage",
            ]
    };

    internal static string? Get(int code)
    {
        if (code >= 100 && code < 600)
        {
            int i = code / 100;
            int j = code % 100;
            if (j < HttpReasonPhrases[i]!.Length)
            {
                return HttpReasonPhrases[i]![j];
            }
        }
        return null;
    }
}

@martincostello
Copy link
Member

As you're chasing performance, I'm curious, does using Math.DivRem() make the array version faster still, rather than doing / and % separately?

@WhatzGames
Copy link
Contributor Author

WhatzGames commented Jun 26, 2024

Funny enough I had the same idea.

I ran both benchmarks twice and results in the order as before:

First run:

Method Mean Error StdDev Ratio Allocated Alloc Ratio
ReasonPhrases_Array 12.65 ns 0.180 ns 0.160 ns 0.49 - NA
ReasonPhrases_FrozenDict 25.88 ns 0.325 ns 0.304 ns 1.00 - NA
ReasonPhrases_DivRem 12.56 ns 0.116 ns 0.103 ns 0.49 - NA
Method Mean Error StdDev Ratio
ReasonPhrases_Array 130.0 ns 0.54 ns 0.42 ns 0.90
ReasonPhrases_FrozenDict 145.1 ns 0.95 ns 0.89 ns 1.00
ReasonPhrases_DivRem 130.0 ns 0.67 ns 0.60 ns 0.90

second run:

Method Mean Error StdDev Ratio Allocated Alloc Ratio
ReasonPhrases_Array 12.55 ns 0.208 ns 0.195 ns 0.49 - NA
ReasonPhrases_FrozenDict 25.57 ns 0.394 ns 0.368 ns 1.00 - NA
ReasonPhrases_DivRem 12.38 ns 0.113 ns 0.106 ns 0.48 - NA
Method Mean Error StdDev Ratio
ReasonPhrases_Array 130.5 ns 0.89 ns 0.79 ns 0.89
ReasonPhrases_FrozenDict 146.2 ns 1.52 ns 1.27 ns 1.00
ReasonPhrases_DivRem 132.9 ns 1.30 ns 1.22 ns 0.91

The results are a bit inconclusive to me, so it would be probably be best if the benchmarks were run on a different machine to double check.

But as Shuffle seems to have quite the influence on performance here, i tend to believe that performance does improve when using DivRem.

Benchmarks

using System.Collections.Frozen;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;


BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run();
[MemoryDiagnoser]
public class Benchmark
{
    public int[] statusCode = [0, 100, 102, 200, 226, 300, 308, 499, 500, 511];

    [Benchmark]
    public string? ReasonPhrases_Array()
    {
        string? value = string.Empty;
        foreach (var item in statusCode)
        {
            value = Reasons_Array.Get(item);
        }
        return value;
    }

    [Benchmark(Baseline = true)]
    public string? ReasonPhrases_FrozenDict()
    {
        string? value = string.Empty;
        foreach (var item in statusCode)
        {
            value = Reasons_Frozen.Get(item);
        }
        return value;
    }

    [Benchmark]
    public string? ReasonPhrases_DivRem()
    {
        string? value = string.Empty;
        foreach (var item in statusCode)
        {
            value = Reasons_DivRem.Get(item);
        }
        return value;
    }

}

public class ShuffledBenchmark
{
    public int[] statusCode = [0, 100, 102, 200, 226, 300, 308, 499, 500, 511];

    [Benchmark]
    public string? ReasonPhrases_Array()
    {
        Random.Shared.Shuffle(statusCode);
        string? value = string.Empty;
        foreach (var item in statusCode)
        {
            value = Reasons_Array.Get(item);
        }
        return value;
    }

    [Benchmark(Baseline = true)]
    public string? ReasonPhrases_FrozenDict()
    {
        Random.Shared.Shuffle(statusCode);
        string? value = string.Empty;
        foreach (var item in statusCode)
        {
            value = Reasons_Frozen.Get(item);
        }
        return value;
    }

    [Benchmark]
    public string? ReasonPhrases_DivRem()
    {
        Random.Shared.Shuffle(statusCode);
        string? value = string.Empty;
        foreach (var item in statusCode)
        {
            value = Reasons_DivRem.Get(item);
        }
        return value;
    }

}


static class Reasons_Frozen
{
    private static readonly FrozenDictionary<int, string?> Reasons = new Dictionary<int, string?>{
        {100, "Continue"},
        {101, "Switching Protocols"},
        {200,"OK"},
        {201,"Created"},
        {202,"Accepted"},
        {203,"Non-Authoritative Information"},
        {204,"No Content"},
        {205,"Reset Content"},
        {206,"Partial Content"},
        {207,"Multi-Status"},
        {300,"Multiple Choices"},
        {301,"Moved Permanently"},
        {302,"Found"},
        {303,"See Other"},
        {304,"Not Modified"},
        {305,"Use Proxy"},
        {306, null},
        {307,"Temporary Redirect"},
        {400, "Bad Request"},
        {401, "Unauthorized"},
        {402, "Payment Required"},
        {403, "Forbidden"},
        {404, "Not Found"},
        {405, "Method Not Allowed"},
        {406, "Not Acceptable"},
        {407, "Proxy Authentication Required"},
        {408, "Request Timeout"},
        {409, "Conflict"},
        {410, "Gone"},
        {411, "Length Required"},
        {412, "Precondition Failed"},
        {413, "Request Entity Too Large"},
        {414, "Request-Uri Too Long"},
        {415, "Unsupported Media Type"},
        {416, "Requested Range Not Satisfiable"},
        {417, "Expectation Failed"},
        {418, null},
        {419, null},
        {420, null},
        {421, null},
        {422, "Unprocessable Entity"},
        {423, "Locked"},
        {424, "Failed Dependency"},
        {425, null},
        {426, "Upgrade Required"},
        {500, "Internal Server Error"},
        {501, "Not Implemented"},
        {502, "Bad Gateway"},
        {503, "Service Unavailable"},
        {504, "Gateway Timeout"},
        {505, "Http Version Not Supported"},
        {506, null},
        {507, "Insufficient Storage"},
    }
    .ToFrozenDictionary();

    internal static string? Get(int statusCode){
        return Reasons.TryGetValue(statusCode, out string? reason) ? reason : null; 
    }
}


static class Reasons_Array
{
    private static readonly string?[]?[] HttpReasonPhrases = new string?[]?[]
    {
            null,

            [
                /* 100 */ "Continue",
                /* 101 */ "Switching Protocols",
                /* 102 */ "Processing"
            ],

            [
                /* 200 */ "OK",
                /* 201 */ "Created",
                /* 202 */ "Accepted",
                /* 203 */ "Non-Authoritative Information",
                /* 204 */ "No Content",
                /* 205 */ "Reset Content",
                /* 206 */ "Partial Content",
                /* 207 */ "Multi-Status"
            ],

            [
                /* 300 */ "Multiple Choices",
                /* 301 */ "Moved Permanently",
                /* 302 */ "Found",
                /* 303 */ "See Other",
                /* 304 */ "Not Modified",
                /* 305 */ "Use Proxy",
                /* 306 */ null,
                /* 307 */ "Temporary Redirect"
            ],

            [
                /* 400 */ "Bad Request",
                /* 401 */ "Unauthorized",
                /* 402 */ "Payment Required",
                /* 403 */ "Forbidden",
                /* 404 */ "Not Found",
                /* 405 */ "Method Not Allowed",
                /* 406 */ "Not Acceptable",
                /* 407 */ "Proxy Authentication Required",
                /* 408 */ "Request Timeout",
                /* 409 */ "Conflict",
                /* 410 */ "Gone",
                /* 411 */ "Length Required",
                /* 412 */ "Precondition Failed",
                /* 413 */ "Request Entity Too Large",
                /* 414 */ "Request-Uri Too Long",
                /* 415 */ "Unsupported Media Type",
                /* 416 */ "Requested Range Not Satisfiable",
                /* 417 */ "Expectation Failed",
                /* 418 */ null,
                /* 419 */ null,
                /* 420 */ null,
                /* 421 */ null,
                /* 422 */ "Unprocessable Entity",
                /* 423 */ "Locked",
                /* 424 */ "Failed Dependency",
                /* 425 */ null,
                /* 426 */ "Upgrade Required", // RFC 2817
            ],

            [
                /* 500 */ "Internal Server Error",
                /* 501 */ "Not Implemented",
                /* 502 */ "Bad Gateway",
                /* 503 */ "Service Unavailable",
                /* 504 */ "Gateway Timeout",
                /* 505 */ "Http Version Not Supported",
                /* 506 */ null,
                /* 507 */ "Insufficient Storage",
            ]
    };

    internal static string? Get(int code)
    {
        if (code >= 100 && code < 600)
        {
            int i = code / 100;
            int j = code % 100;
            if (j < HttpReasonPhrases[i]!.Length)
            {
                return HttpReasonPhrases[i]![j];
            }
        }
        return null;
    }
}

static class Reasons_DivRem
{
    private static readonly string?[]?[] HttpReasonPhrases = new string?[]?[]
    {
            null,

            [
                /* 100 */ "Continue",
                /* 101 */ "Switching Protocols",
                /* 102 */ "Processing"
            ],

            [
                /* 200 */ "OK",
                /* 201 */ "Created",
                /* 202 */ "Accepted",
                /* 203 */ "Non-Authoritative Information",
                /* 204 */ "No Content",
                /* 205 */ "Reset Content",
                /* 206 */ "Partial Content",
                /* 207 */ "Multi-Status"
            ],

            [
                /* 300 */ "Multiple Choices",
                /* 301 */ "Moved Permanently",
                /* 302 */ "Found",
                /* 303 */ "See Other",
                /* 304 */ "Not Modified",
                /* 305 */ "Use Proxy",
                /* 306 */ null,
                /* 307 */ "Temporary Redirect"
            ],

            [
                /* 400 */ "Bad Request",
                /* 401 */ "Unauthorized",
                /* 402 */ "Payment Required",
                /* 403 */ "Forbidden",
                /* 404 */ "Not Found",
                /* 405 */ "Method Not Allowed",
                /* 406 */ "Not Acceptable",
                /* 407 */ "Proxy Authentication Required",
                /* 408 */ "Request Timeout",
                /* 409 */ "Conflict",
                /* 410 */ "Gone",
                /* 411 */ "Length Required",
                /* 412 */ "Precondition Failed",
                /* 413 */ "Request Entity Too Large",
                /* 414 */ "Request-Uri Too Long",
                /* 415 */ "Unsupported Media Type",
                /* 416 */ "Requested Range Not Satisfiable",
                /* 417 */ "Expectation Failed",
                /* 418 */ null,
                /* 419 */ null,
                /* 420 */ null,
                /* 421 */ null,
                /* 422 */ "Unprocessable Entity",
                /* 423 */ "Locked",
                /* 424 */ "Failed Dependency",
                /* 425 */ null,
                /* 426 */ "Upgrade Required", // RFC 2817
            ],

            [
                /* 500 */ "Internal Server Error",
                /* 501 */ "Not Implemented",
                /* 502 */ "Bad Gateway",
                /* 503 */ "Service Unavailable",
                /* 504 */ "Gateway Timeout",
                /* 505 */ "Http Version Not Supported",
                /* 506 */ null,
                /* 507 */ "Insufficient Storage",
            ]
    };

    internal static string? Get(int code)
    {
        if (code >= 100 && code < 600)
        {
            int i = Math.DivRem(code, 100, out int j);
            if (j < HttpReasonPhrases[i]!.Length)
            {
                return HttpReasonPhrases[i]![j];
            }
        }
        return null;
    }
}

@WhatzGames
Copy link
Contributor Author

So I got some more results.

Home-PC:

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3737/23H2/2023Update/SunValley3)
AMD Ryzen 5 2600X, 1 CPU, 12 logical and 6 physical cores
.NET SDK 9.0.100-preview.5.24307.3
  [Host]     : .NET 9.0.0 (9.0.24.30607), X64 RyuJIT AVX2
  DefaultJob : .NET 9.0.0 (9.0.24.30607), X64 RyuJIT AVX2
Method Mean Error StdDev Ratio RatioSD
Reasoning_2DArray 12.52 ns 0.180 ns 0.168 ns 1.00 0.00
Reasoning_2DArrayDivRem 11.35 ns 0.255 ns 0.239 ns 0.91 0.02

Codespace-Instance:

BenchmarkDotNet v0.13.12, Ubuntu 20.04.6 LTS (Focal Fossa) (container)
AMD EPYC 7763, 1 CPU, 2 logical cores and 1 physical core
.NET SDK 9.0.100-preview.5.24307.3
  [Host]     : .NET 9.0.0 (9.0.24.30607), X64 RyuJIT AVX2
  DefaultJob : .NET 9.0.0 (9.0.24.30607), X64 RyuJIT AVX2
Method Mean Error StdDev Median Ratio RatioSD
Reasoning_2DArray 13.78 ns 0.379 ns 1.106 ns 13.30 ns 1.00 0.00
Reasoning_2DArrayDivRem 13.08 ns 0.336 ns 0.898 ns 12.80 ns 0.96 0.10

Work-Machine:

BenchmarkDotNet v0.13.12, Windows 10 (10.0.19045.4529/22H2/2022Update)
11th Gen Intel Core i7-1165G7 2.80GHz, 1 CPU, 8 logical and 4 physical cores
.NET SDK 8.0.302
  [Host]     : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
  DefaultJob : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
Method Mean Error StdDev Median Ratio RatioSD
Reasoning_2DArray 12.50 ns 0.313 ns 0.779 ns 12.22 ns 1.00 0.00
Reasoning_2DArrayDivRem 13.85 ns 0.319 ns 0.426 ns 13.86 ns 1.09 0.08

Based on these and my previous results I would tend to use DivRem with the latest approach, if that's alright by you.

Benchmark
using System.Collections.Frozen;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run();
public class PreparedBenchmarks
{

    private readonly int[] _statusCodes = [0, 100, 102, 200, 226, 300, 308, 499, 500, 511];

    [GlobalSetup]
    public void Setup() => Random.Shared.Shuffle(_statusCodes);

    [Benchmark(Baseline = true)]
    public string Reasoning_2DArray()
    {
        string values = string.Empty;
        foreach (var statusCode in _statusCodes)
        {
            ReasonPhrases_Array.GetReasonPhrase(statusCode);
        }
        return values;
    }

    [Benchmark]
    public string Reasoning_2DArrayDivRem()
    {
        string values = string.Empty;
        foreach (var statusCode in _statusCodes)
        {
            ReasonPhrases_Array_DivRem.GetReasonPhrase(statusCode);
        }
        return values;
    }
}

public static class ReasonPhrases_Array
{
    private static readonly string[][] HttpReasonPhrases = [
        [],
        [
            /* 100 */ "Continue",
            /* 101 */ "Switching Protocols",
            /* 102 */ "Processing"
        ],
        [
            /* 200 */ "OK",
            /* 201 */ "Created",
            /* 202 */ "Accepted",
            /* 203 */ "Non-Authoritative Information",
            /* 204 */ "No Content",
            /* 205 */ "Reset Content",
            /* 206 */ "Partial Content",
            /* 207 */ "Multi-Status",
            /* 208 */ "Already Reported",
            /* 209 */ string.Empty,
            /* 210 */ string.Empty,
            /* 211 */ string.Empty,
            /* 212 */ string.Empty,
            /* 213 */ string.Empty,
            /* 214 */ string.Empty,
            /* 215 */ string.Empty,
            /* 216 */ string.Empty,
            /* 217 */ string.Empty,
            /* 218 */ string.Empty,
            /* 219 */ string.Empty,
            /* 220 */ string.Empty,
            /* 221 */ string.Empty,
            /* 222 */ string.Empty,
            /* 223 */ string.Empty,
            /* 224 */ string.Empty,
            /* 225 */ string.Empty,
            /* 226 */ "IM Used"
        ],
        [
            /* 300 */ "Multiple Choices",
            /* 301 */ "Moved Permanently",
            /* 302 */ "Found",
            /* 303 */ "See Other",
            /* 304 */ "Not Modified",
            /* 305 */ "Use Proxy",
            /* 306 */ "Switch Proxy",
            /* 307 */ "Temporary Redirect",
            /* 308 */ "Permanent Redirect"
        ],
        [
            /* 400 */ "Bad Request",
            /* 401 */ "Unauthorized",
            /* 402 */ "Payment Required",
            /* 403 */ "Forbidden",
            /* 404 */ "Not Found",
            /* 405 */ "Method Not Allowed",
            /* 406 */ "Not Acceptable",
            /* 407 */ "Proxy Authentication Required",
            /* 408 */ "Request Timeout",
            /* 409 */ "Conflict",
            /* 410 */ "Gone",
            /* 411 */ "Length Required",
            /* 412 */ "Precondition Failed",
            /* 413 */ "Payload Too Large",
            /* 414 */ "URI Too Long",
            /* 415 */ "Unsupported Media Type",
            /* 416 */ "Range Not Satisfiable",
            /* 417 */ "Expectation Failed",
            /* 418 */ "I'm a teapot",
            /* 419 */ "Authentication Timeout",
            /* 420 */ string.Empty,
            /* 421 */ "Misdirected Request",
            /* 422 */ "Unprocessable Entity",
            /* 423 */ "Locked",
            /* 424 */ "Failed Dependency",
            /* 425 */ string.Empty,
            /* 426 */ "Upgrade Required",
            /* 427 */ string.Empty,
            /* 428 */ "Precondition Required",
            /* 429 */ "Too Many Requests",
            /* 430 */ string.Empty,
            /* 431 */ "Request Header Fields Too Large",
            /* 432 */ string.Empty,
            /* 433 */ string.Empty,
            /* 434 */ string.Empty,
            /* 435 */ string.Empty,
            /* 436 */ string.Empty,
            /* 437 */ string.Empty,
            /* 438 */ string.Empty,
            /* 439 */ string.Empty,
            /* 440 */ string.Empty,
            /* 441 */ string.Empty,
            /* 442 */ string.Empty,
            /* 443 */ string.Empty,
            /* 444 */ string.Empty,
            /* 445 */ string.Empty,
            /* 446 */ string.Empty,
            /* 447 */ string.Empty,
            /* 448 */ string.Empty,
            /* 449 */ string.Empty,
            /* 450 */ string.Empty,
            /* 451 */ "Unavailable For Legal Reasons",
            /* 452 */ string.Empty,
            /* 453 */ string.Empty,
            /* 454 */ string.Empty,
            /* 455 */ string.Empty,
            /* 456 */ string.Empty,
            /* 457 */ string.Empty,
            /* 458 */ string.Empty,
            /* 459 */ string.Empty,
            /* 460 */ string.Empty,
            /* 461 */ string.Empty,
            /* 462 */ string.Empty,
            /* 463 */ string.Empty,
            /* 464 */ string.Empty,
            /* 465 */ string.Empty,
            /* 466 */ string.Empty,
            /* 467 */ string.Empty,
            /* 468 */ string.Empty,
            /* 469 */ string.Empty,
            /* 470 */ string.Empty,
            /* 471 */ string.Empty,
            /* 472 */ string.Empty,
            /* 473 */ string.Empty,
            /* 474 */ string.Empty,
            /* 475 */ string.Empty,
            /* 476 */ string.Empty,
            /* 477 */ string.Empty,
            /* 478 */ string.Empty,
            /* 479 */ string.Empty,
            /* 480 */ string.Empty,
            /* 481 */ string.Empty,
            /* 482 */ string.Empty,
            /* 483 */ string.Empty,
            /* 484 */ string.Empty,
            /* 485 */ string.Empty,
            /* 486 */ string.Empty,
            /* 487 */ string.Empty,
            /* 488 */ string.Empty,
            /* 489 */ string.Empty,
            /* 490 */ string.Empty,
            /* 491 */ string.Empty,
            /* 492 */ string.Empty,
            /* 493 */ string.Empty,
            /* 494 */ string.Empty,
            /* 495 */ string.Empty,
            /* 496 */ string.Empty,
            /* 497 */ string.Empty,
            /* 498 */ string.Empty,
            /* 499 */ "Client Closed Request"
        ],
        [
            /* 500 */ "Internal Server Error",
            /* 501 */ "Not Implemented",
            /* 502 */ "Bad Gateway",
            /* 503 */ "Service Unavailable",
            /* 504 */ "Gateway Timeout",
            /* 505 */ "HTTP Version Not Supported",
            /* 506 */ "Variant Also Negotiates",
            /* 507 */ "Insufficient Storage",
            /* 508 */ "Loop Detected",
            /* 509 */ string.Empty,
            /* 510 */ "Not Extended",
            /* 511 */ "Network Authentication Required"
        ]
    ];


    public static string GetReasonPhrase(int statusCode)
    {
        if (statusCode >= 100 && statusCode < 600)
        {
            int i = statusCode / 100;
            int j = statusCode % 100;
            if (j < HttpReasonPhrases[i].Length)
            {
                return HttpReasonPhrases[i][j];
            }
        }
        return string.Empty;
    }




}

public static class ReasonPhrases_Array_DivRem
{
    private static readonly string[][] HttpReasonPhrases = [
        [],
        [
            /* 100 */ "Continue",
            /* 101 */ "Switching Protocols",
            /* 102 */ "Processing"
        ],
        [
            /* 200 */ "OK",
            /* 201 */ "Created",
            /* 202 */ "Accepted",
            /* 203 */ "Non-Authoritative Information",
            /* 204 */ "No Content",
            /* 205 */ "Reset Content",
            /* 206 */ "Partial Content",
            /* 207 */ "Multi-Status",
            /* 208 */ "Already Reported",
            /* 209 */ string.Empty,
            /* 210 */ string.Empty,
            /* 211 */ string.Empty,
            /* 212 */ string.Empty,
            /* 213 */ string.Empty,
            /* 214 */ string.Empty,
            /* 215 */ string.Empty,
            /* 216 */ string.Empty,
            /* 217 */ string.Empty,
            /* 218 */ string.Empty,
            /* 219 */ string.Empty,
            /* 220 */ string.Empty,
            /* 221 */ string.Empty,
            /* 222 */ string.Empty,
            /* 223 */ string.Empty,
            /* 224 */ string.Empty,
            /* 225 */ string.Empty,
            /* 226 */ "IM Used"
        ],
        [
            /* 300 */ "Multiple Choices",
            /* 301 */ "Moved Permanently",
            /* 302 */ "Found",
            /* 303 */ "See Other",
            /* 304 */ "Not Modified",
            /* 305 */ "Use Proxy",
            /* 306 */ "Switch Proxy",
            /* 307 */ "Temporary Redirect",
            /* 308 */ "Permanent Redirect"
        ],
        [
            /* 400 */ "Bad Request",
            /* 401 */ "Unauthorized",
            /* 402 */ "Payment Required",
            /* 403 */ "Forbidden",
            /* 404 */ "Not Found",
            /* 405 */ "Method Not Allowed",
            /* 406 */ "Not Acceptable",
            /* 407 */ "Proxy Authentication Required",
            /* 408 */ "Request Timeout",
            /* 409 */ "Conflict",
            /* 410 */ "Gone",
            /* 411 */ "Length Required",
            /* 412 */ "Precondition Failed",
            /* 413 */ "Payload Too Large",
            /* 414 */ "URI Too Long",
            /* 415 */ "Unsupported Media Type",
            /* 416 */ "Range Not Satisfiable",
            /* 417 */ "Expectation Failed",
            /* 418 */ "I'm a teapot",
            /* 419 */ "Authentication Timeout",
            /* 420 */ string.Empty,
            /* 421 */ "Misdirected Request",
            /* 422 */ "Unprocessable Entity",
            /* 423 */ "Locked",
            /* 424 */ "Failed Dependency",
            /* 425 */ string.Empty,
            /* 426 */ "Upgrade Required",
            /* 427 */ string.Empty,
            /* 428 */ "Precondition Required",
            /* 429 */ "Too Many Requests",
            /* 430 */ string.Empty,
            /* 431 */ "Request Header Fields Too Large",
            /* 432 */ string.Empty,
            /* 433 */ string.Empty,
            /* 434 */ string.Empty,
            /* 435 */ string.Empty,
            /* 436 */ string.Empty,
            /* 437 */ string.Empty,
            /* 438 */ string.Empty,
            /* 439 */ string.Empty,
            /* 440 */ string.Empty,
            /* 441 */ string.Empty,
            /* 442 */ string.Empty,
            /* 443 */ string.Empty,
            /* 444 */ string.Empty,
            /* 445 */ string.Empty,
            /* 446 */ string.Empty,
            /* 447 */ string.Empty,
            /* 448 */ string.Empty,
            /* 449 */ string.Empty,
            /* 450 */ string.Empty,
            /* 451 */ "Unavailable For Legal Reasons",
            /* 452 */ string.Empty,
            /* 453 */ string.Empty,
            /* 454 */ string.Empty,
            /* 455 */ string.Empty,
            /* 456 */ string.Empty,
            /* 457 */ string.Empty,
            /* 458 */ string.Empty,
            /* 459 */ string.Empty,
            /* 460 */ string.Empty,
            /* 461 */ string.Empty,
            /* 462 */ string.Empty,
            /* 463 */ string.Empty,
            /* 464 */ string.Empty,
            /* 465 */ string.Empty,
            /* 466 */ string.Empty,
            /* 467 */ string.Empty,
            /* 468 */ string.Empty,
            /* 469 */ string.Empty,
            /* 470 */ string.Empty,
            /* 471 */ string.Empty,
            /* 472 */ string.Empty,
            /* 473 */ string.Empty,
            /* 474 */ string.Empty,
            /* 475 */ string.Empty,
            /* 476 */ string.Empty,
            /* 477 */ string.Empty,
            /* 478 */ string.Empty,
            /* 479 */ string.Empty,
            /* 480 */ string.Empty,
            /* 481 */ string.Empty,
            /* 482 */ string.Empty,
            /* 483 */ string.Empty,
            /* 484 */ string.Empty,
            /* 485 */ string.Empty,
            /* 486 */ string.Empty,
            /* 487 */ string.Empty,
            /* 488 */ string.Empty,
            /* 489 */ string.Empty,
            /* 490 */ string.Empty,
            /* 491 */ string.Empty,
            /* 492 */ string.Empty,
            /* 493 */ string.Empty,
            /* 494 */ string.Empty,
            /* 495 */ string.Empty,
            /* 496 */ string.Empty,
            /* 497 */ string.Empty,
            /* 498 */ string.Empty,
            /* 499 */ "Client Closed Request"
        ],
        [
            /* 500 */ "Internal Server Error",
            /* 501 */ "Not Implemented",
            /* 502 */ "Bad Gateway",
            /* 503 */ "Service Unavailable",
            /* 504 */ "Gateway Timeout",
            /* 505 */ "HTTP Version Not Supported",
            /* 506 */ "Variant Also Negotiates",
            /* 507 */ "Insufficient Storage",
            /* 508 */ "Loop Detected",
            /* 509 */ string.Empty,
            /* 510 */ "Not Extended",
            /* 511 */ "Network Authentication Required"
        ]
    ];

    public static string GetReasonPhrase(int statusCode)
    {
        if (statusCode >= 100 && statusCode < 600)
        {
            int i = Math.DivRem(statusCode, 100, out int j);
            if (j < HttpReasonPhrases[i].Length)
            {
                return HttpReasonPhrases[i][j];
            }
        }
        return string.Empty;
    }
}

@WhatzGames WhatzGames changed the title Perf: ReasonPhrases as FrozenDictionary instead of Dictionary Perf: ReasonPhrases as Array instead of Dictionary Jul 2, 2024
@danmoseley
Copy link
Member

This suggests that frozen dictionary could usefully have a specialization for (near?) sequential integer keys, if that case is common enough...?

@WhatzGames
Copy link
Contributor Author

A quick initial update on the benchmark after the latest adjustments:

Method Mean Error StdDev Ratio
Reasoning_Dict 42.606 ns 0.2499 ns 0.2338 ns 1.00
Reasoning_Array 11.076 ns 0.1218 ns 0.1140 ns 0.26
Reasoning_Array_Review 7.442 ns 0.0560 ns 0.0497 ns 0.17

I'll adjust my array based benchmarks and update the description accordingly.

Benchmarks
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run();

public class Benchmarks{

    private readonly int[] _statusCodes = [0, 100, 102, 200, 226, 300, 308, 499, 500, 511];

    [GlobalSetup]
    public void Setup() => Random.Shared.Shuffle(_statusCodes);

     [Benchmark(Baseline = true)]
    public string Reasoning_Dict() {
        string values = string.Empty;
        foreach (var statusCode in _statusCodes)
        {
            ReasonPhrases_Default.GetReasonPhrase(statusCode);
        }
        return values;
    }

    [Benchmark]
    public string Reasoning_Array(){
        string values = string.Empty;
        foreach (var statusCode in _statusCodes)
        {
            ReasonPhrases_Array.GetReasonPhrase(statusCode);
        }
        return values;
    }

    [Benchmark]
    public string Reasoning_Array_Review(){
        string values = string.Empty;
        foreach (var statusCode in _statusCodes)
        {
            ReasonPhrases_Array_Review.GetReasonPhrase(statusCode);
        }
        return values;
    }
}

public static class ReasonPhrases_Default
{
    private static readonly Dictionary<int, string> s_phrases = new()
    {
        { 100, "Continue" },
        { 101, "Switching Protocols" },
        { 102, "Processing" },

        { 200, "OK" },
        { 201, "Created" },
        { 202, "Accepted" },
        { 203, "Non-Authoritative Information" },
        { 204, "No Content" },
        { 205, "Reset Content" },
        { 206, "Partial Content" },
        { 207, "Multi-Status" },
        { 208, "Already Reported" },
        { 226, "IM Used" },

        { 300, "Multiple Choices" },
        { 301, "Moved Permanently" },
        { 302, "Found" },
        { 303, "See Other" },
        { 304, "Not Modified" },
        { 305, "Use Proxy" },
        { 306, "Switch Proxy" },
        { 307, "Temporary Redirect" },
        { 308, "Permanent Redirect" },

        { 400, "Bad Request" },
        { 401, "Unauthorized" },
        { 402, "Payment Required" },
        { 403, "Forbidden" },
        { 404, "Not Found" },
        { 405, "Method Not Allowed" },
        { 406, "Not Acceptable" },
        { 407, "Proxy Authentication Required" },
        { 408, "Request Timeout" },
        { 409, "Conflict" },
        { 410, "Gone" },
        { 411, "Length Required" },
        { 412, "Precondition Failed" },
        { 413, "Payload Too Large" },
        { 414, "URI Too Long" },
        { 415, "Unsupported Media Type" },
        { 416, "Range Not Satisfiable" },
        { 417, "Expectation Failed" },
        { 418, "I'm a teapot" },
        { 419, "Authentication Timeout" },
        { 421, "Misdirected Request" },
        { 422, "Unprocessable Entity" },
        { 423, "Locked" },
        { 424, "Failed Dependency" },
        { 426, "Upgrade Required" },
        { 428, "Precondition Required" },
        { 429, "Too Many Requests" },
        { 431, "Request Header Fields Too Large" },
        { 451, "Unavailable For Legal Reasons" },
        { 499, "Client Closed Request" },

        { 500, "Internal Server Error" },
        { 501, "Not Implemented" },
        { 502, "Bad Gateway" },
        { 503, "Service Unavailable" },
        { 504, "Gateway Timeout" },
        { 505, "HTTP Version Not Supported" },
        { 506, "Variant Also Negotiates" },
        { 507, "Insufficient Storage" },
        { 508, "Loop Detected" },
        { 510, "Not Extended" },
        { 511, "Network Authentication Required" },
    };

    /// <summary>
    /// Gets the reason phrase for the specified status code.
    /// </summary>
    /// <param name="statusCode">The status code.</param>
    /// <returns>The reason phrase, or <see cref="string.Empty"/> if the status code is unknown.</returns>
    public static string GetReasonPhrase(int statusCode)
    {
        return s_phrases.TryGetValue(statusCode, out string? phrase) ? phrase : string.Empty;
    }
}

public static class ReasonPhrases_Array
{
    private static readonly string[][] HttpReasonPhrases = [
        [],
        [
            /* 100 */ "Continue",
            /* 101 */ "Switching Protocols",
            /* 102 */ "Processing"
        ],
        [
            /* 200 */ "OK",
            /* 201 */ "Created",
            /* 202 */ "Accepted",
            /* 203 */ "Non-Authoritative Information",
            /* 204 */ "No Content",
            /* 205 */ "Reset Content",
            /* 206 */ "Partial Content",
            /* 207 */ "Multi-Status",
            /* 208 */ "Already Reported",
            /* 209 */ string.Empty,
            /* 210 */ string.Empty,
            /* 211 */ string.Empty,
            /* 212 */ string.Empty,
            /* 213 */ string.Empty,
            /* 214 */ string.Empty,
            /* 215 */ string.Empty,
            /* 216 */ string.Empty,
            /* 217 */ string.Empty,
            /* 218 */ string.Empty,
            /* 219 */ string.Empty,
            /* 220 */ string.Empty,
            /* 221 */ string.Empty,
            /* 222 */ string.Empty,
            /* 223 */ string.Empty,
            /* 224 */ string.Empty,
            /* 225 */ string.Empty,
            /* 226 */ "IM Used"
        ],
        [
            /* 300 */ "Multiple Choices",
            /* 301 */ "Moved Permanently",
            /* 302 */ "Found",
            /* 303 */ "See Other",
            /* 304 */ "Not Modified",
            /* 305 */ "Use Proxy",
            /* 306 */ "Switch Proxy",
            /* 307 */ "Temporary Redirect",
            /* 308 */ "Permanent Redirect"
        ],
        [
            /* 400 */ "Bad Request",
            /* 401 */ "Unauthorized",
            /* 402 */ "Payment Required",
            /* 403 */ "Forbidden",
            /* 404 */ "Not Found",
            /* 405 */ "Method Not Allowed",
            /* 406 */ "Not Acceptable",
            /* 407 */ "Proxy Authentication Required",
            /* 408 */ "Request Timeout",
            /* 409 */ "Conflict",
            /* 410 */ "Gone",
            /* 411 */ "Length Required",
            /* 412 */ "Precondition Failed",
            /* 413 */ "Payload Too Large",
            /* 414 */ "URI Too Long",
            /* 415 */ "Unsupported Media Type",
            /* 416 */ "Range Not Satisfiable",
            /* 417 */ "Expectation Failed",
            /* 418 */ "I'm a teapot",
            /* 419 */ "Authentication Timeout",
            /* 420 */ string.Empty,
            /* 421 */ "Misdirected Request",
            /* 422 */ "Unprocessable Entity",
            /* 423 */ "Locked",
            /* 424 */ "Failed Dependency",
            /* 425 */ string.Empty,
            /* 426 */ "Upgrade Required",
            /* 427 */ string.Empty,
            /* 428 */ "Precondition Required",
            /* 429 */ "Too Many Requests",
            /* 430 */ string.Empty,
            /* 431 */ "Request Header Fields Too Large",
            /* 432 */ string.Empty,
            /* 433 */ string.Empty,
            /* 434 */ string.Empty,
            /* 435 */ string.Empty,
            /* 436 */ string.Empty,
            /* 437 */ string.Empty,
            /* 438 */ string.Empty,
            /* 439 */ string.Empty,
            /* 440 */ string.Empty,
            /* 441 */ string.Empty,
            /* 442 */ string.Empty,
            /* 443 */ string.Empty,
            /* 444 */ string.Empty,
            /* 445 */ string.Empty,
            /* 446 */ string.Empty,
            /* 447 */ string.Empty,
            /* 448 */ string.Empty,
            /* 449 */ string.Empty,
            /* 450 */ string.Empty,
            /* 451 */ "Unavailable For Legal Reasons",
            /* 452 */ string.Empty,
            /* 453 */ string.Empty,
            /* 454 */ string.Empty,
            /* 455 */ string.Empty,
            /* 456 */ string.Empty,
            /* 457 */ string.Empty,
            /* 458 */ string.Empty,
            /* 459 */ string.Empty,
            /* 460 */ string.Empty,
            /* 461 */ string.Empty,
            /* 462 */ string.Empty,
            /* 463 */ string.Empty,
            /* 464 */ string.Empty,
            /* 465 */ string.Empty,
            /* 466 */ string.Empty,
            /* 467 */ string.Empty,
            /* 468 */ string.Empty,
            /* 469 */ string.Empty,
            /* 470 */ string.Empty,
            /* 471 */ string.Empty,
            /* 472 */ string.Empty,
            /* 473 */ string.Empty,
            /* 474 */ string.Empty,
            /* 475 */ string.Empty,
            /* 476 */ string.Empty,
            /* 477 */ string.Empty,
            /* 478 */ string.Empty,
            /* 479 */ string.Empty,
            /* 480 */ string.Empty,
            /* 481 */ string.Empty,
            /* 482 */ string.Empty,
            /* 483 */ string.Empty,
            /* 484 */ string.Empty,
            /* 485 */ string.Empty,
            /* 486 */ string.Empty,
            /* 487 */ string.Empty,
            /* 488 */ string.Empty,
            /* 489 */ string.Empty,
            /* 490 */ string.Empty,
            /* 491 */ string.Empty,
            /* 492 */ string.Empty,
            /* 493 */ string.Empty,
            /* 494 */ string.Empty,
            /* 495 */ string.Empty,
            /* 496 */ string.Empty,
            /* 497 */ string.Empty,
            /* 498 */ string.Empty,
            /* 499 */ "Client Closed Request"
        ],
        [
            /* 500 */ "Internal Server Error",
            /* 501 */ "Not Implemented",
            /* 502 */ "Bad Gateway",
            /* 503 */ "Service Unavailable",
            /* 504 */ "Gateway Timeout",
            /* 505 */ "HTTP Version Not Supported",
            /* 506 */ "Variant Also Negotiates",
            /* 507 */ "Insufficient Storage",
            /* 508 */ "Loop Detected",
            /* 509 */ string.Empty,
            /* 510 */ "Not Extended",
            /* 511 */ "Network Authentication Required"
        ]
    ];

    public static string GetReasonPhrase(int statusCode)
    {
        if (statusCode >= 100 && statusCode < 600)
        {
            int i = Math.DivRem(statusCode, 100, out int j);
            if (j < HttpReasonPhrases[i].Length)
            {
                return HttpReasonPhrases[i][j];
            }
        }
        return string.Empty;
    }
}

public static class ReasonPhrases_Array_Review
{
    private static readonly string[][] HttpReasonPhrases = [
        [],
        [
            /* 100 */ "Continue",
            /* 101 */ "Switching Protocols",
            /* 102 */ "Processing"
        ],
        [
            /* 200 */ "OK",
            /* 201 */ "Created",
            /* 202 */ "Accepted",
            /* 203 */ "Non-Authoritative Information",
            /* 204 */ "No Content",
            /* 205 */ "Reset Content",
            /* 206 */ "Partial Content",
            /* 207 */ "Multi-Status",
            /* 208 */ "Already Reported",
            /* 209 */ string.Empty,
            /* 210 */ string.Empty,
            /* 211 */ string.Empty,
            /* 212 */ string.Empty,
            /* 213 */ string.Empty,
            /* 214 */ string.Empty,
            /* 215 */ string.Empty,
            /* 216 */ string.Empty,
            /* 217 */ string.Empty,
            /* 218 */ string.Empty,
            /* 219 */ string.Empty,
            /* 220 */ string.Empty,
            /* 221 */ string.Empty,
            /* 222 */ string.Empty,
            /* 223 */ string.Empty,
            /* 224 */ string.Empty,
            /* 225 */ string.Empty,
            /* 226 */ "IM Used"
        ],
        [
            /* 300 */ "Multiple Choices",
            /* 301 */ "Moved Permanently",
            /* 302 */ "Found",
            /* 303 */ "See Other",
            /* 304 */ "Not Modified",
            /* 305 */ "Use Proxy",
            /* 306 */ "Switch Proxy",
            /* 307 */ "Temporary Redirect",
            /* 308 */ "Permanent Redirect"
        ],
        [
            /* 400 */ "Bad Request",
            /* 401 */ "Unauthorized",
            /* 402 */ "Payment Required",
            /* 403 */ "Forbidden",
            /* 404 */ "Not Found",
            /* 405 */ "Method Not Allowed",
            /* 406 */ "Not Acceptable",
            /* 407 */ "Proxy Authentication Required",
            /* 408 */ "Request Timeout",
            /* 409 */ "Conflict",
            /* 410 */ "Gone",
            /* 411 */ "Length Required",
            /* 412 */ "Precondition Failed",
            /* 413 */ "Payload Too Large",
            /* 414 */ "URI Too Long",
            /* 415 */ "Unsupported Media Type",
            /* 416 */ "Range Not Satisfiable",
            /* 417 */ "Expectation Failed",
            /* 418 */ "I'm a teapot",
            /* 419 */ "Authentication Timeout",
            /* 420 */ string.Empty,
            /* 421 */ "Misdirected Request",
            /* 422 */ "Unprocessable Entity",
            /* 423 */ "Locked",
            /* 424 */ "Failed Dependency",
            /* 425 */ string.Empty,
            /* 426 */ "Upgrade Required",
            /* 427 */ string.Empty,
            /* 428 */ "Precondition Required",
            /* 429 */ "Too Many Requests",
            /* 430 */ string.Empty,
            /* 431 */ "Request Header Fields Too Large",
            /* 432 */ string.Empty,
            /* 433 */ string.Empty,
            /* 434 */ string.Empty,
            /* 435 */ string.Empty,
            /* 436 */ string.Empty,
            /* 437 */ string.Empty,
            /* 438 */ string.Empty,
            /* 439 */ string.Empty,
            /* 440 */ string.Empty,
            /* 441 */ string.Empty,
            /* 442 */ string.Empty,
            /* 443 */ string.Empty,
            /* 444 */ string.Empty,
            /* 445 */ string.Empty,
            /* 446 */ string.Empty,
            /* 447 */ string.Empty,
            /* 448 */ string.Empty,
            /* 449 */ string.Empty,
            /* 450 */ string.Empty,
            /* 451 */ "Unavailable For Legal Reasons",
            /* 452 */ string.Empty,
            /* 453 */ string.Empty,
            /* 454 */ string.Empty,
            /* 455 */ string.Empty,
            /* 456 */ string.Empty,
            /* 457 */ string.Empty,
            /* 458 */ string.Empty,
            /* 459 */ string.Empty,
            /* 460 */ string.Empty,
            /* 461 */ string.Empty,
            /* 462 */ string.Empty,
            /* 463 */ string.Empty,
            /* 464 */ string.Empty,
            /* 465 */ string.Empty,
            /* 466 */ string.Empty,
            /* 467 */ string.Empty,
            /* 468 */ string.Empty,
            /* 469 */ string.Empty,
            /* 470 */ string.Empty,
            /* 471 */ string.Empty,
            /* 472 */ string.Empty,
            /* 473 */ string.Empty,
            /* 474 */ string.Empty,
            /* 475 */ string.Empty,
            /* 476 */ string.Empty,
            /* 477 */ string.Empty,
            /* 478 */ string.Empty,
            /* 479 */ string.Empty,
            /* 480 */ string.Empty,
            /* 481 */ string.Empty,
            /* 482 */ string.Empty,
            /* 483 */ string.Empty,
            /* 484 */ string.Empty,
            /* 485 */ string.Empty,
            /* 486 */ string.Empty,
            /* 487 */ string.Empty,
            /* 488 */ string.Empty,
            /* 489 */ string.Empty,
            /* 490 */ string.Empty,
            /* 491 */ string.Empty,
            /* 492 */ string.Empty,
            /* 493 */ string.Empty,
            /* 494 */ string.Empty,
            /* 495 */ string.Empty,
            /* 496 */ string.Empty,
            /* 497 */ string.Empty,
            /* 498 */ string.Empty,
            /* 499 */ "Client Closed Request"
        ],
        [
            /* 500 */ "Internal Server Error",
            /* 501 */ "Not Implemented",
            /* 502 */ "Bad Gateway",
            /* 503 */ "Service Unavailable",
            /* 504 */ "Gateway Timeout",
            /* 505 */ "HTTP Version Not Supported",
            /* 506 */ "Variant Also Negotiates",
            /* 507 */ "Insufficient Storage",
            /* 508 */ "Loop Detected",
            /* 509 */ string.Empty,
            /* 510 */ "Not Extended",
            /* 511 */ "Network Authentication Required"
        ]
    ];

    public static string GetReasonPhrase(int statusCode)
    {
            if ((uint)(statusCode - 100) < 500)
            {
                var (i,j) = Math.DivRem((uint)statusCode, 100);
                string[] phrases = HttpReasonPhrases[i];
                if (j < (uint)phrases.Length)
                {
                    return phrases[j];
                }
            }
            return string.Empty;
    }
}

@adityamandaleeka
Copy link
Member

adityamandaleeka commented Jul 8, 2024

@WhatzGames Thanks for the thoroughness here, and welcome to the repo!

And thanks for the great review feedback @gfoidl

Co-authored-by: Günther Foidl <[email protected]>
@dotnet-policy-service dotnet-policy-service bot added the pending-ci-rerun When assigned to a PR indicates that the CI checks should be rerun label Jul 16, 2024
@adityamandaleeka
Copy link
Member

/azp run

@dotnet-policy-service dotnet-policy-service bot removed the pending-ci-rerun When assigned to a PR indicates that the CI checks should be rerun label Jul 17, 2024
Copy link

Azure Pipelines successfully started running 3 pipeline(s).

@dotnet-policy-service dotnet-policy-service bot added the pending-ci-rerun When assigned to a PR indicates that the CI checks should be rerun label Jul 24, 2024
@mkArtakMSFT
Copy link
Member

Thanks for your patience, @WhatzGames.
@gfoidl thanks for the review. Given that you haven't yet signed off, I wonder if you still have any feedback to share?
@BrennanConroy you've also reviewed this, thank you. If there is nothing blocking from your point of view, could you please sign off?

@BrennanConroy
Copy link
Member

/azp run

@BrennanConroy BrennanConroy removed the pending-ci-rerun When assigned to a PR indicates that the CI checks should be rerun label Jan 15, 2025
Copy link

Azure Pipelines successfully started running 3 pipeline(s).

@BrennanConroy BrennanConroy enabled auto-merge (squash) January 15, 2025 01:07
@BrennanConroy BrennanConroy merged commit 4fc1a87 into dotnet:main Jan 15, 2025
27 checks passed
@dotnet-policy-service dotnet-policy-service bot added this to the 10.0-preview1 milestone Jan 15, 2025
@davidfowl
Copy link
Member

Did we look at frozen dictionary at all?

cc @stephentoub

@BrennanConroy
Copy link
Member

#56304 (comment)

@davidfowl
Copy link
Member

Feels like this could be an optimization of frozen dict if the keysaare int and the range is known and under some limit. (#56304 (comment))

@danmoseley
Copy link
Member

Feels like this could be an optimization of frozen dict if the keysaare int and the range is known and under some limit. (#56304 (comment))

@stephentoub anything worth recording here as a suggestion? frozendictionary has various different heuristics already and it's not clear to me whether there's something here.
linking dotnet/runtime#77891 but it seems to be basically left for source generation.

@stephentoub
Copy link
Member

stephentoub commented Jan 17, 2025

There are many possible specializations in FrozenSet/Dictionary. Each one adds more code, heuristics, maintenance, etc. Do we think this scenario of densely-packed Int32s is common enough to special-case it? It obviously could implement such a scheme; the question is whether it's worthwhile. This particular scheme is also very-finely tuned specifically for HTTP status codes, with groups that start evenly at 100/200/300/400/500; handling other groupings would either require more space or more expensive partitioning. Again, it comes down to what scenarios we're trying to optimize and how much we want to invest to do so.

@stephentoub
Copy link
Member

I put up dotnet/runtime#111886. Not sure you'd actually want to switch to it, but you could experiment; it'll likely consume more memory but might be a tad faster.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-networking Includes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions community-contribution Indicates that the PR has been added by a community member
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants