Какие методы кодирования вы используете для оптимизации программ на C?

Несколько лет назад я был на панели, которая давала интервью кандидатам для относительно старшей позиции программиста C.

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

Итак, в интересах составления списка для потомков - какие методы и конструкции вы обычно используете при оптимизации программ на С?

Ответы на оптимизацию для скорости и размера как принято.

Ответ 1

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

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

И выясните, почему вам нужно оптимизировать в первую очередь. Чего вы пытаетесь достичь? Если вы пытаетесь, скажем, улучшить время отклика на какое-то событие, если есть возможность изменить порядок выполнения, чтобы свести к минимуму критические области времени. Например, если вы пытаетесь улучшить реакцию на какое-либо внешнее прерывание, можете ли вы сделать какую-либо подготовку в мертвое время между событиями?

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

Итак, что вы можете сделать в этих областях?

  • минимизировать проверку условий. Условия проверки (например, условия завершения циклов) - это время, которое не расходуется на фактическую обработку. Проверка состояния может быть сведена к минимуму с помощью таких методов, как разворот цикла.
  • В некоторых случаях проверка условий также может быть устранена с помощью указателей функций. Например, если вы используете конечный автомат, вы можете обнаружить, что внедрение обработчиков для отдельных состояний в виде небольших функций (с однородным прототипом) и сохранение "следующего состояния" путем хранения указателя функции следующего обработчика более эффективно, чем использование большой оператор switch с кодом обработчика, реализованным в отдельных случаях. YMMV.
  • минимизировать вызовы функций. Функциональные вызовы обычно несут нагрузку на сохранение контекста (например, запись локальных переменных, содержащихся в регистрах в стек, сохранение указателя стека), поэтому, если вам не нужно делать вызов, это время сохраняется. Один из вариантов (если вы оптимизируете скорость, а не пространство) - использовать встроенные функции.
  • Если вызовы функций неизбежны, скройте данные, передаваемые этим функциям. Например, передающие указатели, вероятно, будут более эффективными, чем передача структур.
  • При оптимизации скорости выберите типы данных, которые являются родным для вашей платформы. Например, на 32-битном процессоре, вероятно, будет более эффективно управлять 32-битными значениями, чем 8 или 16-битные значения. (обратите внимание - стоит проверить, что компилятор делает то, что, по вашему мнению, есть. У меня были ситуации, когда я обнаружил, что мой компилятор настаивал на выполнении 16-разрядной арифметики на 8-битных значениях со всеми преобразованиями и от конверсий идти с ними)
  • Найдите данные, которые можно предварительно вычислить, и либо вычислить во время инициализации, либо (еще лучше) во время компиляции. Например, при реализации CRC вы можете либо вычислить свои значения CRC "на лету" (с использованием полинома напрямую), что отлично подходит для размера (но ужасно для производительности), либо вы можете создать таблицу всех промежуточных значений - намного быстрее, в ущерб размеру.
  • Локализовать данные. Если вы манипулируете блобом данных, часто ваш процессор может ускорить процесс, сохраняя все это в кеше. И ваш компилятор может использовать более короткие инструкции, которые подходят для более локализованных данных (например, инструкции, которые используют 8 бит смещения вместо 32 бит).
  • В том же ключе локализуйте свои функции. По тем же причинам.
  • Выполните предположения, которые вы можете сделать о выполняемых вами операциях, и найдите способы их использования. Например, на 8-битной платформе, если единственная операция, которую вы делаете с 32-битным значением, является шагом, вы можете обнаружить, что вы можете сделать лучше, чем компилятор, путем вложения (или создания макроса) специально для этой цели, вместо использования обычной арифметической операции.
  • Избегайте дорогостоящих инструкций. Деление - яркий пример.
  • Ключевое слово "register" может быть вашим другом (хотя, надеюсь, ваш компилятор имеет неплохую идею о вашем использовании реестра). Если вы собираетесь использовать "register", вероятно, вам придется объявить локальные переменные, которые вы хотите сначала зарегистрировать.
  • Будьте в соответствии с вашими типами данных. Если вы делаете арифметику на смеси типов данных (например, шорты и ints, удваиваете и плаваете), то компилятор добавляет неявные преобразования типов для каждого несоответствия. Это потраченные впустую циклы процессора, которые могут не понадобиться.

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

Ответ 2

Как говорили все остальные: профиль , профиль профиля.

Что касается реальных техник, то, о которых я не думаю, уже упоминалось:

Разделение горячих и холодных данных. Оставаться в кэше CPU невероятно важно. Один из способов помочь в этом - это разделить ваши структуры данных на часто используемые ( "горячий" ) и редко доступный ( "холодный" ) раздел.

Пример: предположим, что у вас есть структура для клиента, которая выглядит примерно так:

struct Customer
{
    int ID;
    int AccountNumber;
    char Name[128];
    char Address[256];
};

