【问题标题】:Parse IDs from input string to long number using Span<T> in .Net使用 .Net 中的 Span<T> 将 ID 从输入字符串解析为长数字
【发布时间】:2025-12-10 14:50:01
【问题描述】:

我正在尝试从包含不同数字的单个字符串中解析不同的 ID(长),并且我需要最小化内存分配以提高性能。

下面是使用 Split 提取 ID 的代码,但我发现我可以使用 AsSpan 和 Splice 来做同样的事情,而无需分配内存。但不幸的是,即使在网上查找后,我对这个 Span 概念也不是很熟悉。谁能告诉我如何实现这一目标?

如下所示,输入字符串有 3 个不同的 ID,但我只需要其中 2 个并解析为 long 类型。

        string[] machineIdPart;
        string[] employeeIdPart;
        long machineId;            
        long employeeId;

        //Input String
        var description = "machineId: 276744, engineId: 59440, employeeId: 4619825";

        Console.Out.Write(description);
        Console.Out.WriteLine();
        var infoList = description.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
        foreach (var info in infoList)
        {
            if (info.TrimStart().StartsWith("machineId", StringComparison.OrdinalIgnoreCase))
            {
                machineIdPart = info.Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries);

                if (machineIdPart.Count() > 1)
                {
                    long.TryParse(machineIdPart[1].Trim(), out machineId);
                }                                     
            }

            if (info.TrimStart().StartsWith("employeeId", StringComparison.OrdinalIgnoreCase))
            {
                employeeIdPart = info.Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries);

                if (employeeIdPart.Count() > 1)
                {
                    long.TryParse(employeeIdPart[1].Trim(), out employeeId);
                }
            }
        }   

我想修改此代码以最小化内存分配,因为此方法将非常频繁地运行。

【问题讨论】:

  • 您现在是否发现任何性能问题?
  • 您会发现从strings 解析longs 也不会太快。考虑以老式方式执行此操作,跳过寻找, 的字符串,并使用x = x * 10 + c - '0' 解析自己,避免所有字符串操作一次性完成。如果这仍然是一个性能问题,也可以从Span 完成,但这是在另一个层面上(这意味着以更有效的方式提取description)。然而,根据马特——首先确定这是一个实际的瓶颈,即使你知道它会“非常频繁地”运行。优化错误的东西是浪费时间。
  • @Matt.G 我会说是的。上面的代码将插入到部分巨大的处理代码中。进行上述更改后,它增加了性能开销比较没有。这并不重要,但仍希望进一步改进以尽量减少影响。
  • @JeroenMostert 是的,请参阅我上面的评论。如果您不介意的话,您能否详细说明一下使用 Span 的“老式”方式??
  • 通过跟踪您的位置(索引)而不是实际创建新字符串来编写一个简单的解析器。对于一个字符串,这个简单的重复调用.IndexOf 就可以了,尽管您也可以编写一个小状态机(switch (description[i]) { case ':': state = State.ParsingNumber; ++i; break; })。主要思想是保留字符串,而不是创建新字符串。在不分配任何内容的情况下解析这个字符串可以通过多种方式完成;您甚至不需要为此深入了解Span。 (不,我现在懒得写解析器了,抱歉。:-P)

标签: c# .net parsing memory


【解决方案1】:

此解决方案适用于 .NET Core 2.2。它在ReadOnlySpan&lt;char&gt; (SplitNext) 上使用了免分配扩展方法。

public class Program {
    public void MyAnswer() {
        long machineId = 0;
        long employeeId = 0;

        var description = "machineId: 276744, engineId: 59440, employeeId: 4619825";

        var span = description.AsSpan();
        while (span.Length > 0) {
            var entry = span.SplitNext(',');

            var key = entry.SplitNext(':').TrimStart(' ');
            var value = entry.TrimStart(' ');

            if (key.Equals("machineId", StringComparison.Ordinal)) {
                long.TryParse(value, out machineId);
            }
            if (key.Equals("employeeId", StringComparison.Ordinal)) {
                long.TryParse(value, out employeeId);
            }
        }
    }
}

