Почему Regex и StringBuilder медленнее удаляют пробелы?

Мы занимаемся интеграцией связи с внешним API. До сих пор это было немного головной болью из-за непоследовательного именования, плохой документации и ненадежных ответов/сообщений об ошибках.

Одна из вещей, с которой мы имеем дело, заключается в том, что определенные запросы, которые мы им отправляем, имеют ограничения на длину строк. Ничего новаторского, кроме любого запроса, который содержит любую строку, превышающую требования по длине, просто отклоняется и терпит неудачу.

Наше решение состояло в том, чтобы создать метод расширения для строки, который просто принимает максимальную длину и возвращает подстроку этой длины, начиная с индекса 0.

Я младший разработчик, и это моя первая работа, поэтому я знаю, что мои решения, возможно, не самые элегантные или эффективные. В любом случае, я указал на то, что с нашим текущим расширением мы могли бы в конечном итоге удалить соответствующую информацию, включив потенциально бесполезные пробелы, поскольку мы не обрезаем и не делаем ничего для проверки на наличие двойных пробелов и т. д. Мой руководитель сказал мне, что я могу не стесняться делать перегрузка расширения, которое позволит вам также выбрать удаление пробелов.

Я придумал 3 решения, которые полностью удаляют любые двойные пробелы. Я знаю, что метод Regex - единственный, который действительно удаляет все пробелы, тогда как два других удаляют любое вхождение двух пробелов подряд. Однако этот сайт будет использоваться исключительно в США, поэтому я не уверен, что дополнительное время регулярного выражения оправдано.

Мой главный интерес в публикации этого заключается в том, что мне интересно, может ли кто-нибудь объяснить, почему мой метод с использованием StringBuilder настолько неэффективен по сравнению с двумя другими, он даже медленнее, чем Regex, я ожидал, что он будет самым быстрым из трех. Любое понимание здесь ценится, а также намек на то, что может быть лучше, чем любой из тех, которые я придумал.

Вот мои три расширения:

    public static string SafeSubstringSomehowTheQuickest(this string stringToShorten, int maxLength)
    {
        if (stringToShorten?.Length < maxLength || string.IsNullOrWhiteSpace(stringToShorten)) return stringToShorten;

        stringToShorten = stringToShorten.Trim();
        int stringOriginalLength = stringToShorten.Length;
        int extraWhitespaceCount = 0;
        for (int i = 0; i < stringOriginalLength - extraWhitespaceCount; i++)
        {
            int stringLengthBeforeReplace = stringToShorten.Length;
            stringToShorten = stringToShorten.Replace("  ", " ");
            if(stringLengthBeforeReplace < stringToShorten.Length) { extraWhitespaceCount += stringToShorten.Length - stringLengthBeforeReplace; } 
        }

        return stringToShorten.Length > maxLength ? stringToShorten.Substring(0, maxLength) : stringToShorten;
    }

    public static string SafeSubstringWithRegex(this string stringToShorten, int maxLength)
    {
        if (stringToShorten?.Length < maxLength || string.IsNullOrWhiteSpace(stringToShorten)) return stringToShorten;
        stringToShorten = System.Text.RegularExpressions.Regex.Replace(stringToShorten, @"\s{2,}", " ").Trim();

        return stringToShorten.Length > maxLength ? stringToShorten.Substring(0, maxLength) : stringToShorten;
    }

    public static string SafeSubstringFromBuilder(this string stringToShorten, int maxLength)
    {
        if (stringToShorten?.Length < maxLength || string.IsNullOrWhiteSpace(stringToShorten)) return stringToShorten;

        StringBuilder bob = new StringBuilder();
        bool lastCharWasWhitespace = false;

        foreach (char c in stringToShorten)
        {
            if (c == ' ' && !lastCharWasWhitespace) { bob.Append(c); }
            lastCharWasWhitespace = c == ' ';
            if (!lastCharWasWhitespace) { bob.Append(c); }
        }
        stringToShorten = bob.ToString().Trim();

        return stringToShorten.Length < maxLength ? stringToShorten : stringToShorten.Substring(0, maxLength);
    }

