Runtime: Question: Optimise const string comparison for startup time

Created on 3 Jul 2019  路  8Comments  路  Source: dotnet/runtime

I'm working on support for a new command line switch on a .NET Core 3.0 application in https://github.com/PowerShell/PowerShell/pull/10050 and I'm trying to optimise for startup time since we're quite sensitive to it.

Basically I'm taking the string[] args argument vector and want to compare it against some known strings like "login" and "command" doing as little loading or allocation as possible.

Is there any advantage to using a ReadOnlySpan<char> vs just a string literal, and is it better to stack allocate the char array rather than "string".AsSpan()?

The logic looks like:

const string login = "login";
// ReadOnlySpan<char> login = "login".AsSpan();
// ReadOnlySpan<char> login = stackalloc char[] { 'l', 'o', 'g', 'i', 'n' };

if (IsParam(arg, login))
{
    DoLogin();
}

...

private static bool IsParam(string arg, string requiredParam)
{
    // iterative string comparison
}
question

Most helpful comment

If you want to optimize startup time, I would recommend to make your code as simple as possible. Startup time tends to be dominated by what it takes to bring the code into memory and prepare for execution.

All 8 comments

The string literal shouldn't allocate and in .NET Core 3.0 the ReadOnlySpan<char> and string comparison use the same code path if you specify Ordinal e.g.

private static bool IsParam(string arg, string requiredParam)
{
    return requiredParam.Equals(arg, StringComparison.Ordinal);
}

If you are case insenitive then specifying the OrdinalIgnoreCase rather than using Invariant culture or Culture comparision is also faster in 3.0 e.g.

private static bool IsParam(string arg, string requiredParam)
{
    return requiredParam.Equals(arg, StringComparison.OrdinalIgnoreCase);
}

Ah awesome!

We actually do a prefix comparison, so we manually iterate and compare characters somewhat like:

for (int i = 0; i < requiredParam.Length; i++)
{
    if (arg[i] != requiredParam[i]) { return false; }
}
return true;

I assume in that case there's still no difference between ReadOnlySpan<char> and string?

@rjmholt arg[i].StartsWith(requiredParam, StringComparison.Ordinal) should perform better since it's vectorized (and unrolled ) via SpanHelpers.SequenceEqual.

then I'd use StartsWith which should be faster

private static bool IsParam(string arg, string requiredParam)
{
    return arg.StartsWith(requiredParam, StringComparison.Ordinal);
}

@benaadams @rjmholt
made a quick benchmark:

public class Benchs
{
    const string requiredParam = "login";

    public IEnumerable<string> TestInput()
    {
        yield return "login long long string long long string long long string long long string";
        yield return "login";
        yield return "logiz";
        yield return "zogin";
        yield return "a";
    }

    [Benchmark(Baseline = true)]
    [ArgumentsSource(nameof(TestInput))]
    public bool IsParam_StartsWith(string arg)
    {
        return arg.StartsWith(requiredParam, StringComparison.Ordinal);
    }

    [Benchmark]
    [ArgumentsSource(nameof(TestInput))]
    public bool IsParam_StartsWith2(string arg)
    {
        return (uint)arg.Length > (uint)requiredParam.Length && arg.StartsWith(requiredParam, StringComparison.Ordinal);
    }

    [Benchmark]
    [ArgumentsSource(nameof(TestInput))]
    public bool IsParam_Loop(string arg)
    {
        if (arg.Length < requiredParam.Length)
            return false;

        for (int i = 0; i < requiredParam.Length; i++)
        {
            if (arg[i] != requiredParam[i]) { return false; }
        }
        return true;
    }

    [Benchmark]
    [ArgumentsSource(nameof(TestInput))]
    public bool IsParam_Hardcoded(string arg)
    {
        if ((uint)arg.Length > 4)
            return arg[0] == 'l' && arg[1] == 'o' && arg[2] == 'g' && arg[3] == 'i' && arg[4] == 'n';
        return false;
    }
}

Results:


BenchmarkDotNet=v0.11.5, OS=Windows 10.0.18362
Intel Core i7-8700K CPU 3.70GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=3.0.100-preview6-012264
  [Host]     : .NET Core 3.0.0-preview6-27804-01 (CoreCLR 4.700.19.30373, CoreFX 4.700.19.30308), 64bit RyuJIT
  DefaultJob : .NET Core 3.0.0-preview6-27804-01 (CoreCLR 4.700.19.30373, CoreFX 4.700.19.30308), 64bit RyuJIT


| Method | arg | Mean | Error | StdDev | Ratio |
|-------------------- |--------------------- |----------:|----------:|----------:|------:|
| IsParam_StartsWith | a | 1.7075 ns | 0.0037 ns | 0.0033 ns | 1.00 |
| IsParam_StartsWith2 | a | 0.2274 ns | 0.0037 ns | 0.0034 ns | 0.13 |
| IsParam_Loop | a | 0.1188 ns | 0.0028 ns | 0.0026 ns | 0.07 |
| IsParam_Hardcoded | a | 0.2370 ns | 0.0042 ns | 0.0039 ns | 0.14 |
| | | | | | |
| IsParam_StartsWith | login | 1.1774 ns | 0.0046 ns | 0.0038 ns | 1.00 |
| IsParam_StartsWith2 | login | 0.2756 ns | 0.0035 ns | 0.0029 ns | 0.23 |
| IsParam_Loop | login | 2.4247 ns | 0.0005 ns | 0.0004 ns | 2.06 |
| IsParam_Hardcoded | login | 0.2204 ns | 0.0027 ns | 0.0026 ns | 0.19 |
| | | | | | |
| IsParam_StartsWith | login(...)tring [73] | 4.2283 ns | 0.0029 ns | 0.0026 ns | 1.00 |
| IsParam_StartsWith2 | login(...)tring [73] | 4.4491 ns | 0.0121 ns | 0.0113 ns | 1.05 |
| IsParam_Loop | login(...)tring [73] | 2.4207 ns | 0.0003 ns | 0.0003 ns | 0.57 |
| IsParam_Hardcoded | login(...)tring [73] | 0.2207 ns | 0.0028 ns | 0.0026 ns | 0.05 |
| | | | | | |
| IsParam_StartsWith | logiz | 4.1979 ns | 0.0055 ns | 0.0051 ns | 1.00 |
| IsParam_StartsWith2 | logiz | 0.2538 ns | 0.0002 ns | 0.0002 ns | 0.06 |
| IsParam_Loop | logiz | 3.0222 ns | 0.0125 ns | 0.0111 ns | 0.72 |
| IsParam_Hardcoded | logiz | 0.3341 ns | 0.0029 ns | 0.0026 ns | 0.08 |
| | | | | | |
| IsParam_StartsWith | zogin | 1.6130 ns | 0.0004 ns | 0.0004 ns | 1.00 |
| IsParam_StartsWith2 | zogin | 0.2526 ns | 0.0031 ns | 0.0027 ns | 0.16 |
| IsParam_Loop | zogin | 0.7524 ns | 0.0018 ns | 0.0016 ns | 0.47 |
| IsParam_Hardcoded | zogin | 0.2329 ns | 0.0053 ns | 0.0047 ns | 0.14 |

If you want to optimize startup time, I would recommend to make your code as simple as possible. Startup time tends to be dominated by what it takes to bring the code into memory and prepare for execution.

also, a loop inside a method disables QuickJit (if it's enabled) 馃檪

Ok, I think I've got enough to go on. Thanks for the responses!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

iCodeWebApps picture iCodeWebApps  路  3Comments

omajid picture omajid  路  3Comments

nalywa picture nalywa  路  3Comments

aggieben picture aggieben  路  3Comments

matty-hall picture matty-hall  路  3Comments