Как я мог осуществить рефакторинг этот код с производительностью в памяти?

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

class MyClass
{
private:
   unsigned int m_step;
public:
   MyClass() : m_step(0) {};

   Elem GetElem()
   {
      // This switch statement will be optimized as a jump table by the compiler.
      // Note that there is no break statments between the cases.
      switch (m_step)
      {
      case 0:
         if (UseHeuristic1())
         {
            m_step = 1; // Heuristic one is special it will never provide more than one element.
            return theElem;
         }

         m_step = 1;

      case 1:
         DoSomeOneTimeInitialisationForHeuristic2();
         m_step = 2;

      case 2:
         if (UseHeuristic2())
         {
            return theElem;
         }

         m_step = 3;

      case 3:
         if (UseHeuristic3())
         {
            return theElem;
         }
         m_step = 4; // But the method should not be called again
      }

      return someErrorCode;
   };
}

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

Причина инициализации все не сделаны заранее, состоит в том, что они никогда не могли бы быть необходимы. Это всегда возможно (и распространено) для GetElem, который не назовут снова после того, как это возвратило элемент, даже если существуют все еще элементы, которые это могло бы возвратить.

В то время как это - эффективное внедрение, я нахожу это действительно ужасным. Оператор выбора является взломом; использование его без повреждения также hackish; метод становится действительно длинным, даже если каждая эвристика инкапсулируется в ее собственном методе.

Как я должен осуществить рефакторинг этот код, таким образом, это более читаемо и изящно при сохранении его максимально эффективным?

5
задан Carl Manaster 1 January 2010 в 20:32
поделиться

10 ответов

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

Однако, если новые эвристики добавлены и удалены, и вы считаете, что это процесс, склонный к ошибкам, то вы должны подумать о его рефакторинге. Очевидным выбором для этого будет введение шаблона State design pattern. Это заменит ваше утверждение переключения на полиморфизм, который может замедлить процесс, но вам придется профилировать и то, и другое, чтобы быть уверенным.

2
ответ дан 18 December 2019 в 09:50
поделиться

Похоже, что в этом коде действительно мало что можно оптимизировать - вероятно, большую часть оптимизации можно провести в UseHeuristic функциях. Что же в них?

2
ответ дан 18 December 2019 в 09:50
поделиться

Заверните каждую эвристику в итератор. Полностью инициализируйте его при первом вызове на hasNext(). Затем соберите все итераторы в списке и используйте супер-iterator для итерации через все:

boolean hasNext () {
    if (list.isEmpty()) return false;

    if (list.get(0).hasNext()) return true;

    while (!list.isEmpty()) {
        list.remove (0);
        if (list.get(0).hasNext()) return true;
    }
    return false;
}
Object next () {
    return list.get (0).next ();
}

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

.

[ЭДИТ] Изменил "превращать каждый" на "обертывать каждый", чтобы прояснить мои намерения.

4
ответ дан 18 December 2019 в 09:50
поделиться

Вы можете повернуть управляющий поток внутрь.

template <class Callback>  // a callback that returns true when it's done
void Walk(Callback fn)
{
    if (UseHeuristic1()) {
        if (fn(theElem))
            return;
    }
    DoSomeOneTimeInitialisationForHeuristic2();
    while (UseHeuristic2()) {
        if (fn(theElem))
            return;
    }
    while (UseHeuristic3()) {
        if (fn(theElem))
            return;
    }
}

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

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

.
2
ответ дан 18 December 2019 в 09:50
поделиться

Если обрабатываемый элементный код может быть преобразован в интегральное значение, то на основе элемента можно построить таблицу указателей функции и индекс. Таблица будет иметь по одной записи для каждого 'обработанного' элемента, и по одной для каждого известного, но не обработанного элемента. Для неизвестных элементов перед индексацией таблицы указателей функции выполните быструю проверку.

Вызов функции обработки элементов быстр.

Вот рабочий пример кода:

#include <cstdlib>
#include <iostream>
using namespace std;

typedef void (*ElementHandlerFn)(void);

void ProcessElement0()
{
    cout << "Element 0" << endl;
}

void ProcessElement1()
{
    cout << "Element 1" << endl;
}
void ProcessElement2()
{
    cout << "Element 2" << endl;
}

void ProcessElement3()
{
    cout << "Element 3" << endl;
}

void ProcessElement7()
{
    cout << "Element 7" << endl;
}

void ProcessUnhandledElement()
{
    cout << "> Unhandled Element <" << endl;
}




