aspnetcore icon indicating copy to clipboard operation
aspnetcore copied to clipboard

Perf: ReasonPhrases as FrozenDictionary instead of Dictionary

Open WhatzGames opened this issue 1 year ago • 11 comments

ReasonPhrases as FrozenDictionary instead of Dictionary

  • [x] You've read the Contributor Guide and Code of Conduct.
  • [x] You've included unit or integration tests for your change, where applicable.
  • [x] 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 a FrozenDictionary.

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 43.80 ns 0.328 ns 0.307 ns 1.00
Reasoning_FrDict 25.27 ns 0.290 ns 0.271 ns 0.58
Reasoning_Switch 35.28 ns 0.425 ns 0.377 ns 0.81

UnpreparedBenchmarks

Method Mean Error StdDev Ratio RatioSD
Default 152.8 ns 1.32 ns 1.17 ns 1.00 0.00
FrozenDictionary 135.6 ns 1.29 ns 1.01 ns 0.89 0.01
Switch 213.1 ns 2.11 ns 1.76 ns 1.39 0.02

PreparedBenchmarks

Method Mean Error StdDev Ratio
Reasoning_Dict 41.54 ns 0.416 ns 0.369 ns 1.00
Reasoning_Dict 41.54 ns 0.416 ns 0.369 ns 1.00
Reasoning_FrDict 24.87 ns 0.476 ns 0.445 ns 0.60
Reasoning_Switch 35.04 ns 0.232 ns 0.217 ns 0.84

StraightBenchmarks

Method StatusCode Mean Error StdDev Ratio RatioSD
Reasoning_Dict 0 3.4472 ns 0.0425 ns 0.0398 ns 1.00 0.00
Reasongin_FrDict 0 1.6600 ns 0.0498 ns 0.0441 ns 0.48 0.01
Reasoning_Switch 0 2.3406 ns 0.0185 ns 0.0145 ns 0.68 0.01
Reasoning_Dict 100 3.7830 ns 0.1137 ns 0.1008 ns 1.00 0.00
Reasongin_FrDict 100 1.8294 ns 0.0827 ns 0.0773 ns 0.49 0.03
Reasoning_Switch 100 0.7691 ns 0.0078 ns 0.0061 ns 0.20 0.01
Reasoning_Dict 102 3.9100 ns 0.1110 ns 0.1038 ns 1.00 0.00
Reasongin_FrDict 102 1.7416 ns 0.0108 ns 0.0090 ns 0.44 0.01
Reasoning_Switch 102 0.8137 ns 0.0354 ns 0.0331 ns 0.21 0.01
Reasoning_Dict 200 3.7386 ns 0.0331 ns 0.0294 ns 1.00 0.00
Reasongin_FrDict 200 1.8359 ns 0.0803 ns 0.0789 ns 0.49 0.02
Reasoning_Switch 200 1.5291 ns 0.0169 ns 0.0132 ns 0.41 0.00
Reasoning_Dict 226 4.7190 ns 0.0411 ns 0.0343 ns 1.00 0.00
Reasongin_FrDict 226 1.7793 ns 0.0637 ns 0.0565 ns 0.38 0.01
Reasoning_Switch 226 1.8923 ns 0.0405 ns 0.0379 ns 0.40 0.01
Reasoning_Dict 300 3.7972 ns 0.0397 ns 0.0332 ns 1.00 0.00
Reasongin_FrDict 300 2.2401 ns 0.0626 ns 0.0523 ns 0.59 0.01
Reasoning_Switch 300 0.7895 ns 0.0053 ns 0.0047 ns 0.21 0.00
Reasoning_Dict 308 3.8153 ns 0.0695 ns 0.0650 ns 1.00 0.00
Reasongin_FrDict 308 1.8161 ns 0.0852 ns 0.0797 ns 0.48 0.02
Reasoning_Switch 308 0.7688 ns 0.0281 ns 0.0249 ns 0.20 0.01
Reasoning_Dict 499 3.7876 ns 0.0734 ns 0.0651 ns 1.00 0.00
Reasongin_FrDict 499 1.7590 ns 0.0150 ns 0.0133 ns 0.46 0.01
Reasoning_Switch 499 1.7082 ns 0.0349 ns 0.0326 ns 0.45 0.01
Reasoning_Dict 500 3.7359 ns 0.0368 ns 0.0287 ns 1.00 0.00
Reasongin_FrDict 500 1.7945 ns 0.0708 ns 0.0663 ns 0.48 0.02
Reasoning_Switch 500 1.6812 ns 0.0292 ns 0.0244 ns 0.45 0.01
Reasoning_Dict 511 3.8497 ns 0.1179 ns 0.1103 ns 1.00 0.00
Reasongin_FrDict 511 1.8036 ns 0.0737 ns 0.0689 ns 0.47 0.02
Reasoning_Switch 511 2.0824 ns 0.0361 ns 0.0320 ns 0.54 0.02