public static class Extensions {
    public static ReadOnlySpan<char> SplitNext(this ref ReadOnlySpan<char> span, char seperator) {
        int pos = span.IndexOf(seperator);
        if (pos > -1) {
            var part = span.Slice(0, pos);
            span = span.Slice(pos + 1);
            return part;
        } else {
            var part = span;
            span = span.Slice(span.Length);
            return part;
        }
    }
}

我通过 BenchmarkDotnet 比较了您的原始代码、现有的 answer 和我的答案。它表明该解决方案确实是免分配的,并且比原始版本执行得更快:

BenchmarkDotNet=v0.11.4, OS=Windows 10.0.17763.316 (1809/October2018Update/Redstone5)
Intel Core i5-2500K CPU 3.30GHz (Sandy Bridge), 1 CPU, 4 logical and 4 physical cores
.NET Core SDK=2.2.104
  [Host]     : .NET Core 2.2.2 (CoreCLR 4.6.27317.07, CoreFX 4.6.27318.02), 64bit RyuJIT
  DefaultJob : .NET Core 2.2.2 (CoreCLR 4.6.27317.07, CoreFX 4.6.27318.02), 64bit RyuJIT

|   Method |       Mean |     Error |    StdDev | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op |
|--------- |-----------:|----------:|----------:|------------:|------------:|------------:|--------------------:|
| Original | 1,164.1 ns | 11.606 ns | 10.289 ns |      0.2937 |           - |           - |               928 B |
|   Answer |   460.5 ns |  4.527 ns |  4.234 ns |           - |           - |           - |                   - |
| MyAnswer |   445.7 ns |  2.578 ns |  2.412 ns |           - |           - |           - |                   - |

除了字符串处理的优化,实际的解析功能也可以进行优化。这将是一个更快的长解析器:

    public static long LongParseFast(ReadOnlySpan<char> value) {
        long result = 0;
        for (int i = 0; i < value.Length; i++) {
            result = 10 * result + (value[i] - 48);
        }
        return result;
    }

如果在我的示例中使用,它会在我的基准测试中将性能提高一倍至216.0 ns。当然,这个函数不能处理负数、逗号、点和其他语言环境的东西。但是,如果您对此感到满意,那么这可能是您能获得的最快速度。

【讨论】:

  • 谢谢!这确实是我基准测试中最快的。顺便说一句,您是否介意您能说出如何使用 Benchmarkdotnet 生成分配的内存列?再次感谢!
  • 当然。只需将 [MemoryDiagnoser] 属性添加到您要进行基准测试的类。这是我的完整基准代码:gist.github.com/discostu105/69103b549a392182fbfa2b09d11cee2f
【解决方案2】:

解决方案将变得比当前复杂一些,但不会有更多的字符串分配。适用于 .NET Core 2.2

long machineId = 0;
long employeeId = 0;

var description = "machineId: 276744, engineId: 59440, employeeId: 4619825";

ReadOnlySpan<char> descriptionSpan = description.AsSpan();

var nameValueBlockStartIndex = 0;
while(nameValueBlockStartIndex < description.Length)
{
    var blockEndIndex = description.IndexOf(',', nameValueBlockStartIndex);
    if (blockEndIndex == -1)
    {
        blockEndIndex = description.Length;
    }

    var namePartEndIndex = description.IndexOf(':', nameValueBlockStartIndex);
    var namePartLength = namePartEndIndex - nameValueBlockStartIndex;
    var namePart = descriptionSpan.Slice(nameValueBlockStartIndex, namePartLength);

    var valuePartStartIndex = namePartEndIndex + 1;
    var valuePartLength = blockEndIndex - valuePartStartIndex + 1;
    var valuePart = descriptionSpan.Slice(valuePartStartIndex, valuePartLength - 1);

    while(namePart[0] == ' ')
    {
        namePart = namePart.Slice(1);
    }

    if (namePart.Equals("machineId", StringComparison.OrdinalIgnoreCase))
    {
        Int64.TryParse(valuePart, out machineId);
    }
    else if (namePart.Equals("employeeId", StringComparison.OrdinalIgnoreCase))
    {
        Int64.TryParse(valuePart, out employeeId);
    }

    nameValueBlockStartIndex = blockEndIndex + 1;
}

【讨论】: