Маленькое - это красиво, но быстро ли? - PullRequest
6 голосов
/ 24 ноября 2010

У меня был спор с коллегой о реализации простого парсера строк. Один «маленький», 10 строк кода с использованием c ++ и потоков, другой - 70 строк кода, использование переключателей и итерация строки char по char. Мы проверили это более 1 миллиона итераций и измерили скорость с помощью команды времени. Похоже, что длинный и уродливый подход в среднем на 1 секунду быстрее.

Проблема: Ввод: строка

"v=spf1 mx include:_spf-a.microsoft.com include:_spf-b.microsoft.com include:_spf-c.microsoft.com include:_spf-ssg-a.microsoft.com ip4:131.107.115.212 ip4:131.107.115.215 ip4:131.107.115.214 ip4:205.248.106.64 ip4:205.248.106.30 ip4:205.248.106.32 ~all a:1.2.3.4"

Выход: map<string, list<string>> со всеми значениями для каждого ключа, такими как: ip4, include, a

пример вывода одной итерации в приведенной выше строке ввода:

ключ:

1.2.3.4,

ключ: включить

_spf-a.microsoft.com, _spf-b.microsoft.com, _spf-c.microsoft.com, _spf-ssg-a.microsoft.com,

ключ: ip4

131.107.115.212, 131.107.115.215, 131.107.115.214, 205.248.106.64, 205.248.106.30, 205.248.106.32,

Парсер "маленький - это красиво":

        istringstream iss(input);
        map<string, list<string> > data;
        string item;
        string key;
        string value;

        size_t pos;
        while (iss.good()) {
                iss >> item;
                pos = item.find(":");
                key = item.substr(0,pos);
                data[key].push_back(item.substr(pos+1));
        }

Второй более быстрый подход:

  typedef enum {I,Include,IP,A,Other} State;
  State state = Other;
  string line = input;
  string value;
  map<string, list<string> > data;
  bool end = false;
  size_t pos = 0;
  while (pos < line.length()) {
   switch (state) {
    case Other:
     value.clear();
     switch (line[pos]) {
      case 'i':
       state = I;
       break;
      case 'a':
       state = A;
       break;
      default:
       while(line[pos]!=' ' && pos < line.length())
        pos++;
     }
     pos++;
     break;
    case I:
     switch (line[pos]) {
      case 'p':
       state = IP;
       break;
      case 'n':
       state = Include;
       break;
     }
     pos++;
     break;
    case IP:
     pos+=2;
     for (;line[pos]!=' ' && pos<line.length(); pos++) {
      value+=line[pos];
     }
     data["ip4"].push_back(value);
     state = Other;
     pos++;
     break;
    case Include:
     pos+=6;
     for (;line[pos]!=' ' && pos<line.length(); pos++) {
      value+=line[pos];
     }
     data["include"].push_back(value);
     state = Other;
     pos++;
     break;
    case A:
     if (line[pos]==' ')
      data["a"].push_back("a");
     else {
      pos++;
      for (;line[pos]!=' ' && pos<line.length(); pos++) {
       value+=line[pos];
      }
     }
     data["a"].push_back(value);
     state = Other;
     pos++;
     break;
   }
  }

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

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

Обновление: Добавлено определение состояния и инициализация. Контекст: чем дольше подход выполняет 1 миллион итераций на одной и той же строке за 15,2 секунды, тем меньше код делает то же самое в среднем за 16,5 секунды.

обе версии скомпилированы с g ++ -O3, g ++ - 4.4, работает на процессоре Intel® Core ™ 2 Duo E8200 с частотой 2,66 ГГц, Linux Mint 10

Хорошая сторона выиграла эту битву :) Я обнаружил небольшую ошибку в маленькой программе, она добавляла на карту даже недопустимые значения, те, у которых в строке не было двоеточия ":". После добавления оператора if для проверки наличия двоеточия меньший код выполняется быстрее, намного быстрее. Теперь время: «маленький и красивый»: 12,3, а длинный и некрасивый: 15,2.

Маленькое - это красиво :) 1041 *

Ответы [ 11 ]

16 голосов
/ 24 ноября 2010

Меньше не может быть быстрее. Один пример: пузырьковая сортировка очень короткая, но это O (n * n). QuickSort и MergeSort длиннее и кажутся более сложными, но это O (n log n).

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

14 голосов
/ 24 ноября 2010

Меньше строк кода у вас есть;лучшее.Не добавляйте еще 60 строк, если вам это не нужно.Если это медленно, профиль.Тогда оптимизируйте.Не оптимизируйте, пока вам это не нужно.Если он работает нормально, оставьте все как есть.Добавление большего количества кода добавит больше ошибок.Вы этого не хотите.Держать его коротким.Действительно.

