Методы, чтобы избежать минимальной неэффективности объектов со сложными объектами в циклах на С++? - программирование
Подтвердить что ты не робот

Методы, чтобы избежать минимальной неэффективности объектов со сложными объектами в циклах на С++?

Вопрос Первый

Есть ли элегантное решение на С++, чтобы предотвратить необходимость объявления сложных переменных объекта, которые используются только в цикле вне цикла по причинам эффективности?

Подробное объяснение

Коллега поднял интересный момент. к нашей политике кода, которая заявляет (перефразирует): всегда используйте минимальную область для переменных и объявляйте переменную при первой инициализации.

Пример кодирования Пример:

// [A] DO THIS
void f() {
  ...
  for (int i=0; i!=n; ++i) {
    const double x = calculate_x(i);
    set_squares(i, x*x);
  }
  ...
}

// [B] DON'T do this:
void f() {
  int i;
  int n;
  double x;
  ...
  for (i=0; i!=n; ++i) {
    x = calculate_x(i);
    set_squares(i, x*x);
  }
  ...
}

Это хорошо и хорошо, и в этом нет ничего плохого, пока вы не перейдете от примитивных типов к объектам. (для определенного типа интерфейса)

Пример:

// [C]
void fs() {
  ...
  for (int i=0; i!=n; ++i) {
    string s;
    get_text(i, s); // void get_text(int, string&);
    to_lower(s);
    set_lower_text(i, s);
  }
  ...
}

Здесь строка s будет разрушена, она освободит память каждый цикл цикла, а затем каждый цикл функции get_text должен будет вновь выделить память для s-буфера.

Было бы более эффективно писать:

  // [D]
  string s;
  for (int i=0; i!=n; ++i) {
    get_text(i, s); // void get_text(int, string&);
    to_lower(s);
    set_lower_text(i, s);
  }

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

Отказ от ответственности: Обратите внимание: Поскольку это циклы, и мы говорим о распределении памяти, я не считаю преждевременную оптимизацию думать об этом проблема в целом. Конечно, есть случаи и циклы, где накладные расходы не имеют значения; но n имеет тенденцию ворчания, которая будет больше, чем Dev ожидает изначально, и код имеет тенденцию к ворчанию в контексте, где производительность имеет значение.

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

Обратите внимание, что я пишу его так:

// [E]
void fs() {
  ...
  {
    string s;
    for (int i=0; i!=n; ++i) {
      get_text(i, s); // void get_text(int, string&);
      to_lower(s);
      set_lower_text(i, s);
    }
  }
  ...
}

не является решением, так как читаемость еще больше!

Вдумчивая далее, интерфейс функции get_text в любом случае не является идиоматическим, так как вчера все параметры были такими же, а "хороший" интерфейс возвращался по значению:

  // [F]
  for (int i=0; i!=n; ++i) {
    string s = get_text(i); // string get_text(int);
    to_lower(s);
    set_lower_text(i, s);
  }

Здесь мы не платим double за распределение памяти, так как очень вероятно, что s будет построено через RVO из возвращаемого значения, поэтому для [F] мы выплачиваем то же самое накладные расходы, что и в [C], В отличие от случая [C], мы не можем оптимизировать этот вариант интерфейса.

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

Проблема заключается не столько в том, что иногда приходится отказываться от чистого кода для эффективности, проблема в том, что как только Devs начнет находить такие особые случаи, весь Руководство по кодированию (см. [A], [B]) теряет полномочия.

Теперь вопрос будет выглядеть следующим образом: см. первый абзац

4b9b3361

Ответ 1

Я бы:

  • сделать исключение из правила для этих тяжеловесов. например "D", и обратите внимание, что вы можете ограничить область действия по желанию.
  • разрешить вспомогательную функцию (строка также может быть параметром)
  • и если вам это действительно не понравилось, вы можете объявить локальное в своей области цикла for с помощью многоэлементного объекта, в котором хранился ваш счетчик/итератор и временный. std::pair<int,std::string> будет одним из вариантов, хотя специализированный контейнер может уменьшить синтаксический шум.

(и во всех случаях параметр out будет быстрее, чем стиль RVO)

Ответ 2

Было бы явно более эффективно писать: [начало примера D...]

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

Имеет смысл выводить инварианты из вашей петли, если они создают узкое место (которое профайлер скажет вам). В противном случае используйте код, который является идиоматическим.

Ответ 3

Зависит от реализации get_text.

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

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

(BTW, std::string обычно реализует так называемую "оптимизацию небольших строк", что позволяет избежать динамического распределения для небольших строк. Поэтому, если вы знаете, что большинство ваших строк будут достаточно маленькими, а реализация std::string не будет вы могли бы теоретически избегать динамического выделения даже при создании нового объекта на каждой итерации. Однако это было бы очень хрупким, поэтому я бы рекомендовал против него.)


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

Ответ 4

Если у вас есть реализация класса string, то tolower (s) все равно выделяет память, поэтому неясно, что вы можете получить производительность, просто объявив s вне цикла.

По моему мнению, есть две возможности: 1.) У вас есть класс, конструктор которого делает что-то нетривиальное, которое не нужно повторять на каждой итерации. Тогда логически просто поставить декларацию вне цикла. 2.) У вас есть класс, конструктор которого не делает ничего полезного, а затем помещает объявление внутри цикла.

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

StringReservedMemory m (500); /* base object for something complex, allocating 500 bytes of space */
for (...) {
   MyOptimizedStringImplementation s (m);
   ...
}