Вот мой быстрый тест, который я использую для сравнения времени, необходимого для запуска каждого расширения:

    static void Main(string[] args)
    {
        var stopwatch = new System.Diagnostics.Stopwatch();

        string test =
            "   foo bar   foobar           f    oo        bar foobar      foofoo                                            " +
            "barbar    foo b  ar                                                                                       " +
            "   foo bar   foobar           f    oo        bar foobar      foofoo                                            " +
            "barbar    foo b  ar                                                                                       " +
            "   foo bar   foobar           f    oo        bar foobar      foofoo                                            " +
            "barbar    foo b  ar                                                                                       " +
            "   foo bar   foobar           f    oo        bar foobar      foofoo                                            " +
            "barbar    foo b  ar                                                                                       " +
            "   foo bar   foobar           f    oo        bar foobar      foofoo                                            " +
            "barbar    foo b  ar                                                                                       " +
            "   foo bar   foobar           f    oo        bar foobar      foofoo                                            " +
            "barbar    foo b  ar                                                                                       " +
            "   foo bar   foobar           f    oo        bar foobar      foofoo                                            " +
            "barbar    foo b  ar                                                                                       " +
            "   foo bar   foobar           f    oo        bar foobar      foofoo                                            " +
            "barbar    foo b  ar                                                                                       " +
            "   foo bar   foobar           f    oo        bar foobar      foofoo                                            " +
            "barbar    foo b  ar                                                                                       " +
            "   foo bar   foobar           f    oo        bar foobar      foofoo                                            " +
            "barbar    foo b  ar                                                                                       " +
            "   foo bar   foobar           f    oo        bar foobar      foofoo                                            " +
            "barbar    foo b  ar                                                                                       " +
            "   foo bar   foobar           f    oo        bar foobar      foofoo                                            " +
            "barbar    foo b  ar                                                                                       " +
            "   foo bar   foobar           f    oo        bar foobar      foofoo                                            " +
            "barbar    foo b  ar                                                                                       ";

        int stringStartingLength = test.Length;
        int stringMaxLength = 30;

        stopwatch.Start();
        string somehowTheQuickestResult = test.SafeSubstringSomehowTheQuickest(stringMaxLength);
        stopwatch.Stop();
        var somehowTheQuickestResultTicks = stopwatch.ElapsedTicks;

        stopwatch.Start();
        string regexResult = test.SafeSubstringWithRegex(stringMaxLength);
        stopwatch.Stop();
        var regexResultTicks = stopwatch.ElapsedTicks;

        stopwatch.Start();
        string stringBuilderResult = test.SafeSubstringFromBuilder(stringMaxLength);
        stopwatch.Stop();
        var stringBuilderResultTicks = stopwatch.ElapsedTicks;
    }

Наконец, это результаты, тики немного меняются при каждом запуске, но разница между тремя методами довольно постоянна:

Все три возвращают одну и ту же строку: "foo bar foobar f oo bar foobar"

как-тоTheQuickestResult (метод 1): 12840 тактов

regexResult (метод 2): 14889 тактов

stringBuilderResult (метод 3): 15798 тактов


person WRP    schedule 28.07.2016    source источник
comment
Вероятно, было бы еще быстрее, если бы вы работали с массивом символов и перемещали непробельные символы.. для больших сравнений строк вы, вероятно, получили бы очень разные результаты с вашими текущими методами   -  person BugFinder    schedule 28.07.2016
comment
В качестве примечания, все методы будут возвращать строку с большим количеством пробелов без изменений, даже если она будет слишком длинной.   -  person GSerg    schedule 28.07.2016
comment
Для регулярного выражения вы, вероятно, захотите исключить время компиляции, создав его явный static readonly экземпляр с обязательным флаги. Для построителя строк вы, вероятно, захотите передать maxLength как capacity.   -  person GSerg    schedule 28.07.2016
comment
@GSerg не уверен, что ты имеешь в виду под своим первым комментарием? Если строка короче maxLength, я просто возвращаю ее, потому что в этом случае мне не нужно удалять пробелы. Цель удаления пробела заключается в том, что, когда строка превышает maxLength, можно передать как можно больше соответствующей информации с запросом. Поэтому я думаю, что первая и последняя строки в каждом методе решают указанную вами проблему. Что касается вашего второго комментария, спасибо за информацию, я рассмотрю оба варианта и проверю результаты!   -  person WRP    schedule 28.07.2016
comment
@WRP Если вы передадите строку, состоящую из тысячи пробелов и ничего больше, она будет возвращена без изменений. stringToShorten?.Length < maxLength будет false, string.IsNullOrWhiteSpace(stringToShorten) будет true, false || true будет true.   -  person GSerg    schedule 28.07.2016


Ответы (1)


Вы немного неправильно проводите бенчмаркинг.

Во-первых, вам нужно «разогреться» и позволить JIT выполнять свою работу. По сути, просто вызовите свои три метода и отбросьте результаты.

Далее, одиночная попытка не является репрезентативной. Попробуйте среднее (или среднее время) более 100 или более итераций.

В-третьих, вы неправильно используете Stopwatch. Start() после Stop() возобновляет интервальные измерения. Restart() это путь. С ним мои тесты показывают следующие результаты:

9569
314
58

Итак, StringBuilder способ на самом деле самый быстрый.

person Anton Gogolev    schedule 28.07.2016
comment
Отличный улов! Извините, это первый раз, когда я использовал секундомер. Собираюсь переделать это и получить лучшее среднее за несколько тысяч циклов. - person WRP; 28.07.2016