Почему сравнение строк без учетом регистра менее производительно, чем с учетом?

150
04 июля 2021, 20:30

Нашел вот такой тест:

И собственно возник вопрос:

Почему все именно так?

На мой взгляд, разница не должна быть на столько ощутимой.

Answer 1

Может немного неожиданный ответ, но:

Сравнение строк без учетом регистра менее производительно, чем с учетом регистра потому, что при сравнении без учета регистра нужно применять больше правил при сравнении каждого символа.

В случае Windows в исходники нативного API заглянуть не получится, но .net core, насколько я понял, использует для сравнения ICU. Код самого сравнения - там проход по строке с применением правил для каждого символа + пара частных случаев.

Важно то, что метод сравнения строк нельзя выбирать по результатам бенчмарка. Это разные методы, и они дают разный результат на одних и тех же входных данных. Это как сравнивать сложение, деление и интегрирование. Стоит использовать то, что вам нужно. А не использовать всегда Ordinal.

К результатам конкретного бенчмарка, использованного в вопросе стоит относится с сомнением, т.к. автор бенчмарка допустил сразу все возможные ошибки

  • мерял время через DateTime.Now
  • не привязал тест к конкретной локали
  • не учитывал результаты сравнения (оптимизатор потенциально выбрасывает такие тесты в Release)
  • использовал данные, на которых некоторые варианты работают заведомо медленнее
  • индексировал циклы с 1.
Answer 2

Провёл бенчмарк с помощью BenchmarkDotnet и получил следующие результаты:


    [CoreJob, CoreRtJob]
    [RPlotExporter, RankColumn, MemoryDiagnoser]
    public class StringEqualityBenchmark
    {
        [Params(10, 100, 1000, 10_000, 100000)] // размер строки в байтах.
        public int N;
        private string firstString;
        private string secondString;
        [GlobalSetup]
        public void Setup()
        {
            {
                var bytes = new byte[N];
                new Random(42).NextBytes(bytes);
                firstString = Encoding.UTF8.GetString(bytes);
            }
            {
                var bytes = new byte[N];
                new Random(42).NextBytes(bytes);
                secondString = Encoding.UTF8.GetString(bytes);
            }
        }
        [Benchmark]
        public bool SimpleEquals() => firstString == secondString;
        [Benchmark]
        public bool ObjectEquals() => object.Equals(firstString, secondString);
        [Benchmark]
        public bool ReferenceEquals() => object.ReferenceEquals(firstString, secondString);
        [Benchmark]
        public bool ToLowerEquals() => firstString.ToLower() == secondString.ToLower();
        [Benchmark]
        public bool ToUpperEquals() => firstString.ToUpper() == secondString.ToUpper();
        [Benchmark]
        public bool InvariantCultureIgnoreCaseEquals() => string.Equals(firstString, secondString, StringComparison.InvariantCultureIgnoreCase);
        [Benchmark]
        public bool InvariantCultureEquals() => string.Equals(firstString, secondString, StringComparison.InvariantCulture);
        [Benchmark]
        public bool OrdinalIgnoreCaseEquals() => string.Equals(firstString, secondString, StringComparison.OrdinalIgnoreCase);
        [Benchmark]
        public bool OrdinalEquals() => string.Equals(firstString, secondString, StringComparison.Ordinal);
        [Benchmark]
        public int StringCompare() => string.Compare(firstString, secondString);
        [Benchmark]
        public int StringOrdinalCompare() => string.CompareOrdinal(firstString, secondString);
    }

Из этого всего следует, что самое быстрое сравнение - естественно, по равенству ссылок, добавил в бенчмарк для наглядности. Использование ToLower и ToUpper почти не отличаются по производительности, где-то одно быстрее, где-то другое. Но оба плохи тем, что создают копию строки, что может быть катастрофично если вызывается часто и/или для больших строк.

Самым лучшим вариантом является использование ordinal (согласно предложению от MS)

Игнорирование кейса это дополнительные условия проверки, поэтому она выполняется дольше, но не требует дополнительной памяти, как можно увидеть из результатов бенчмарка.

Для того чтобы понять, что конкретно происходит при сравнении строк, нужно найти исходники для: ComNlsInfo::InternalCompareStringOrdinalIgnoreCase или ComNlsInfo::InternalCompareString т.к. происходит вызов нативного апи

READ ALSO
Как правильно оформить select запрос последней записи столбца

Как правильно оформить select запрос последней записи столбца

Как правильно оформить select-запрос в SQL, который берет только последнею запись со столбца finished?

82
Последовательность отношений в view

Последовательность отношений в view

Создаю форум на laravel, использую стандартный набор (Eloquent, Blade) Задача такова: Есть разделы, у разделов категории, у категорий темы, у тем сообщенияНа...

93
вызов и вывод метода с свойством echo в ООП

вызов и вывод метода с свойством echo в ООП

Есть такая конструкция кода

112