Outro dia, estava procurando estratégias para encontrar e extrair números em strings e cheguei a seguinte estratégia, como a mais popular, no Stackoverflow.
O código é simples (o que é bom). Entretanto, a Regex não compilada, junto com as alocações repetidas de strings pressionando o GC não parecem performáticas.
Resolvi fazer algumas soluções alternativas para comparação com a solução proposta. Foram elas:
- Regex compilados e parsing;
- Busca caractere-por-caractere por números, extração com substring e parsing;
- Busca caractere-por-caractere por números, extração com span e parsing;
Também resolvi comparar as performances para encontrar, extrair e converter números no início, no meio e no fim de strings.
using System; using System.Runtime.CompilerServices; using System.Text.RegularExpressions; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Running; [MemoryDiagnoser] public class Program { static void Main(string[] args) { BenchmarkRunner.Run<Program>(); } [Benchmark] [Arguments("42, starting with a number.")] [Arguments("With a number 42 in the middle.")] [Arguments("The secret number is 42.")] [Arguments("42")] public int ExtractIntUsingRegex(string input) { var number = Regex.Match(input, @"d+").Value; return int.Parse(number); } Regex numberExtractor; [GlobalSetup] public void GlobalSetup() { numberExtractor = new Regex(@"d+", RegexOptions.Compiled); } [Benchmark] [Arguments("42, starting with a number.")] [Arguments("With a number 42 in the middle.")] [Arguments("The secret number is 42.")] [Arguments("42")] public int ExtractIntUsingCompiledRegex(string input) { var number = numberExtractor.Match(input).Value; return int.Parse(number); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static (int Start, int Length) GetNumberPosition(string s) { var start = 0; for (int i = 0; i < s.Length; i++) { if (char.IsDigit(s[i])) { start = i; break; } } for (int i = start + 1; i < s.Length; i++) { if (!char.IsDigit(s[i])) { return (start, i - start); } } return (start, s.Length - start); } [Benchmark] [Arguments("42, starting with a number.")] [Arguments("With a number 42 in the middle.")] [Arguments("The secret number is 42.")] [Arguments("42")] public int ExtractIntUsingSubstring(string input) { var position = GetNumberPosition(input); return int.Parse(input.Substring(position.Start, position.Length)); } [Benchmark] [Arguments("42, starting with a number.")] [Arguments("With a number 42 in the middle.")] [Arguments("The secret number is 42.")] [Arguments("42")] public int ExtractIntUsingSpan(string input) { var position = GetNumberPosition(input); var numberSpan = input.AsSpan(position.Start, position.Length); return int.Parse(numberSpan); } }
Os resultados obtidos foram bem esclarecedores.
Como imaginava, a performance da estratégia popular no Stackoverflow se mostrou como a menos eficiente em todos os cenários testados. Utilizar uma Regex compilada melhorou a performance consideravelmente.
A solução utilizando Substring teve a melhor performance quando a string possuia apenas o número.
A solução utilizando Span foi consistentemente a mais eficiente em todos os demais cenários (mérito por não fazer alocações e não gerar coletas). Aliás, essa estratégia de “não alocação” é a grande responsável pela melhoria de performance que temos observado em .NET.
Sugestões para tornar esse código mais performático?