Edit: updated benchmarks to show results according to https://github.com/dotnet/aspnetcore/pull/56304#issuecomment-2183338237

WhatzGames avatar Jun 18 '24 20:06 WhatzGames

@dotnet-policy-service agree

WhatzGames avatar Jun 18 '24 20:06 WhatzGames

@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 avatar Jun 20 '24 16:06 gfoidl

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
    };
}

gfoidl avatar Jun 21 '24 13:06 gfoidl

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
    };
}

WhatzGames avatar Jun 21 '24 19:06 WhatzGames

change the PR to use a FrozenDictionary instead.

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

gfoidl avatar Jun 24 '24 15:06 gfoidl

@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?

captainsafia avatar Jun 25 '24 00:06 captainsafia

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

WhatzGames avatar Jun 25 '24 20:06 WhatzGames

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;
    }
}

WhatzGames avatar Jun 25 '24 21:06 WhatzGames

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

martincostello avatar Jun 25 '24 22:06 martincostello

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 avatar Jun 26 '24 04:06 WhatzGames

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 avatar Jun 28 '24 21:06 WhatzGames

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

danmoseley avatar Jul 04 '24 17:07 danmoseley

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;
    }
}

WhatzGames avatar Jul 04 '24 18:07 WhatzGames

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

And thanks for the great review feedback @gfoidl

adityamandaleeka avatar Jul 08 '24 18:07 adityamandaleeka

/azp run

adityamandaleeka avatar Jul 17 '24 01:07 adityamandaleeka

Azure Pipelines successfully started running 3 pipeline(s).

azure-pipelines[bot] avatar Jul 17 '24 01:07 azure-pipelines[bot]

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?

mkArtakMSFT avatar Dec 19 '24 01:12 mkArtakMSFT

/azp run

BrennanConroy avatar Jan 15 '25 01:01 BrennanConroy

Azure Pipelines successfully started running 3 pipeline(s).

azure-pipelines[bot] avatar Jan 15 '25 01:01 azure-pipelines[bot]

Did we look at frozen dictionary at all?

cc @stephentoub

davidfowl avatar Jan 15 '25 03:01 davidfowl

https://github.com/dotnet/aspnetcore/pull/56304#issuecomment-2190015487

BrennanConroy avatar Jan 15 '25 03:01 BrennanConroy

Feels like this could be an optimization of frozen dict if the keysaare int and the range is known and under some limit. (https://github.com/dotnet/aspnetcore/pull/56304#issuecomment-2209369961)

davidfowl avatar Jan 15 '25 03:01 davidfowl

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 https://github.com/dotnet/runtime/issues/77891 but it seems to be basically left for source generation.

danmoseley avatar Jan 17 '25 19:01 danmoseley

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 avatar Jan 17 '25 20:01 stephentoub

I put up https://github.com/dotnet/runtime/pull/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.

stephentoub avatar Jan 28 '25 06:01 stephentoub