1. C# / Говнокод #17470

    +91

    1. 1
    2. 2
    3. 3
    4. 4
    5. 5
    private static int NumberOfLeadingSpaces(string str)
    {
        str = str.TrimEnd();
        return str.Length - str.Trim().Length;
    }

    Из моего проекта. Так я писал код 1.5 год назад.
    Вместо того, чтобы пройтись циклом с начала строки, пока не встретиться символ, не являющийся пробелом.

    Запостил: Janycz, 18 Января 2015

    Комментарии (44) RSS

    • >Вместо того, чтобы пройтись циклом с начала строки, пока не встретиться символ, не являющийся пробелом.
      Но... зачем? Типа только потому, что так быстрее?
      Ответить
      • > Типа только потому, что так быстрее?
        И логику лучше видно, чем в этой фигне с тримами. На самом деле там надо бы какую-нибудь функцию для поиска "первого не пробела", но я шарп плохо знаю, поэтому название не скажу.
        Ответить
        • На стековерфловочке вот так предложили:
          int x = testString.TakeWhile(char.IsWhiteSpace).Count();
          Ответить
        • class Program
          {
              private static readonly Stopwatch stopwatch = new Stopwatch();
          
              private static int GetNumberOfLeadingSpacesViaTrim(string source)
              {
                  source = source.TrimEnd();
                  return source.Length - source.Trim().Length;
              }
          
              private static int GetNumberOfLeadingSpacesViaBreakingFor(string source)
              {
                  for (var i = 0; i < source.Length; i++)
                      if (source[i] != ' ') return i;
          
                  return source.Length;
              }
          
              private static void Main()
              {
                  var text = "   bbbaka!";
                  int spacesCount;
          
                  stopwatch.Start();
                  spacesCount = GetNumberOfLeadingSpacesViaTrim(text); // 00:00:00.0002045
                  stopwatch.Stop();
          
                  Debug.WriteLine("Счёт через String.Trim() и String.Length занял {0}, результат: {1}", stopwatch.Elapsed, spacesCount);
          
                  stopwatch.Reset();
          
                  stopwatch.Start();
                  spacesCount = GetNumberOfLeadingSpacesViaBreakingFor(text); // 00:00:00.0001926
                  stopwatch.Stop();
          
                  Debug.WriteLine("Счёт через цикл занял {0}, результат: {1}", stopwatch.Elapsed, spacesCount);
              }
          }


          Чё-то не сильно лучше видно. Да и не особо быстрее, как оказалось. Ну да ладно.
          Ответить
          • > не сильно лучше видно
            Да почему - ищем первый не пробел и возвращаем его позицию. А вот из кода ОП'а это хер выведешь, имхо.

            P.S. А у версии с Linq какая производительность?
            P.P.S. Кто ж так бенчит? Надо хотя бы 1000 раз в цикле...
            Ответить
            • >ищем первый не пробел и возвращаем его позицию
              Ну, как бы, понятно то, что NumberOfLeadingSpaces и "позиция первого непробела" – это одно и то же. Но всё равно, меня как-то напрягает такая трактовка, не знаю почему (хотя знаю, но мне стыдно в этом признаться).

              >А у версии с Linq какая производительность?
              Не знаю, не тестила. Смею предположить, что медленнее. Надо попробовать.
              Ответить
              • Ну вот интересно посмотреть...
                Ответить
                • В общем, вот что получилось на 9000 итераций.

                  Счёт через String.Trim() и String.Length в среднем занял 0,000193833333333321
                  Счёт через цикл в среднем занял 0,000176633333333322
                  Счёт через LINQ в среднем занял 0,000504066666666807
                  Ответить
              • > меня как-то напрягает такая трактовка
                А разность длин тримнутой и нетримнутой строк не напрягает? ;)

                Кстати, цикл не эквивалентен - табы не режет.
                Ответить
                • >Кстати, цикл не эквивалентен - табы не режет.
                  Окей, поправила. Замеры выше – с учётом фикса.
                  Ответить
          • Во втором случае есть неприятное обстоятельство: в каждом цикле делается две проверки, и, гарантировано, одна из них не нужна. Но если предположить, что в строках чаще таки будут не-пробелы, чем их не будет, то можно соптимизировать до одной проверки и исключения в патологическом случае.
            Ответить
            • function indentation(string) {
                  var i;
                  try { for (i = 0; string[i] == ' '; i++){} }
                  catch (e) {}
                  return i;
              }
              Ответить
              • Здорово. Идеологический python, записанный на ECMAScript.
                Ответить
            • Можно подумать, что в первом случае ее нет... Просто она в там в трим инкапсулирована.

              P.S. Имхо, не стоит заниматься такими микрооптимизациями без повода.
              Ответить
              • В триме может быть другая оптимизация. Например, я не знаю, как там шишарп оптимизирует проверку диапазона в форыче или нет. Но, вполне может быть, что оптимизирует, и тогда форыч был бы лучше обоих вариантов. Трим тут в любом случае не подходит, т.как отрезает с обеих сторон. Но, теоретическе, он мог бы использовать форыч, и обогнать всех на длинных строках изза того, что не делал проверку диапазона.

                А оптимизировать или нет - ну так мы ж тут уже начали бенчмарками мерятся и т.д.... как же так, не оптимизировать?
                Ответить
                • > Трим тут в любом случае не подходит
                  Да там поди есть TrimStart(), раз TrimEnd() нашёлся.
                  Ответить
                  • Ещё один бенч, с учётом этого:

                    private static int GetNumberOfLeadingSpacesViaTrim(string source)
                    {
                        return source.Length - source.TrimStart().Length;
                    }


                    Счёт через String.TrimStart() и String.Length в среднем занял 0,000138488888888883
                    Счёт через цикл в среднем занял 0,000117655555555551
                    Счёт через LINQ в среднем занял 0,000455677777777891
                    Ответить
    • А как насчёт регулярного выражения?
      return Regex.Match(str, @"^\s*").Length;
      Ответить
      • Счёт через String.TrimStart() и String.Length в среднем занял 0,000178077777777767
        Счёт через цикл в среднем занял 0,00016739999999999
        Счёт через LINQ в среднем занял 0,000532255555555699
        Счёт через регулярку в среднем занял 0,00101864444444456

        Получилось медленнее всего.
        Ответить
        • Капец... У меня Regex выдал 0,0004456...
          Ответить
        • Ещё вот:
          static int GetNumberOfLeadingSpacesViaEachChar(string str)
          {
          	int i = 0;
          	foreach (char ch in str) if (char.IsWhiteSpace(ch)) ++i; else break;
          	return i;
          }

          Результат: 0,0001771 :)
          Ответить
          • Итак, продолжаем.

            Была дана строка:
            "                  bbbaka!"

            Счёт через String.TrimStart() и String.Length в среднем занял 0,000243334047349131
            Счёт через цикл for в среднем занял 0,000307815206661784
            Счёт через цикл foreach в среднем занял 0,000313723992773795
            Счёт через LINQ в среднем занял 0,000482685286941327
            Счёт через регулярку в среднем занял 0,00152696509397179

            Была дана строка:
            "   bbbaka!"

            Счёт через String.TrimStart() и String.Length в среднем занял 0,000143253022146697
            Счёт через цикл for в среднем занял 0,000161914429936376
            Счёт через цикл foreach в среднем занял 0,000175021420455629
            Счёт через LINQ в среднем занял 0,000261402201651063
            Счёт через регулярку в среднем занял 0,000844150352972605

            Была дана строка:
            " bbbaka!"
            .
            Счёт через String.TrimStart() и String.Length в среднем занял 0,000127726451468525
            Счёт через цикл for в среднем занял 0,000174751336450242
            Счёт через цикл foreach в среднем занял 0,000142662562631326
            Счёт через LINQ в среднем занял 0,000235014994319443
            Счёт через регулярку в среднем занял 0,000746058636169464
            Ответить
        • И ещё o_O:
          unsafe static int GetNumberOfLeadingSpacesViaUnsafe(string str)
          {
          	int i = 0;
          	short* c = (short*)Marshal.StringToBSTR(str).ToPointer();
          	while (*(c++) == 32) ++i;
          	return i;
          }

          Результат: 0,0003608 :)
          Ответить
          • Вот это круто. Тянет на отдельный ГК.
            Ответить
          • Ещё:
            unsafe static int GetNumberOfLeadingSpacesViaEnumerator(string str)
            {
            	int i = 0;
            	using(var enumerator = str.GetEnumerator())
            	{
            		while (enumerator.MoveNext())
            			if (char.IsWhiteSpace(enumerator.Current)) ++i; else break;
            	}
            	return i;
            }

            Результат: 0,0006230 :)
            Ответить
        • В случае с Trim'ами будут создаваться новые строки - мусор. Потом когда-нибудь сборщик мусора их удалит, потратив время. Это тоже нужно учитывать.
          Ответить
    • А что, AsParallel ещё никто не предложил?
      Ответить
      • AsInParallelUniverse?
        Ответить
      • private static int GetNumberOfLeadingSpacesAsParallel(string source)
        {
            return source.AsParallel().TakeWhile(char.IsWhiteSpace).Count();
        }


        Счёт через String.TrimStart() и String.Length в среднем занял 0,00016589164974754
        Счёт через цикл for в среднем занял 0,000193916590608682
        Счёт через цикл foreach в среднем занял 0,000158231508558716
        Счёт через LINQ в среднем занял 0,000260426639596914
        Счёт через регулярку в среднем занял 0,000849728519009069
        Счёт через IEnumerable.AsParallel() в среднем занял 0,0568745259559993

        Правда, TakeWhile() можно было бы заменить на ForAll() и условия в лямбде.
        Ответить
    • Продолжаем... :) Интересно. Вот этот метод
      static int GetNumberOfLeadingSpacesViaWhere(string str)
      {
      	return str.Where((c, i) => char.IsWhiteSpace(c)).Count();
      }

      даёт 0,0041484, в вот этот (без индекса):
      static int GetNumberOfLeadingSpacesViaWhere(string str)
      {
      	return str.Where(c => char.IsWhiteSpace(c)).Count();
      }

      даёт 0,0052 (в среднем). В чём фишка? o_O
      Ответить
    • Тестить, так тестить! Меряем не только время, но и память, и количество сборок мусора. С разным количеством пробелов и количеством повторов.

      http://pastebin.com/urZPZMSe
      При тысяче пробелов и миллионе повторов у моего старенького ноутбука памяти не хватат (2 гига).

      http://pastebin.com/9Amg4Fvn
      Это результаты, если кому интересно.

      Я щетаю, AssParallel всех нагнул!
      А если серьёзно, то цикл for без лишнего if явный фаворит. Он обогнал даже unsafe код.
      Ответить
      • >А если серьёзно, то цикл for без лишнего if явный фаворит.
        Странно.
        Неужели для осознания простого факта что императивный for рулит, необходимо написать несколько десятков комментов и проводить кучу тестов?

        ОПу как и мне это было очевидно сразу.
        Ответить
        • Ну мне, например, было интересно, насколько LINQ медленней. Всего в 2-4 раза, терпимо для большинства применений.
          Ответить
          • Сравнивая linq с циклами, мало кто учитывает, что в linq используется checked - проверка на переполнение. Это снижает производительность.

            Хотелось бы иметь возможность отключить эти проверки. Что-то типа AsUnchecked, по аналогии с AsParallel.
            Ответить
            • Это как бы нужно не просить отключать / и включать - боже упаси. Это канпелятор сам должен решать. Если смог разрулить, что тип выбраный для индексации не переполнитыса при доступу к коллекции, то и отключить можно, а нет - так ни за что не соглашаться отключать. А то народные мастера наоптимизируют.
              Ответить
        • Все зависит от того, что умеет оптимизировать компилятор. Линкю вполне может гарантировать что значение не выйдет за диапазон возможных, и это может позволить компилятору убрать проверку диапазона. В общем случае, такую гарантию тяжелее дать компилятору используя цикл (см. Флойд, Наур, Хоар, любую книгу по методам статической проверки / анализу моделей). Просто компилятор, который уже двадцать или даже больше лет оптимизировали под что-то одно, еще не умеет хорошо справлятся с новым типом задач.
          Ответить
          • > может позволить компилятору убрать проверку диапазона
            Это он дополнительные проверки убирает, в a[i], к примеру. Но он с тем же успехом убирает ее и из цикла.
            Ответить
            • С циклом в общем случае это сложнее сделать, т.как цикл дает програмисту контроль над i. Форыч или линкю скрывают этот момент, и, в принципе, делают работу проще.
              Ответить
              • > даёт программисту контроль над I
                Ну тут хотя бы частные случаи можно оптимизнуть, когда индекс внутри цикла не меняют и условие остановки удобоваримое.

                P.S. Гцц вроде бы даже переворачивало цикл вверх-ногами, если результат не влияет на ответ.
                Ответить
                • >P.S. Гцц вроде бы даже переворачивало цикл вверх-ногами, если результат не влияет на ответ.
                  А зачем? Почему часто обходят наоборот?
                  Ответить
                  • Если цикл от нуля крутится - в обратную сторону код проще, можно флаг от декремента поюзать для перехода. И не надо верхнюю границу помнить - экономим регистр и/или загрузки из памяти. Вроде бы всё.
                    Ответить
            • Гугл дает вот такой вот материал к размышлению любителям шишарпа:
              http://blogs.msdn.com/b/clrcodegeneration/archive/2009/08/13/array-bounds-check-elimination-in-the-clr.aspx
              т.е. если где-то угадать или не угадать с тем, как там у компилятора фишка ляжет по поводу проверки границ - то лучше пользоваться библиотечным методом, в котором (ну есть надежда же), что этот момент уже проверили.
              Ответить
        • Результаты я привёл для тех, у кого нет компилятора шарпа.

          Вывод добавил для тех, кому лень просматривать и сравнивать десятки строк результата.

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

    Добавить комментарий