Pull to refresh

Comments 36

Не увидел, почему-то, самого короткого варианта:

text.Split().MaxBy(word => word.Length)

Не проводил бенчмарков, но на вскидку сказал бы что это будет работать шустрее, чем Aggreagate() и OrderByDescending().First(), да и читается лучше

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

вот тесты для кода

string word = str.Split(' ', ',', '.').MaxBy(word => word.Length);
| Method   | Length | Mean          | Allocated |
|----------|--------|---------------|-----------|
| Jump     | 1      |      61.76 ns |      40 B |
| Agregate | 1      |     312.72 ns |     920 B |
| MaxBy    | 1      |     312.18 ns |     920 B |
|----------|--------|---------------|-----------|
| Jump     | 100    |   3,525.11 ns |      40 B |
| Agregate | 100    |  28,048.78 ns |   82496 B |
| MaxBy    | 100    |  26,584.78 ns |   82496 B |
|----------|--------|---------------|-----------|
| Jump     | 1000   |  38,580.05 ns |      40 B |
| Agregate | 1000   | 831,457.50 ns |  824133 B |
| MaxBy    | 1000   | 809,927.50 ns |  824133 B |

Не увидел, почему-то, самого короткого варианта

К сожалению я не настолько продвинут в C#, поэтому многие вещи могу не знать.

У самурая нет императивного подхода, только декларатив..!

Если вы хотите быстрое решение, то всё таки в первую очередь надо знать и использовать стандартные методы. А именно использование IndexOf метода должно поднять производительность относительно поиска в циклах как у вас. Особенно если вы будете использовать .Net 8 варианты этого метода.

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

почитал спеки на MSDN и что-то я не понял как именно IndexOf должен увеличить производительность. Я конечно позже попробую реализовать через него, но пока я не вижу разницы с IsAsciiLetter например.

Если не трудно, напишите пример реализации с использованием IndexOf, так как я не смог придумать как это сделать. Например с IndexOfAny если задать массив char[] то вроде и можно поработать, но это же будет O(m*n) где m - длина массива char[], что точно не будет быстрым решением. А как фильтровать хотя бы латинские символы с помощью IndexOf я пока не могу понять, но и там это получится просто вариант IsLetter.

Да в .Net 7 нет вариантов которые ускорят обычный алгоритм, согласен.

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

Алгоритм с прыжками этими методами уже не ускорить к сожалению.

Вообще я бы в проде использовал обычный RegEx. Если производительность критична, то с компиляцией в код... Получилась бы почти производительность обычного поиска, и возможность быстрого изменения, если бизнес правила изменятся...

    public static int FastTwoLoops(string str)
    {
        var maxLength = 0;

        for (int startIndex = 0, endIndex = 0; endIndex < str.Length;
            startIndex = ++endIndex, 
            endIndex += maxLength)
        {
            if (!char.IsAsciiLetter(str[endIndex]))
                continue;

            for (var breakIndex = endIndex - 1; breakIndex >= startIndex; breakIndex--)
            {
                if (!char.IsAsciiLetter(str[breakIndex]))
                {
                    endIndex = breakIndex;
                    goto Continue;
                }
            }

            while (++endIndex < str.Length && char.IsAsciiLetter(str[endIndex])) ;
            maxLength = endIndex - startIndex;

        Continue:
            ;
        }

        return maxLength;
    }

Мой вариант алгоритма с прыжками, у вас как то путано показалось написано...

у вас как то путано показалось написано

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

У вас интересная реализация получилась, буду изучать, так как непонятно немного )))

| Method       | Length | Mean         |
|--------------|--------|--------------|
| Jump         | 1      |     46.07 ns |
| JumpNew      | 1      |     50.30 ns |
| FastTwoLoops | 1      |     44.20 ns |
|--------------|--------|--------------|
| Jump         | 100    |  2,464.11 ns |
| JumpNew      | 100    |  2,706.07 ns |
| FastTwoLoops | 100    |  2,628.97 ns |
|--------------|--------|--------------|
| Jump         | 1000   | 24,759.34 ns |
| JumpNew      | 1000   | 28,123.92 ns |
| FastTwoLoops | 1000   | 27,054.33 ns |

