Советы по производительности C++ и практические правила?

Какое практическое правило следует учитывать при кодировании в отношении производительности? Существует бесконечное количество способов оптимизации для конкретной платформы и компилятора, но я ищу ответы, которые одинаково хорошо (или почти) применимы для разных компиляторов и платформ.

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
7
0
4 585
25
Перейти к ответу Данный вопрос помечен как решенный

Ответы 25

Совет №1 по производительности - профилировать свой код как можно раньше и чаще. Есть много общих советов «не делайте этого», но действительно трудно гарантировать, что это повлияет на производительность вашего приложения. Почему? Каждое приложение индивидуально. Легко сказать, что передача вектора по значению - это плохо, если у вас много элементов, но ваша программа даже использует вектор (вам, вероятно, следует, но ...)?

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

Обновлено:

Несколько человек прокомментировали «раннюю» часть моего ответа. Я не думаю, что вам следует заниматься профилированием с первого дня. Однако вам также не следует ждать до 1 месяца с момента отправки.

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

В типичном проекте я обнаружил, что у меня есть код, отвечающий этим критериям на 30-40% прохождения проекта (100% находится в руках клиентов). Я условно причисляю это время к раннему периоду.

Согласен с Марцином. Измеряйте на ранней стадии только в том случае, если это действительно очевидно при тестировании (взятие 100 вместо 1). Другими словами, измеряйте только тогда, когда это проблема.

strager 24.11.2008 23:49

Рано относительно. Я не говорю о профильном дне №1. Но в проекте сроком на 1 год я бы начал профилирование не позднее, чем через 3 или 4 месяца, а не до конца.

JaredPar 24.11.2008 23:50

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

Torlack 25.11.2008 00:49

Существует большая разница между оптимизацией дизайна (O (n!) По сравнению с O (log n)) или оптимизацией кода (повторно реализовать некоторую функцию в сборке)

Patrick Huizinga 25.11.2008 01:26

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

call me Steve 25.11.2008 03:24

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

Ed S. 07.01.2012 23:40

«преждевременная оптимизация - корень всех зол» (Кнут, Дональд)

Это действительно зависит от типа кода, который вы пишете, и его типичного использования.

Ответ принят как подходящий

На ум приходит известная цитата:

«Мы должны забыть о небольшой эффективности, скажем, примерно в 97% случаев: преждевременная оптимизация - корень всех зол». (Кнут, Дональд. Структурированное программирование с переходом к утверждениям, ACM Journal Computing Surveys, том 6, № 4, декабрь 1974 г., стр.268.)

Но, возможно, вам все равно не стоит передавать большие структуры данных по значению ... :-)

Обновлено: И, возможно, также избегайте О (N ^ 2) или более сложных алгоритмов ...

Я бы взял алгоритм O (2) в любой день. Это занимает в два раза больше времени, чем O (1)!

coppro 24.11.2008 23:51

Но берегитесь последних 3%, они могут быть убийственными.

Torlack 25.11.2008 00:53

O (2) не обязательно занимает вдвое больше времени, чем O (1), см. Определение большого O. Во-вторых, предложение «Избегайте O (N ^ 2) или более» бесполезно. O (N ^ 2) не может быть достигнуто для тысяч задач.

Cartesius00 11.09.2011 13:08

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

Кроме того, алгоритмические оптимизации обычно имеют большее влияние, чем микро. Использование A-звезды вместо поиска пути методом грубой силы будет быстрее, точно так же, как круги Брезенхема лучше, чем использование sin / cos. Конечно, есть исключения, но они очень (очень) редки (<0,1%). Если у вас хороший дизайн, изменение алгоритма изменяет только один модуль в вашем коде. Легкий.

один простой совет - выработать привычку делать ++ i, а не i ++. i ++ делает копию, и это может быть дорого.

Я сам использую это для всех приращений в отдельной строке (что составляет 90% от - и ++).

strager 24.11.2008 23:50

Я хотел бы думать, что компиляторы оптимизировали бы (неиспользованное) возвращаемое значение i ++, но вы правы - при перегрузке это две разные функции.

aib 24.11.2008 23:52

Держите свой код как можно более чистым. Компиляторы сегодня ЧУДЕСНЫЕ. Затем, если у вас есть проблема с перфомансом, введите profile.

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

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

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

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

strager 24.11.2008 23:56

Если вы просто перемещаете вещи, вероятно, компилятор все равно сделает это за вас.

Marcin 25.11.2008 01:53

Вот несколько:

  • Эффективно используйте Встраивание (в зависимости от вашей платформы).

  • По возможности избегайте использования временные (и знайте, что это такое)

    х = у + г;

    Было бы лучше оптимизировать, если бы было написано как:

    х = у;

    х + = z;

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

Если у вас хорошее настроение, посмотрите Эффективный C++. У меня есть копия дома, когда я учился в школе.

Следует отметить, что x, y и z будут экземплярами класса, а не PoD, и что оператор + дорогостоящий, а временные конструкции дороги.

strager 24.11.2008 23:52

Да, при таком ограничении это имеет смысл.

peterchen 24.11.2008 23:53