Customer customers[1000];

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

struct CustomerAccount
{
    int ID;
    int AccountNumber;
    CustomerData *pData;
};

strut CustomerData
{
    char Name[128];
    char Address[256];
}

CustomerAccount customers[1000];

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

Ответ 3

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

Ответ 4

Наиболее распространенными методами, которые я встречал, являются:

  • развертка цикла
  • оптимизация цикла для лучшей выборки кэша (т.е. выполнять N операций в M циклах вместо NxM сингулярных операций)
  • выравнивание данных
  • встроенные функции
  • фрагменты asm, созданные вручную

Что касается общих рекомендаций, большинство из них уже озвучены:

  • выберите лучший альгос
  • использовать профайлер
  • не оптимизируйте, если он не даст повышение производительности на 20-30%.

Ответ 5

Для оптимизации низкого уровня:

  • START_TIMER/STOP_TIMER макросы из ffmpeg (точность на уровне часов для измерения любого кода).
  • Oprofile, конечно, для профилирования.
  • Огромные количества сборок с ручной кодировкой (просто сделайте wc -l в каталоге x264/common/x86, а затем запомните большую часть кода).
  • Тщательное кодирование в целом; более короткий код обычно лучше.
  • Умные низкоуровневые алгоритмы, такие как 64-разрядный писатель битового потока, который я написал, используют только один, если и не больше.
  • Явное объединение писем.
  • Принимая во внимание важные странные аспекты процессоров, такие как проблема с расценкой на кеш-сервер Intel.
  • Поиск случаев, когда можно без потерь или почти без потерь сделать раннее завершение, когда проверка раннего завершения стоит гораздо меньше, чем скорость, которую он получает от нее.
  • Фактически встроенная сборка для задач, которые намного больше подходят для модуля x86 x86, таких как медианные вычисления (требуется проверка времени компиляции для поддержки MMX).

Ответ 6

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

Ответ 7

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

char buf[1024] = { 0, };
/* becomes: */
char buf[1024];
memset(buf, 0, sizeof(buf));

Это приведет к удалению 1024 нулевых байтов из секции .DATA и вместо этого создаст буфер в стеке во время выполнения и заполнит его нулями.

EDIT: О да, и мне нравится кэшировать вещи. Это не C, но в зависимости от того, что вы кешируете, это может дать вам огромный импульс производительности.

PS: Пожалуйста, сообщите нам, когда ваш список закончен, мне очень любопытно.;)

Ответ 8

Избегайте использования кучи. Используйте устройства с препятствиями или пул-распределитель для объектов одинакового размера. Поместите мелкие вещи с коротким сроком службы на стек. alloca все еще существует.

Ответ 9

Предварительно зрелая оптимизация - это корень всего зла! ;)

Ответ 10

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

Например, если возможно, напишите

for (i=n; i!=0; --i) { ... }

вместо

for (i=0; i!=n; ++i) { ... }

Ответ 11

Другая вещь, о которой не упоминалось:

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

Ответ 12

Основа/общая:

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

некоторые вещи, которые действительно помогли:

Выберите размер/память:

  • Использование битовых полей для хранения bools
  • повторно использовать большие глобальные массивы путем наложения на объединение (будьте осторожны)

Выбирайте скорость (будьте осторожны):

  • по возможности используйте предварительно вычисленные таблицы
  • размещение важных функций/данных в быстрой памяти
  • Использовать специальные регистры для часто используемых глобальных переменных
  • count to-zero, флаг нуля свободен

Ответ 13

Трудно суммировать...

  • Структуры данных:

    • Разделение структуры данных в зависимости от случая использования чрезвычайно важно. Обычно рассматривается структура, в которой хранятся данные, которые доступны на основе управления потоком. Эта ситуация может значительно снизить использование кеша.
    • Принимать во внимание правила размера строки кэша и правила предварительной выборки.
    • Чтобы изменить порядок членов структуры, чтобы получить последовательный доступ к ним из вашего кода
  • Алгоритмы:

    • Найдите время, чтобы подумать о своей проблеме и найти правильный алгоритм.
    • Знайте ограничения алгоритма, который вы выбрали (сортировка/сортировка radix для 10 элементов, которые нужно отсортировать, может быть не лучшим выбором).
  • Низкий уровень:

    • Что касается новейших процессоров, не рекомендуется разворачивать цикл с небольшим телом. Процессор обеспечивает свой собственный механизм обнаружения для этого и будет замыкать весь участок его конвейера.
    • Доверяйте HW prefetcher. Конечно, если ваши структуры данных хорошо разработаны;)
    • Обратите внимание на пропуски строк в кэше L2.
    • Постарайтесь максимально уменьшить локальный рабочий набор вашего приложения, поскольку процессоры склоняются к меньшим кешам на ядрах (C2D пользовался максимальным объемом 3 Мбайт на ядро, где iCore7 обеспечит максимальную 256 КБ на ядро ​​+ 8 МБ, общую для всех сердечники для четырехъядерных кристаллов.).