Небольшая оптимизация

    public static int TwoLoops1Jump(string str)
    {
        var maxLength = 0;

        for (int endIndex = 0, len = str.Length; endIndex < len; endIndex += maxLength + 1)
        {
            if (!char.IsAsciiLetter(str[endIndex]))
                continue;

            // Can IndexOf be faster here?
            var startIndex = endIndex - maxLength;
            for (var breakIndex = endIndex - 1; breakIndex > startIndex; breakIndex--)
            {
                if (!char.IsAsciiLetter(str[breakIndex]))
                {
                    endIndex = breakIndex;
                    goto Continue;
                }
            }
            if (!char.IsAsciiLetter(str[startIndex]))
                startIndex++;

            // Can IndexOf be faster here?
            while (++endIndex < len && char.IsAsciiLetter(str[endIndex])) ;

            maxLength = endIndex - startIndex;

        Continue:
            ;
        }

        return maxLength;
    }

Небольшая оптимизация

| Method          | Length | Mean         |
|-----------------|--------|--------------|
| Jump            | 1      |     43.56 ns |
| JumpNew         | 1      |     48.33 ns |
| FastTwoLoops    | 1      |     42.81 ns |
| FastTwoLoopsNew | 1      |     47.74 ns |
|-----------------|--------|--------------|
| Jump            | 100    |  2,405.17 ns |
| JumpNew         | 100    |  2,842.87 ns |
| FastTwoLoops    | 100    |  2,561.28 ns |
| FastTwoLoopsNew | 100    |  3,324.67 ns |
|-----------------|--------|--------------|
| Jump            | 1000   | 23,956.23 ns |
| JumpNew         | 1000   | 27,156.41 ns |
| FastTwoLoops    | 1000   | 26,018.34 ns |
| FastTwoLoopsNew | 1000   | 23,978.35 ns |

Попробуйте переделать на do-while, эта реализация цикла сильно быстрее работает.

Насчёт циклов вы не правы. Любой цикл компилируется в условные переходы. В дотНет достаточно неплохой компилятор циклов, хоть и не идеальный.

Вы нашли у себя неоптимальности в главном цикле? Там у вас шаг по одному символу, я поэтому дальше смотреть код не стал, там явно ошибка...

Для отладки разделите слова в тестах большим количеством пробелов, например 100, в этом случае ваша реализация будет медленно работать, у вас там нет прыжков на длину слова. Думал заметите сами.

Насчёт циклов вы не правы

Я не настолько продвинут в C# чтобы спорить, я только руководствуюсь экспериментальными значениями и do-while работает быстрее чем for/while.

Вы нашли у себя неоптимальности в главном цикле? Там у вас шаг по одному символу, я поэтому дальше смотреть код не стал, там явно ошибка...

Вроде вот эта строка этим занимается, вы об этом? Я как раз тут учитывал прыжки по "пробелам"

i += maxlen;

Это как раз в 3 апдейте в статье появилось.

В главном цикле нет прыжков

for (int i = 0; i < len - maxlen; i++)

В главном цикле i меняется на maxlen+1, происходит это двумя итераторами