Проверьте факты, сегодня все компиляторы реализуют оптимизацию возвращаемого значения и оптимизацию именованного возвращаемого значения. Использование «x = y + z» позволяет вам сделать «x» константой, что может привести к лучшей оптимизации, большей читабельности и меньшему количеству ошибок ... Возможно, не всегда стоит удалять «небольшой» временный элемент.

Vincent Robert 25.11.2008 00:37

Часть об отказе от использования временных файлов неверна. Я часто делал код быстрее, используя «x = y + z» вместо другого. Профилируйте и посмотрите машинный код, прежде чем вносить подобные микро изменения.

Torlack 25.11.2008 00:58

Обновлено: я действительно имел в виду некорректность временных. И это относится и к типам, отличным от POD.

Torlack 25.11.2008 01:15
  • По возможности используйте if или switch вместо вызовов через указатели функций. Уточнение: void doit(int m) { switch(m) { case 1: f1(); break; case 2: f2(); break; } } вместо void doit(void(*m)()) { m(); } может встроить вызовы.
  • Когда это возможно и не причиняет вреда, предпочитайте CRTP виртуальным функциям.
  • По возможности избегайте строк C и используйте класс String. Чаще всего это будет быстрее. ("мера" постоянной продолжительности, добавление амортизированной постоянной времени, ...)
  • Всегда передает определенные пользователем типизированные значения (кроме тех, где это не имеет смысла, например, итераторы) по ссылке на const (T const &) вместо копирования значения.
  • Для определяемых пользователем типов всегда отдавайте предпочтение ++t вместо t++.
  • Используйте const как можно раньше. Самое главное, чтобы улучшить читаемость.
  • Постарайтесь свести new к минимуму. Всегда предпочитайте автоматические переменные (в стеке), если это возможно
  • Вместо того, чтобы заполнять массивы самостоятельно, предпочитайте инициализацию пустым списком инициализаторов, например T t[N] = { };, если вам нужны нули.
  • Используйте список инициализаторов конструктора как можно чаще, особенно при инициализации типизированных элементов, определенных пользователем.
  • Используйте функторы (типы с перегруженным operator()). Они встраиваются лучше, чем вызовы через указатели на функции.
  • Не используйте классы вроде std::vector или std::string, если у вас есть количество фиксированного размера, которое не растет. Используйте boost::array<T, Size> или голый массив и используйте его правильно.

И действительно, чуть не забыл:

Преждевременная оптимизация - корень всех зол

Избегайте строк C и используйте класс String для повышения производительности? Я не думаю, что когда-либо преобразовывал строковый код C в управляемый строковый класс по соображениям производительности. Всегда было наоборот. По большей части это сводится к большему контролю над распределением памяти.

Torlack 25.11.2008 00:55

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

Johannes Schaub - litb 25.11.2008 01:09

по одному. но обратите внимание на stackoverflow.com/questions/270408/…. Если ваша структура не имеет настраиваемого конструктора копирования и довольно мала, передача по значению может выиграть.

Johannes Schaub - litb 25.11.2008 05:12

Используйте существующий проверенный код, который использовался и использовался повторно. (Пример: STL, ускорение или прокатка собственных контейнеров и алгоритмов)

Обновление по комментариям: ПРАВИЛЬНО используйте существующий проверенный код, который использовался и повторно использовался.

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

strager 25.11.2008 00:02

Да, не катите свои собственные, но довольно легко использовать существующие библиотеки неэффективным образом. Возьмем, к примеру, вектор STL. Просто заполнить его несколькими тысячами записей и везде передавать по значению. Фактически, разница в один символ (&) может легко вызвать этот сценарий

JaredPar 25.11.2008 00:08

В Викиучебнике есть кое-что.

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

Использование общих алгоритмов - отличный совет по оптимизации - не с точки зрения времени выполнения, а времени написания кода. Зная, что вы можете сортировать (начало, конец) и ожидать, что диапазон - будь то два указателя или итераторы для базы данных - будет отсортирован (и, более того, используемый алгоритм также будет эффективным во время выполнения). Универсальное программирование - это то, что делает C++ уникальным и мощным, и вы всегда должны помнить об этом. Вам не нужно писать много алгоритмов, потому что версии уже существуют (и, вероятно, так же быстро или быстрее, чем все, что вы написали бы). Если у вас есть другие соображения, вы можете специализировать алгоритмы.

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

template <typename T>
struct add {
    operator T ()(T const& a, T const& b) const { return a + b; }
};

int result = add<int>()(1, 2);

Их можно использовать почти в любом контексте, где можно использовать обычную функцию или указатель на функцию. Обычно они происходят от std::unary_function или std::binary_function, но часто в этом нет необходимости (и фактически делается только для наследования некоторых полезных typedef).

РЕДАКТИРОВАТЬ В приведенном выше коде необходима явная квалификация типа <int>. Вывод типа работает только для вызовов функций, но не для создания экземпляров. Однако его часто можно опустить, используя вспомогательную функцию make. Это сделано в STL для pair:

template <typename T1, typename T2>
pair<T1, T2> make_pair(T1 const& first, T2 const& second) {
    return pair<T1, T2>(first, second);
}