Читать это сообщение вики .

«Преждевременная оптимизация - корень всего зла» - Дональд Кнут , довольно умный парень.

Можно написать более быстрый код, написав меньше егопросто более разумно.Один из способов повысить скорость: делать меньше.

Цитировать Рэймонда Чена:

«Один из вопросов, которые я получаю, -« Мое приложение медленно запускается.Какие супер секретные злые трюки, которые вы, ребята из Microsoft, используете, чтобы заставить ваши приложения запускаться быстрее? »Ответ таков:« Супер злой уловок - делать меньше вещей »-« Пять вещей, которые должен знать каждый программист Win32 »"(16 сентября 2005 г.)

Кроме того, посмотрите почему GNU grep работает быстро .

4 голосов
/ 24 ноября 2010

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

Реальный вопрос, который нужно задать, - важна ли эта дополнительная секунда?Это имеет значение?

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

2 голосов
/ 24 ноября 2010

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

Нет, это не так. Ну, это не так долго, когда вы говорите, что одна на одну секунду быстрее, чем другая, вы подразумеваете, что примерно одна занимает 10 секунд, а другая - 11 секунд, а одна - не 0,1 секунды, а другая - 1,1 секунды. Даже тогда, если вам нужно только один раз запустить анализ при запуске программы, это может не стоить беспокоиться.

Всегда предпочитают лаконичный, легкий для понимания код, а не длинную непрозрачную, но более быструю версию, если при профилировании не может быть продемонстрировано значительное повышение производительности. Не забывайте, что время программиста тоже чего-то стоит. Если вы потратите еще 10 минут, пытаясь выяснить, что делает нижний бит кода, это эквивалентно экономии 600 запусков кода.

2 голосов
/ 24 ноября 2010

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

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

Я счастлив, зная, что когда я вызываю функцию Sort какой-то библиотеки, она использует QuickSort вместо двухрядной супер элегантной, но медленной BubbleSort: -)

1 голос
/ 03 декабря 2010

Я бы взломал его с помощью генератора лексера. Судя по входным данным, я думаю, что большая часть сложности состоит в том, чтобы выяснить, что такое токены. Простой токенизатор и синтаксический анализатор конечного автомата, написанный от руки (я предполагаю, что он будет иметь около 2 или 3 состояний), должны составлять около 20 строк кода:

extern string payload;

Output Process() {
   Output output;

   string key = "";
   bool value = false;
   while(!eof()) {
     switch(yylex()) {
      case tok_string:
       if (value) {
         output[key].push_back(payload);
         value = false;
         key = "";
       } else {
         key = payload;
       }
       break;

      case tok_colon:
       value = (key != "");
       break;
     }
   }
   return output;
}
1 голос
/ 30 ноября 2010

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

Доверяйте своему собственному суждению!

В отношении этого конкретного случаяУ меня есть следующие заметки:

  • Две версии на самом деле не делают одно и то же.Более короткий обрабатывает все возможные имена клавиш, в то время как более длинный поддерживает только несколько жестко закодированных ключей.

  • I предположит (это должно быть профилировано!), Чтобольшая часть времени, потраченного на оба алгоритма, заключается в построении / распределении узлов std :: map и std :: list.Переход на другую структуру данных, не требующую выделения отдельных ключей и значений, вероятно, значительно ускорит обе реализации.

1 голос
/ 24 ноября 2010

Мне трудно понять, что делает вторая версия. С одной стороны, состояние не инициализируется с самого начала.

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

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

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

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

1 голос
/ 24 ноября 2010

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

1 голос
/ 24 ноября 2010

Рассмотрим размер:

  1. Языковые стандарты
  2. Используемые цепи инструментов
  3. Обязательное знание для получения кода
  4. Генерируемый выход
  5. Время компиляции

Обратите внимание, что эти пункты применяются к подмножествам, используемым, если код стиля C был фактически скомпилирован на компиляторе C ++.

Важно понимать, что время, необходимое для разработки и начала разработки без неявного волшебства различных элементов стандартной библиотеки C ++, которые вы использовали в «маленькой» версии, может перевесить время, затрачиваемое на кодирование «длинной» версии.

Также, чтобы прокомментировать различия в скорости: широкое использование библиотек сгладит различия в возможностях программирования. Чтобы по-настоящему выжать производительность и «красоту», вам необходимо овладеть языком самого низкого уровня, применимым к проблеме.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...