Самое важное: Мера рано, Мера часто и никогда не делает предположений, основывайте свое мышление и оптимизацию на данных, полученных профилировщиком (используйте PTU).

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

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

Ответ 14

В наши дни важнее всего оптимизировать:

  • соблюдение кеша - попробуйте получить доступ к памяти в простых шаблонах и не разворачивайте петли просто для удовольствия. Используйте массивы вместо структур данных с большим количеством преследователей указателей, и это, вероятно, будет быстрее для небольших объемов данных. И не делайте ничего слишком большого.
  • избегая латентности - старайтесь избегать делений и вещей, которые замедляются, если другие вычисления напрямую зависят от них. Доступ к памяти, зависящий от других обращений к памяти (т.е. [B [c]]) плох.
  • избегая непредсказуемости - много случаев, когда непредсказуемые условия или условия, которые приводят к большей задержке, действительно будут вас путать. Там много нераскрытых математических трюков, которые здесь полезны, но они увеличивают задержку и полезны только в том случае, если они вам действительно нужны. В противном случае просто напишите простой код и не получите сумасшедших условий цикла.

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

Ответ 15

Сбор профилей исполнения кода дает вам 50% пути. Другие 50% занимаются анализом этих отчетов.

Кроме того, если вы используете GCC или VisualС++, вы можете использовать "оптимизацию с учетом профиля", где компилятор будет получать информацию от предыдущих исполнений и переназначать инструкции, чтобы сделать процессор более счастливым.

Ответ 16

Встроенные функции! Вдохновленный профилирующими вентиляторами здесь я профилировал мое приложение и обнаружил небольшую функцию, которая немного ускоряет работу с MP3-фреймами. Это составляет около 90% всех вызовов функций в моем приложении, поэтому я сделал это inline и voila - теперь программа использует половину процессорного времени, которое она делала раньше.

Ответ 17

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

Первое правило в оптимизации скорости - найти свой критический путь.
Обычно вы обнаружите, что этот путь не так длинный и не такой сложный. Трудно сказать в общих чертах, как оптимизировать это, это зависит от того, что вы делаете и что в ваших силах сделать. Например, вы обычно избегаете memcpy по критическому пути, поэтому когда-либо вам нужно использовать DMA или оптимизировать, но что делать, если у вас нет DMA? проверьте, является ли реализация memcpy лучшей, если не переписать ее.
Не используйте динамическое размещение вообще во встроенных, но если вы по какой-то причине не делаете этого в критическом пути.
Правильно упорядочивайте приоритеты своих потоков, что правильно - это реальный вопрос, и это явно зависит от конкретной системы.
Мы используем очень простые инструменты для анализа бутылочных шеек, простых макросов, которые хранят метку времени и индекс. Мало (2-3) работает в 90% случаев найдет, где вы проводите свое время.
И последнее - это обзор кода очень важный. В большинстве случаев мы очень часто избегаем проблем с производительностью при просмотре кода.

Ответ 18

  • Измерение производительности.
  • Используйте реалистичные и нетривиальные ориентиры. Помните, что "все быстро для малого N" .
  • Используйте профайлер, чтобы найти горячие точки.
  • Сокращение количества распределений динамической памяти, доступа к диску, доступа к базам данных, доступа к сети и переходов пользователя/ядра, поскольку они часто имеют тенденцию быть горячими точками.
  • Измерение производительности.

Кроме того, вы должны измерять производительность.

Ответ 19

Иногда вам приходится решать, будет ли вам больше пространства или больше скорости, что приведет к почти противоположным оптимизации. Например, чтобы получить максимальную отдачу от вас, вы pack структуры, например. #pragma pack (1) и используйте битовые поля в структурах. Для большей скорости вы устанавливаете для согласования с предпочтениями процессоров и избегаете бит-полей.

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

Ответ 20

Если у кого-то нет ответа на этот вопрос, возможно, они мало что знают.

Также может быть, что они знают много. Я много знаю (ИМХО:-), и если бы меня задали этот вопрос, я бы спросил вас: почему вы думаете, что важно?

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

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

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

Чтобы дать вам конкретный пример, это приложение C, которое было оптимизировано.

Ответ 21

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

  • объединительный компоновщик

    Если у вас есть приложение, разделенное на два файла, скажем main.c и lib.c, во многих случаях вы можете просто добавить \#include "lib.c" в свой main.c. Это полностью обходит компоновщик и позволяет значительно повысить эффективность оптимизации для компилятора.

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

Ответ 22

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

Ответ 23

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

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

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

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

Мои главные инструменты для быстрого кода:

hashtable - для быстрого поиска и результатов кэширования

qsort - это единственный вид, который я использую

bsp - для поиска вещей на основе области (рендеринг карты и т.д.)