// Implied types:
pair<int, float> pif = make_pair(1, 1.0f);

Кто-то упомянул в комментариях, что функторы иногда называют «функционоидами». Да иш - но не совсем. Фактически, «функтор» - это (несколько странное) сокращение от «функционального объекта». Функционал концептуально похож, но реализован с использованием виртуальных функций (хотя они иногда используются как синонимы). Например, функциональноид может выглядеть так (вместе с определением его необходимого интерфейса):

template <typename T, typename R>
struct UnaryFunctionoid {
    virtual R invoke(T const& value) const = 0;
};

struct IsEvenFunction : UnaryFunctionoid<int, bool> {
    bool invoke(int const& value) const { return value % 2 == 0; }
};

// call it, somewhat clumsily:
UnaryFunctionoid const& f = IsEvenFunction();
f.invoke(4); // true

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

В FAQ по C++ есть больше сказать по этой теме.

Я не думаю, что <int> в вашей последней строке вообще необходим (я могу ошибаться), поэтому вам не нужно убивать удобочитаемость при использовании «функторов» (я слышал, что вместо этого они называются «functionids»).

strager 25.11.2008 00:06

Отличный момент! Нет необходимости помещать ключевое слово inline в ваши небольшие функции, если вы используете их с помощью указателей. Указанные функции НИКОГДА не встраиваются, какие бы ключевые слова ни использовались. Помните об этом и используйте функторы.

Vincent Robert 25.11.2008 00:41

strager: Необходим <int>, см. обновление к моему сообщению. То же самое и с функционоидами.

Konrad Rudolph 25.11.2008 00:58

хороший пост. хотя моя точка зрения против функции ptrs и для switch / if больше похожа на if (method == 1) return log10 (value); иначе вернуть журнал (vaue); а не double (* method) (double) = & log10; ... метод (значение); Я бы все равно предпочел, если бы против boost :: function и объекта функции здесь (нельзя использовать шаблон!)

Johannes Schaub - litb 25.11.2008 01:12

litb: Ну я в тупике. o.O… Я никогда не видел, чтобы кто-то делал это слишком расточительным использованием какой-либо функции. ;-)

Konrad Rudolph 25.11.2008 01:13

Спасибо за обновление; Я не понимал этого в отношении функциональных объектов. знак равно

strager 25.11.2008 01:13

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

Torlack 25.11.2008 01:17

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

Konrad Rudolph 25.11.2008 01:29

Рассмотрите возможность использования пул памяти.

Два лучших совета для C++:

Купите эффективный C++, Скотт Мейерс.

Затем приобретите более эффективный C++ от Скотта Мейерса.

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

  1. Всегда старайтесь думать о том, как выглядит ваша память - например, массив - это последовательная строка памяти размером numOfObjects X sizeof(object). двумерный массив равен n X m X sizeof(object), и каждый объект находится в индексе n + m X n, и поэтому

    for(int i = 0 ; i < n ; i++){
        for(int j = 0 ; j < m ; j++){
            arr[i,j] = f();
    

    намного лучше (в одном процессе):

    for(int i = 0 ; i < n ; i++){
        for(int j = 0 ; j < m ; j++){
            arr[j,i] = f();
    

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

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

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

Практически все остальное второстепенно по сравнению с этим.

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

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

Однако, чтобы полностью реализовать это преимущество, вам необходимо убедиться, что вы не раскрываете внутренний выбор дизайна как часть интерфейса вашего класса / модуля / чего угодно. Ни один из ваших клиентов не должен зависеть за использование вами std::vector. По крайней мере, предоставьте typedef, который они (и вы) можете использовать, что должно позволить изменить вектор на список (или что-то еще) в зависимости от ваших потребностей.

Точно так же убедитесь, что в вашем распоряжении самый широкий выбор отлаженных алгоритмов и контейнеров. Boost и / или TR1 в наши дни очень необходимы.

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

хотя и не затрагивает точную проблему, вот несколько советов:

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

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

  • Постарайтесь выделить все свои объекты при запуске, минимизируйте использование new во время выполнения.
  • Создавайте свои структуры данных так, чтобы ваши алгоритмы имели хороший шанс быть O (1).
  • И, как обычно, модулируйте, чтобы потом можно было вытащить и заменить. Это означает, что у вас есть исчерпывающий набор модульных тестов, которые дают вам уверенность в правильности вашего нового решения.
  • Добавьте тесты производительности в свой набор модульных тестов, чтобы случайно не получить код O (n * n) :-)

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

vrdhn 20.02.2009 11:10
  • Правило 1: не делайте этого
  • Правило 2: Измерьте

Из одной из книг по C++, на которую я ссылался («Эффективные методы производительности C++» Булки и Мэйхью), в которой прямо говорится об аспектах производительности C++. Один из них был;

при определении конструкторов ... инициализируйте также другие конструкторы; что-то типа;

class x {

x::x(char *str):m_x(str) {} // and not as x::x(char *str) { m_str(str); }

private:
std::string m_x;

};

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

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

Другие вопросы по теме