for (int i = 0; i < len - maxlen; i++) // тут +1
{
    i += maxlen; // тут на maxlen

https://github.com/AlexRadch/dotNetMisc HabrMisc\LongestWordLength\LongestWordLength.sln

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

| TwoLoops      | 100000     | 3,674.812 μs | 71.7678 μs | 95.8080 μs | 3,645.900 μs |     736 B |
| TwoLoops1Jump | 100000     | 1,283.652 μs | 29.3687 μs | 83.3142 μs | 1,278.200 μs |     736 B |
| Jump          | 100000     | 3,133.623 μs | 48.3515 μs | 40.3757 μs | 3,129.000 μs |     736 B |
| JumpNew       | 100000     | 3,255.214 μs | 61.6167 μs | 54.6216 μs | 3,239.050 μs |     736 B |

вы взяли старые версии, свежее в UPD3

Это версия из "начала" и она у вас в проекте:

int maxLen = 0, //maxIndex = -1,
    len = str.Length;
for (int i = 0; i < len - maxLen; i++)
{
     if (char.IsAsciiLetter(str[i]))
     {
         int index = i;
         i += maxLen;

Это версия из UPD3:

int maxlen = 0, maxindex = -1, len = str.Length;
for (int i = 0; i < len - maxlen; i++)
{
    int index = i;
    i += maxlen;

так же легко видно по замене на do-while

int k = maxLen > 0 ? 1 : 0;
do i -= k;
while (i > index && char.IsAsciiLetter(str[i]));

а у вас старая версия

у меня ваш бенч не хочет проходить при размере в 1000, а ошибку я сам не найду.

Да обновил до версии UPD3, видать невнимательно скопировал.
Этот вариант не проходит тесты, к сожалению. Jump return 15 should 14

Он возращает что самое длинное слово имеет длину 15, по моим расчётам должна быть 14.

LongestWord = Regex.Split(Words, "[^a-zA-Z]+").Max(word => word.Length);

Я использую такую регулярку для проверки.

я немного переделал основной цикл, пока тестирую, там под вашу идею, сразу в цикле i += maxlen + 1, это позволяет убрать из первого if проверку на выход за пределы строки, так как такая проверка уже проводится в for.

    public string Jump()
    {
        int maxlen = 0, maxindex = -1, len = str.Length;
        for (int i = 0; i < len - maxlen; i += maxlen + 1)
        {
            int index = i - maxlen;
            //i += maxlen;
            if (char.IsAsciiLetter(str[i]))
            {

По ошибке сложно сказать, у меня с разными строками выполняется сразу несколько реализаций и все дают одинаковый результат.

Тест на такой строке:

"I  . . . . . . . . . . . . . . have . . . . . . . . . . .  a . . . . . . . .  friend . . . . . . . .  Whose . . . . . . . . name . . . . . . is... And . . . . . . . we . . . . . . . have . . . . . . . fun . . . . . . together. . . . . . .  We  . . . . . . laugh  . . . . . . and  . . . . . . play  . . . . . And  . . . . . . sing  . . . . . all  . . . . . day . . . . . . In . . . . . .any . . . . .  kind . . . . . of  . . . . . weather. . . . . . ."
| Method        | Length | Mean         |
|---------------|--------|--------------|
| Jump          | 1      |     98.17 ns |
| JumpNew       | 1      |     97.57 ns |
| TwoLoops      | 1      |    113.38 ns |
| TwoLoops1Jump | 1      |     88.61 ns |
|---------------|--------|--------------|
| Jump          | 100    |  4,847.12 ns |
| JumpNew       | 100    |  5,204.50 ns |
| TwoLoops      | 100    |  5,322.67 ns |
| TwoLoops1Jump | 100    |  5,534.59 ns |
|---------------|--------|--------------|
| Jump          | 1000   | 56,442.69 ns |
| JumpNew       | 1000   | 53,372.84 ns |
| TwoLoops      | 1000   | 53,534.55 ns |
| TwoLoops1Jump | 1000   | 51,109.82 ns |

Можете еще одну статью посмотреть, там автор вроде IndexOf использует, возможно это тот вариант что вы хотели проверить.

Полностью поддерживаю автора, по концепции, не люблю я писать сложные алгоритмы которые тяжело поддерживать при изменении требований.
Лучше написать простую и достаточно эффективную реализацию, которая пусть даже в 5-10 раз медленнее, но которую легко менять под изменения.
Грубо говоря я бы сделал алгоритм который бы быстро и лениво возвращал следующее слово без учёта всяких других требований.
А вот вокруг него уже можно натянуть какие хочешь требования бизнеса. Да конкретно для поиска самого длинного слова он будет примерно в 3 раза медленнее, но если требования будут меняться, добавляться, его поддерживать буде раз в 100 быстрее. А это важнее...

Насчёт конкретной реализации я думаю что у автора не самая удачная. Можно лучше и проще.

У вас во втором сниппете кода баг. Сможете найти?

// SkipWord
int maxlen = 1;
int maxindex = -1;
int temp_maxlen = 0;
int temp_maxindex = 0;
int i = 0;
while (i < str.Length)
{
    if ((str[i] >= 'a' && str[i] <= 'z') || (str[i] >= 'A' && str[i] <= 'Z'))
    {
        temp_maxlen = 0;
        temp_maxindex = i;
        do
        {
            temp_maxlen++;
            i++;
        }
        while (((str[i] >= 'a' && str[i] <= 'z') || (str[i] >= 'A' && str[i] <= 'Z')) && i < str.Length);
        if (temp_maxlen > maxlen)
        {
            maxlen = temp_maxlen;
            maxindex = temp_maxindex;
        }
    }
    i++;
}
Console.WriteLine("Длина = {0}, слово = {1}", maxlen, str.Substring(maxindex, maxlen));

  1. О багах знаю, нашел сильно после выхода статьи, исправлять не стал так как из головы уже всё выветрилось, а заново изучать код, быстро вникать в тему - я не способен. Поэтому просто не стал трогать.

  2. Сниппет — это блок с краткой информацией о странице сайта, содержащий ссылку на документ, который отображается в результатах поиска.

    Основная задача сниппета — дать пользователю первоначальное представление о содержимом страницы и показать, насколько страница соответствует поисковому запросу, не позволяя при этом переходить по ссылке на саму страницу.

    В сниппете могут отображаться:

    • фавикон;

    • заголовок и описание;

    • адрес страницы;

    • быстрые ссылки;

    • контактная информация;

    • метки (если у сайта есть знаки);

    • рейтинг организации.

Что вы подразумеваете под сниппетом в контексте статьи?

  1. Если что я не программист даже в ближайшем приближении, так что копание в моих статьях с целью найти ошибку и таким образом компенсировать недостатки вашей статьи - плохая затея. Примите как константу что я тупой и глупый говнокодер.

О багах знаю, нашел сильно после выхода статьи, исправлять не стал так как из головы уже всё выветрилось, а заново изучать код, быстро вникать в тему - я не способен. Поэтому просто не стал трогать.

Получается, если я захочу воспроизвести ваши результаты на своем компьютере и измерить время работы у себя, у меня не получится это сделать? В чем смысл тогда вообще код приводить, если вы знаете, что он работать не будет?

Сниппет — это блок с краткой информацией о странице сайта, содержащий ссылку на документ, который отображается в результатах поиска.

Сниппет - это просто англицизм от англ. snippet - фрагмент, отрывок. Тем более, смысл был из контекста понятен.

Получается, если я захочу воспроизвести ваши результаты на своем компьютере и измерить время работы у себя, у меня не получится это сделать?

се-ля-ви, 80% кода в интернете имеет такие качества. Но вы так пишете словно у меня вообще всё не работает.

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

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

Сниппет - это просто англицизм от англ. snippet - фрагмент, отрывок. Тем более, смысл был из контекста понятен.

Я не знаю английский, поэтому аналогию я попробовал взять из поисковика, который вам привёл, там что-то про сайты, что не вписывалось в тему.

Имея на руках две реализации можем посмотреть как они себя поведут в бенчмарке (Length - кратное увеличение размера строки, чтобы смотреть как ведет себя реализация на больших данных)

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

Если я верно понял, вы просто много раз повторяете исходную строку? В таком случае, это не совсем честный бенчмарк, т.к. максимум достигается почти сразу (на первом же вхождении исходной строки в длинную). Интереснее было бы посмотреть на тест, в котором максимум постоянно растет.

Из статьи не совсем ясно, что такое "большие данные"

10 байт - маленькие данные, 10 гигабайт - большие данные

Нет ни слова о том, как вы их на самом деле генерируете

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

а это может существенно влиять на бенчмарки

может, но я тестировал и на цельном TXT, на романе Жюля Верна и результаты не менялись, так что это опять не важно для статьи.

Если я верно понял, вы просто много раз повторяете исходную строку? В таком случае, это не совсем честный бенчмарк, т.к. максимум достигается почти сразу (на первом же вхождении исходной строки в длинную). Интереснее было бы посмотреть на тест, в котором максимум постоянно растет.

Если вы внимательно читали статью, то заметили что она о другом. На экстремумах я всё так же тестировал, флуктуации есть, но не принципиальные. Например в упомянутом тексте Верна максимальное слово находится на отметке кажется в 240000 знаков, а весь файл 840000, учитывая что вероятность появления длинного слова в начале такая же кк и в конце, то изучение этого может быть чем-то самостоятельным, но не для этой статьи, она о другом.

Sign up to leave a comment.

Articles