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





Совет №1 по производительности - профилировать свой код как можно раньше и чаще. Есть много общих советов «не делайте этого», но действительно трудно гарантировать, что это повлияет на производительность вашего приложения. Почему? Каждое приложение индивидуально. Легко сказать, что передача вектора по значению - это плохо, если у вас много элементов, но ваша программа даже использует вектор (вам, вероятно, следует, но ...)?
Профилирование - единственный способ понять производительность вашего приложения. Я был в слишком многих ситуациях, когда люди «оптимизировали» код, но никогда не профилировали. Оказалось, что «оптимизация» привнесла много ошибок и даже не стала горячей точкой в пути кода. Пустая трата времени.
Обновлено:
Несколько человек прокомментировали «раннюю» часть моего ответа. Я не думаю, что вам следует заниматься профилированием с первого дня. Однако вам также не следует ждать до 1 месяца с момента отправки.
Обычно я сначала составляю профилирование, когда у меня есть пара окончательных сквозных сценариев или, в более крупном проекте, в основном функциональный компонент. Я беру день или два (обычно работая с QA), чтобы собрать несколько больших сценариев и бросить их в код. Это отличная выборочная проверка для раннего обнаружения очевидных проблем с производительностью. Исправить их на этом этапе немного проще.
В типичном проекте я обнаружил, что у меня есть код, отвечающий этим критериям на 30-40% прохождения проекта (100% находится в руках клиентов). Я условно причисляю это время к раннему периоду.
Рано относительно. Я не говорю о профильном дне №1. Но в проекте сроком на 1 год я бы начал профилирование не позднее, чем через 3 или 4 месяца, а не до конца.
Вау, без ранней оптимизации? Вы, ребята, ОРЕХИ? :) Одна из наиболее распространенных проблем, с которыми я сталкиваюсь во время оптимизации (а я этим занимаюсь, чтобы заработать себе на жизнь), - это необходимость иметь дело с кодом, который ждал до конца, чтобы затем заставить его работать быстро. Как и рефакторинг для ясности, оптимизацию нужно рассмотреть заранее, чтобы избежать ...
Существует большая разница между оптимизацией дизайна (O (n!) По сравнению с O (log n)) или оптимизацией кода (повторно реализовать некоторую функцию в сборке)
На самом деле разница есть, но я чувствую, что обе потребности в оптимизации будут выявлены путем профилирования и тщательного анализа данных профилирования.
Но иногда просто знаешь. Если я пишу алгоритм обработки изображений, я знаю, что все внутри внутреннего цикла стоит дорого. Я не собираюсь вызывать там кучу геттеров / сеттеров или других лишних функций, если они не встроены, но я, вероятно, просто полностью избегу этого. Я заранее узнаю, что один алгоритм предпочтительнее из-за их сложности выполнения. Я с Торлаком, писать глупый код - это не то, к чему нужно стремиться, а написание эффективного кода, который не ставит под угрозу простоту обслуживания в будущем, не является преждевременной оптимизацией.
«преждевременная оптимизация - корень всех зол» (Кнут, Дональд)
Это действительно зависит от типа кода, который вы пишете, и его типичного использования.
На ум приходит известная цитата:
«Мы должны забыть о небольшой эффективности, скажем, примерно в 97% случаев: преждевременная оптимизация - корень всех зол». (Кнут, Дональд. Структурированное программирование с переходом к утверждениям, ACM Journal Computing Surveys, том 6, № 4, декабрь 1974 г., стр.268.)
Но, возможно, вам все равно не стоит передавать большие структуры данных по значению ... :-)
Обновлено: И, возможно, также избегайте О (N ^ 2) или более сложных алгоритмов ...
Я бы взял алгоритм O (2) в любой день. Это занимает в два раза больше времени, чем O (1)!
Но берегитесь последних 3%, они могут быть убийственными.
O (2) не обязательно занимает вдвое больше времени, чем O (1), см. Определение большого O. Во-вторых, предложение «Избегайте O (N ^ 2) или более» бесполезно. O (N ^ 2) не может быть достигнуто для тысяч задач.
Не пытайтесь оптимизировать, пока это не понадобится. Чтобы узнать, нужно ли это, пройдите в профиль. Не угадай; есть доказательства.
Кроме того, алгоритмические оптимизации обычно имеют большее влияние, чем микро. Использование A-звезды вместо поиска пути методом грубой силы будет быстрее, точно так же, как круги Брезенхема лучше, чем использование sin / cos. Конечно, есть исключения, но они очень (очень) редки (<0,1%). Если у вас хороший дизайн, изменение алгоритма изменяет только один модуль в вашем коде. Легкий.
один простой совет - выработать привычку делать ++ i, а не i ++. i ++ делает копию, и это может быть дорого.
Я сам использую это для всех приращений в отдельной строке (что составляет 90% от - и ++).
Я хотел бы думать, что компиляторы оптимизировали бы (неиспользованное) возвращаемое значение i ++, но вы правы - при перегрузке это две разные функции.
Держите свой код как можно более чистым. Компиляторы сегодня ЧУДЕСНЫЕ. Затем, если у вас есть проблема с перфомансом, введите profile.
Все это делается после выбора лучших алгоритмов, доступных для вашей проблемы.
Лучшее, что вы можете сделать с точки зрения производительности, - это начать с твердой архитектуры и модели потоковой передачи. Все остальное будет построено на этом, поэтому, если ваша основа неубедительна, ваш готовый продукт будет не хуже этого. Профилирование происходит немного позже, и даже позже приходят микрооптимизации (в общем, они несущественны и усложняют код больше всего на свете).
Мораль этой истории такова: начните с эффективной основы, опирайтесь на осознание того, что нельзя делать что-то совершенно глупое и медленное, и все будет в порядке.
Я не согласен с вашим взглядом на микрооптимизации. В основном (для меня) они связаны с перемещением фрагментов кода, чтобы переменные были близки к операциям, изменяющим их, и т. д., А также перемещению вещей алгебраически. Я стараюсь не убивать читабельность кода при рефакторинге / оптимизации.
Если вы просто перемещаете вещи, вероятно, компилятор все равно сделает это за вас.
Вот несколько:
Эффективно используйте Встраивание (в зависимости от вашей платформы).
По возможности избегайте использования временные (и знайте, что это такое)
х = у + г;
Было бы лучше оптимизировать, если бы было написано как:
х = у;
х + = z;
Также избегайте виртуальные функции и создавайте объекты только тогда, когда вам нужно их использовать.
Если у вас хорошее настроение, посмотрите Эффективный C++. У меня есть копия дома, когда я учился в школе.
Следует отметить, что x, y и z будут экземплярами класса, а не PoD, и что оператор + дорогостоящий, а временные конструкции дороги.
Да, при таком ограничении это имеет смысл.
Проверьте факты, сегодня все компиляторы реализуют оптимизацию возвращаемого значения и оптимизацию именованного возвращаемого значения. Использование «x = y + z» позволяет вам сделать «x» константой, что может привести к лучшей оптимизации, большей читабельности и меньшему количеству ошибок ... Возможно, не всегда стоит удалять «небольшой» временный элемент.
Часть об отказе от использования временных файлов неверна. Я часто делал код быстрее, используя «x = y + z» вместо другого. Профилируйте и посмотрите машинный код, прежде чем вносить подобные микро изменения.
Обновлено: я действительно имел в виду некорректность временных. И это относится и к типам, отличным от POD.
if или switch вместо вызовов через указатели функций. Уточнение: void doit(int m) { switch(m) { case 1: f1(); break; case 2: f2(); break; } } вместо void doit(void(*m)()) { m(); } может встроить вызовы.++t вместо t++.const как можно раньше. Самое главное, чтобы улучшить читаемость.new к минимуму. Всегда предпочитайте автоматические переменные (в стеке), если это возможноT t[N] = { };, если вам нужны нули.operator()). Они встраиваются лучше, чем вызовы через указатели на функции.std::vector или std::string, если у вас есть количество фиксированного размера, которое не растет. Используйте boost::array<T, Size> или голый массив и используйте его правильно.И действительно, чуть не забыл:
Преждевременная оптимизация - корень всех зол
Избегайте строк C и используйте класс String для повышения производительности? Я не думаю, что когда-либо преобразовывал строковый код C в управляемый строковый класс по соображениям производительности. Всегда было наоборот. По большей части это сводится к большему контролю над распределением памяти.
Торлак. если у вас есть строки фиксированной длины, последнее правило говорит не использовать вектор (или строку), а использовать голые массивы. но если вам нужно передавать строки, объединять или увеличивать их, вам лучше использовать строковые классы. как по производительности, так и по безопасности.
по одному. но обратите внимание на stackoverflow.com/questions/270408/…. Если ваша структура не имеет настраиваемого конструктора копирования и довольно мала, передача по значению может выиграть.
Используйте существующий проверенный код, который использовался и использовался повторно. (Пример: STL, ускорение или прокатка собственных контейнеров и алгоритмов)
Обновление по комментариям: ПРАВИЛЬНО используйте существующий проверенный код, который использовался и повторно использовался.
Проблема здесь в лицензиях. Тем не менее, есть достаточно места, где это не должно мешать вам (потому что там есть некоторые функции общественного достояния), если только все бесплатные из них не имеют низкой производительности. +1 за то, что поднять это. знак равно
Да, не катите свои собственные, но довольно легко использовать существующие библиотеки неэффективным образом. Возьмем, к примеру, вектор STL. Просто заполнить его несколькими тысячами записей и везде передавать по значению. Фактически, разница в один символ (&) может легко вызвать этот сценарий
Полезно знать, насколько эффективно то, что вы используете. Насколько быстро сложение с умножением, насколько быстро вектор сравнивается с нормальным массивом или с более высокими масштабами, как сравниваются определенные алгоритмы. Это позволяет выбрать наиболее эффективный инструмент для решения задачи.
Использование общих алгоритмов - отличный совет по оптимизации - не с точки зрения времени выполнения, а времени написания кода. Зная, что вы можете сортировать (начало, конец) и ожидать, что диапазон - будь то два указателя или итераторы для базы данных - будет отсортирован (и, более того, используемый алгоритм также будет эффективным во время выполнения). Универсальное программирование - это то, что делает 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»).
Отличный момент! Нет необходимости помещать ключевое слово inline в ваши небольшие функции, если вы используете их с помощью указателей. Указанные функции НИКОГДА не встраиваются, какие бы ключевые слова ни использовались. Помните об этом и используйте функторы.
strager: Необходим <int>, см. обновление к моему сообщению. То же самое и с функционоидами.
хороший пост. хотя моя точка зрения против функции ptrs и для switch / if больше похожа на if (method == 1) return log10 (value); иначе вернуть журнал (vaue); а не double (* method) (double) = & log10; ... метод (значение); Я бы все равно предпочел, если бы против boost :: function и объекта функции здесь (нельзя использовать шаблон!)
litb: Ну я в тупике. o.O… Я никогда не видел, чтобы кто-то делал это слишком расточительным использованием какой-либо функции. ;-)
Спасибо за обновление; Я не понимал этого в отношении функциональных объектов. знак равно
Остерегайтесь функторов, это накладные расходы. Часто компилятор не может оптимизировать шаблоны, а также встроенный код. Я видел простые функторы, не использующие регистры, в отличие от встроенного кода. Это одна из причин, по которой Скотт Мейер ошибся в своей статье о рукописных петлях.
@Torlack, я был бы очень благодарен за пример, потому что это то, что у меня проблемы вообразить. Обычно небольшие (шаблонные) функторы полностью встроены и извлекают выгоду из всех оптимизаций регистров компилятора. Вы уверены, что там были неиспользуемые регистры?
Рассмотрите возможность использования пул памяти.
Два лучших совета для C++:
Купите эффективный C++, Скотт Мейерс.
Затем приобретите более эффективный C++ от Скотта Мейерса.
Еще один момент: Самый быстрый код - это код, которого не существует.. Это означает, что чем более надежным и полнофункциональным должен быть ваш проект, тем он будет медленнее. Итог: по возможности избегайте лишних слов, но при этом убедитесь, что вы по-прежнему соответствуете требованиям.
Всегда старайтесь думать о том, как выглядит ваша память - например, массив - это последовательная строка памяти размером 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-й фрагмент должен будет снова и снова извлекать новые ячейки массива в ячейки.
Когда ваше приложение начинает замедляться, используйте тест производительности, чтобы найти точное узкое место
даже простые вызовы GetTickCount могут использоваться для определения времени, необходимого для работы ваших компонентов.
В более крупных проектах используйте соответствующий профилировщик, прежде чем начинать оптимизацию, чтобы вы потратили максимум усилий на оптимизацию там, где это важно.
Не используйте крайне неэффективные алгоритмы, включите оптимизацию в своем компиляторе, не оптимизируйте ничего, если профилировщик не покажет, что это узкое место, и когда вы пытаетесь улучшить вещи, проверьте, хорошо вы или плохо сделали. Помните также, что функции библиотеки обычно оптимизируются людьми лучше вас.
Практически все остальное второстепенно по сравнению с этим.
По сути, наибольший прирост производительности достигается за счет улучшения алгоритмов. Это означает использование наиболее эффективных алгоритмов и, в свою очередь, наиболее эффективных контейнеров для элементов данных.
Иногда трудно понять, каковы лучшие компромиссы, но, к счастью, разработчики STL имели в виду именно этот вариант использования, и, следовательно, контейнеры STL, как правило, достаточно гибкие, чтобы можно было смешивать и сопоставлять контейнеры в соответствии с требованиями. приложения.
Однако, чтобы полностью реализовать это преимущество, вам необходимо убедиться, что вы не раскрываете внутренний выбор дизайна как часть интерфейса вашего класса / модуля / чего угодно. Ни один из ваших клиентов не должен зависеть за использование вами std::vector. По крайней мере, предоставьте typedef, который они (и вы) можете использовать, что должно позволить изменить вектор на список (или что-то еще) в зависимости от ваших потребностей.
Точно так же убедитесь, что в вашем распоряжении самый широкий выбор отлаженных алгоритмов и контейнеров. Boost и / или TR1 в наши дни очень необходимы.
Какие бы действия вы ни предприняли, чтобы сэкономить несколько циклов, помните следующее: не пытайтесь быть умнее компилятора - измерьте, чтобы проверить выигрыш.
хотя и не затрагивает точную проблему, вот несколько советов:
всегда кодируйте интерфейсы (когда дело доходит до алгоритмов), чтобы вы могли плавно заменить их на эффективные (любыми необходимыми средствами).
Согласен с советом по преждевременной оптимизации. Однако есть несколько рекомендаций, которым я хотел бы следовать во время проектирования, потому что их может быть сложно оптимизировать позже:
new во время выполнения.Первый пункт - непрактичность. Вместо этого попробуйте использовать повторно. Как правило, всегда минимизируйте вызовы на «нижние» уровни.
Из одной из книг по 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;
};
Вышесказанное привлекло мое внимание и помогло мне улучшить свой стиль программирования ... в этой книге есть чем поделиться по этой интересной теме производительности.
Выбирайте наиболее эффективные алгоритмы, используйте меньше памяти, используйте меньше ветвлений, используйте быстрые операции, используйте небольшое количество итераций.
Согласен с Марцином. Измеряйте на ранней стадии только в том случае, если это действительно очевидно при тестировании (взятие 100 вместо 1). Другими словами, измеряйте только тогда, когда это проблема.