int main()
{
    // construct a table of function pointers, one for each possible element (even unhandled elements)
    // note: i am assuming that there are 10 possible elements -- 0, 1, 2 ... 9 --
    // and that 5 of them (0, 1, 2, 3, 7) are 'handled'.

    static const size_t MaxElement = 9;
    ElementHandlerFn handlers[] = 
    {
        ProcessElement0,
        ProcessElement1,
        ProcessElement2,
        ProcessElement3,
        ProcessUnhandledElement,
        ProcessUnhandledElement,
        ProcessUnhandledElement,
        ProcessElement7,
        ProcessUnhandledElement,
        ProcessUnhandledElement
    };

    // mock up some elements to simulate input, including 'invalid' elements like 12
    int testElements [] = {0, 1, 2, 3, 7, 4, 9, 12, 3, 3, 2, 7, 8 };
    size_t numTestElements = sizeof(testElements)/sizeof(testElements[0]);

    // process each test element
    for( size_t ix = 0; ix < numTestElements; ++ix )
    {
        // for some robustness...
        if( testElements[ix] > MaxElement )
            cout << "Invalid Input!" << endl;
        // otherwise process normally
        else
            handlers[testElements[ix]]();

    }

    return 0;
}
1
ответ дан 18 December 2019 в 09:50
поделиться

Это микрооптимизация, но нет необходимости устанавливать значение m_elem, когда вы не возвращаетесь из GetElem. См. код ниже.

Более масштабная оптимизация определенно нуждается в упрощающем потоке управления (меньше скачков, меньше возвратов, меньше тестов, меньше вызовов функций), т.к. при выполнении скачка кэш процессора опустошается (ну, в некоторых процессорах есть предсказание ветвей, но это не "серебряная пуля"). Можно попробовать решения, предложенные Аароном или Джейсоном, а есть и другие (например, можно реализовать несколько функций get_elem и вызывать их через указатель функции, но я уверен, что это будет медленнее).

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

class MyClass
{
private:
   unsigned int m_step;
public:
   MyClass() : m_step(0) {};

   Elem GetElem()
   {
      // This switch statement will be optimized as a jump table by the compiler.
      // Note that there is no break statments between the cases.
      switch (m_step)
      {
      case 0:
         if (UseHeuristic1())
         {
            m_step = 1; // Heuristic one is special it will never provide more than one element.
            return theElem;
         }

      case 1:
         DoSomeOneTimeInitialisationForHeuristic2();
         m_step = 2;

      case 2:
         if (UseHeuristic2())
         {
            return theElem;
         }

      case 3:
         m_step = 4;

      case 4:
         if (UseHeuristic3())
         {
            return theElem;
         }
         m_step = 5; // But the method should not be called again
      }

      return someErrorCode;
   };
}
1
ответ дан 18 December 2019 в 09:50
поделиться

Что вы действительно можете здесь сделать, так это заменить условное состояние на шаблон State.

http://en.wikipedia.org/wiki/State_pattern

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

Что могло бы улучшить производительность, так это устранение DoSomeOneTimeInitialisationForHeuristic2(); с отдельным состоянием между. 1 и 2

.
1
ответ дан 18 December 2019 в 09:50
поделиться

Если он не сломан, не исправляйте его.

Он выглядит довольно эффективным и так далее. Понять это тоже не сложно. Добавление итераторов и т.д., наверное, усложнит понимание.

Наверное, лучше сделать

  1. Анализ производительности. Действительно ли время, потраченное на эту процедуру, или большая его часть в вызываемых функциях? Я не вижу здесь значительного времени.
  2. Больше юнит-тестов, чтобы кто-то не смог его взломать, если придется его модифицировать.
  3. Дополнительные комментарии в коде.
0
ответ дан 18 December 2019 в 09:50
поделиться

. Так как каждый эвристический объект представлен функцией с идентичной сигнатурой, то можно составить таблицу указателей функции и пройти по ней.

class MyClass 
{ 
private: 
   typedef bool heuristic_function();
   typedef heuristic_function * heuristic_function_ptr;
   static heuristic_function_ptr heuristic_table[4];
   unsigned int m_step; 
public: 
   MyClass() : m_step(0) {}; 

   Elem GetElem() 
   { 
      while (m_step < sizeof(heuristic_table)/sizeof(heuristic_table[0]))
      {
         if (heuristic_table[m_step]())
         {
            return theElem;
         }
         ++m_step;
      }

      return someErrorCode; 
   }; 
}; 

MyClass::heuristic_function_ptr MyClass::heuristic_table[4] = { UseHeuristic1, DoSomeOneTimeInitialisationForHeuristic2, UseHeuristic2, UseHeuristic3 };
1
ответ дан 18 December 2019 в 09:50
поделиться

Я не думаю, что ваш код так уж плох, но если вы часто делаете такие вещи, и хотите скрыть механизмы, чтобы логика была яснее, вы можете посмотреть на корутинные макросы Саймона Тэтэма . Они предназначены для C (с использованием статических переменных), а не C++ (с использованием переменных-членов), но тривиально это изменить.

Результат должен выглядеть примерно так:

Elem GetElem()
{
  crBegin;

  if (UseHeuristic1())
  {
     crReturn(theElem);
  }

  DoSomeOneTimeInitialisationForHeuristic2();

  while (UseHeuristic2())
  {
     crReturn(theElem);
  }

  while (UseHeuristic3())
  {
     crReturn(theElem);
  }

  crFinish;
  return someErrorCode;
}
3
ответ дан 18 December 2019 в 09:50
поделиться
Другие вопросы по тегам:

Похожие вопросы: