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

Много лет назад компиляторы C не были особенно умными. В качестве обходного решения K & R изобрел ключевое слово register, чтобы намекнуть на компилятор, возможно, было бы неплохо сохранить эту переменную во внутреннем регистре. Они также заставили третичного оператора помочь генерировать лучший код.

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

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

Какие существуют методы кодирования, которые могут позволить компилятору/оптимизатору генерировать более быстрый код?

  • Идентификация платформы и используемого вами компилятора будет оценена.
  • Почему техника работает?
  • Образец кода рекомендуется.

Вот вопрос

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

[Изменить] Связанная с смещением ссылка

Ответ 1

Записывать локальные переменные, а не выводить аргументы! Это может быть огромной помощью для преодоления спада сглаживания. Например, если ваш код выглядит как

void DoSomething(const Foo& foo1, const Foo* foo2, int numFoo, Foo& barOut)
{
    for (int i=0; i<numFoo, i++)
    {
         barOut.munge(foo1, foo2[i]);
    }
}

компилятор не знает, что foo1!= barOut, и, следовательно, он должен каждый раз перезагружать foo1 через цикл. Он также не может читать foo2 [i], пока запись в barOut не будет завершена. Вы можете начать беспорядок с помощью ограниченных указателей, но это так же эффективно (и намного яснее) сделать это:

void DoSomethingFaster(const Foo& foo1, const Foo* foo2, int numFoo, Foo& barOut)
{
    Foo barTemp = barOut;
    for (int i=0; i<numFoo, i++)
    {
         barTemp.munge(foo1, foo2[i]);
    }
    barOut = barTemp;
}

Звучит глупо, но компилятор может гораздо умнее иметь дело с локальной переменной, поскольку он не может перекрываться в памяти с любым из аргументов. Это может помочь вам избежать ужасного загрузочного магазина (упомянутого Фрэнсисом Бойвином в этой теме).

Ответ 2

Здесь практика кодирования, чтобы помочь компилятору создать быстрый код и любой язык, любую платформу, любой компилятор, любую проблему:

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

Далее, прокомментируйте свой код.

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

Ожидайте разочарования и придется очень тяжело работать для небольших улучшений производительности. Современные компиляторы для зрелых языков, таких как Fortran и C, очень и очень хороши. Если вы читаете учетную запись "трюка", чтобы получить лучшую производительность из кода, имейте в виду, что авторы компилятора также прочитали об этом, и, если это стоит того, возможно, это реализовано. Вероятно, они писали, что вы читаете в первую очередь.

Ответ 3

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

#define N 1000000;
int matrix[N][N] = { ... };

//awesomely fast
long sum = 0;
for(int i = 0; i < N; i++){
  for(int j = 0; j < N; j++){
    sum += matrix[i][j];
  }
}

//painfully slow
long sum = 0;
for(int i = 0; i < N; i++){
  for(int j = 0; j < N; j++){
    sum += matrix[j][i];
  }
}

Ответ 4

Общие оптимизации

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

Объявить небольшие функции как inline или макросы

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

Удалить мертвый и избыточный код

Если код не используется или не вносит вклад в результат программы, избавитесь от него.

Упростить дизайн алгоритмов

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

Развертка Loop

В каждом цикле есть накладные расходы на инкремент и завершение проверки. Чтобы получить оценку коэффициента производительности, подсчитайте количество инструкций в служебных данных (минимум 3: приращение, проверка, начало цикла) и разделите количество операторов внутри цикла. Чем меньше число, тем лучше.

Изменить: предоставить пример разворота цикла До:

unsigned int sum = 0;
for (size_t i; i < BYTES_TO_CHECKSUM; ++i)
{
    sum += *buffer++;
}

После разматывания:

unsigned int sum = 0;
size_t i = 0;
**const size_t STATEMENTS_PER_LOOP = 8;**
for (i = 0; i < BYTES_TO_CHECKSUM; **i = i / STATEMENTS_PER_LOOP**)
{
    sum += *buffer++; // 1
    sum += *buffer++; // 2
    sum += *buffer++; // 3
    sum += *buffer++; // 4
    sum += *buffer++; // 5
    sum += *buffer++; // 6
    sum += *buffer++; // 7
    sum += *buffer++; // 8
}
// Handle the remainder:
for (; i < BYTES_TO_CHECKSUM; ++i)
{
    sum += *buffer++;
}

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

У меня были потрясающие результаты, когда я развернул цикл до 32 операторов. Это было одним из узких мест, поскольку программа должна была рассчитать контрольную сумму в файле размером 2 ГБ. Эта оптимизация в сочетании с считыванием блоков улучшает производительность от 1 часа до 5 минут. Прокрутка цикла обеспечивала отличную производительность на языке ассемблера, мой memcpy был намного быстрее, чем компилятор memcpy. - Т.М.

Сокращение операторов if

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

Булевская арифметика ( Отредактировано: применяемый формат кода для фрагмента кода, добавленный пример)

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

bool status = true;
status = status && /* first test */;
status = status && /* second test */;

Короткое замыкание оператора Логический И (&&) предотвращает выполнение тестов, если status - false.

Пример:

struct Reader_Interface
{
  virtual bool  write(unsigned int value) = 0;
};

struct Rectangle
{
  unsigned int origin_x;
  unsigned int origin_y;
  unsigned int height;
  unsigned int width;

  bool  write(Reader_Interface * p_reader)
  {
    bool status = false;
    if (p_reader)
    {
       status = p_reader->write(origin_x);
       status = status && p_reader->write(origin_y);
       status = status && p_reader->write(height);
       status = status && p_reader->write(width);
    }
    return status;
};

Распределение переменных факторов за пределами циклов

Если переменная создается "на ходу" внутри цикла, переместите создание/выделение до цикла. В большинстве случаев переменная не требуется выделять в течение каждой итерации.

Выражения константы фактора вне петель

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

I/O в блоках

Чтение и запись данных в больших кусках (блоки). Больше лучше. Например, чтение одного октета за раз менее эффективно, чем чтение 1024 октетов с одним чтением.
Пример:

static const char  Menu_Text[] = "\n"
    "1) Print\n"
    "2) Insert new customer\n"
    "3) Destroy\n"
    "4) Launch Nasal Demons\n"
    "Enter selection:  ";
static const size_t Menu_Text_Length = sizeof(Menu_Text) - sizeof('\0');
//...
std::cout.write(Menu_Text, Menu_Text_Length);

Эффективность этого метода может быть визуально продемонстрирована.: -)

Не используйте семейство printf для постоянных данных

Постоянные данные могут выводиться с использованием блочной записи. Форматированная запись будет тратить время на сканирование текста для форматирования символов или обработки команд форматирования. См. Пример кода.

Формат в память, затем напишите

Отформатируйте массив char, используя несколько sprintf, затем используйте fwrite. Это также позволяет разбить структуру данных на "постоянные секции" и переменные секции. Подумайте о слиянии почты.

Объявлять константный текст (строковые литералы) как static const

Когда переменные объявляются без static, некоторые компиляторы могут выделять пространство в стеке и копировать данные из ПЗУ. Это две ненужные операции. Это можно исправить с помощью префикса static.

Наконец, код, подобный компилятору, будет

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

Ответ 5

Оптимизатор на самом деле не контролирует производительность вашей программы. Используйте соответствующие алгоритмы и структуры, профиль, профиль, профиль.

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

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

Что приводит к следующему пункту, прочитайте руководство ^ # [email protected]! GCC может векторизовать простой код C, если вы покроете __restrict__ здесь и __attribute__( __aligned__ ) там. Если вам нужно что-то очень специфическое из оптимизатора, возможно, вам придется быть конкретным.

Ответ 6

На большинстве современных процессоров самым большим узким местом является память.

Сглаживание: Load-Hit-Store может быть разрушительным в узком цикле. Если вы читаете одну ячейку памяти и записываете в другую и знаете, что они не пересекаются, то тщательное размещение ключевого слова alias в параметрах функции может помочь компилятору генерировать более быстрый код. Однако, если области памяти перекрываются, и вы использовали "псевдоним", у вас есть хороший сеанс отладки undefined поведения!

Cache-miss: не совсем уверен, как вы можете помочь компилятору, поскольку он в основном алгоритмический, но есть встроенные средства для предварительной выборки памяти.

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

Ответ 7

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

В этом документе загружены другие советы по оптимизации: Оптимизация CPP (хотя немного старый документ)

основные моменты:

  • Использовать списки инициализации конструктора
  • использовать префиксные операторы
  • использовать явные конструкторы
  • встроенные функции
  • избегать временных объектов
  • знать стоимость виртуальных функций
  • вернуть объекты через ссылочные параметры
  • учитывать при распределении классов
  • рассмотрите распределители контейнеров stl
  • оптимизация "пустого элемента"
  • и т.д.

Ответ 8

Подавляющее большинство кода, которое люди пишут, будет связано с I/O (я считаю, что весь код, который я написал за деньги за последние 30 лет, был настолько привязан), поэтому деятельность оптимизатора для большинства людей будет академично.

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

Ответ 9

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

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

Используйте почти бесплатное сравнение против 0, которое большинство процессоров дает вам при выполнении математических или логических операций. Вы почти всегда получаете флаг для == 0 и < 0, из которого вы можете легко получить 3 условия:

x= f();
if(!x){
   a();
} else if (x<0){
   b();
} else {
   c();
}

почти всегда дешевле, чем тестирование других констант.

Другим трюком является использование вычитания для исключения одного сравнения в тестировании диапазона.

#define FOO_MIN 8
#define FOO_MAX 199
int good_foo(int foo) {
    unsigned int bar = foo-FOO_MIN;
    int rc = ((FOO_MAX-FOO_MIN) < bar) ? 1 : 0;
    return rc;
} 

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

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

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

foo_t * make_foo(int a, int b, int c) {
        foo_t * x = malloc(sizeof(foo));
        if (!x) {
             // return NULL;
             return x; // x is NULL, already in the register used for returns, so duh
        }
        x->a= a;
        x->b = b;
        x->c = c;
        return x;
}

Конечно, вы можете изменить логику на то, что есть и только одна точка возврата.

(трюки, которые я вспомнил позже)

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

Ответ 10

Я написал оптимизирующий компилятор C, и вот некоторые очень полезные вещи:

  • Сделать большинство функций статическими. Это позволяет выполнять межпроцедурное распространение и анализ псевдонимов выполнять свою работу, иначе компилятор должен предположить, что эту функцию можно вызвать извне единицы перевода с полностью неизвестными значениями параметров. Если вы посмотрите на известные библиотеки с открытым исходным кодом, все они отмечают функции static, за исключением тех, которые действительно должны быть extern.

  • Если используются глобальные переменные, помечайте их как статические, так и постоянные. Если они инициализируются один раз (только для чтения), лучше использовать список инициализаторов, например, static const int VAL [] = {1,2,3,4}, иначе компилятор может не обнаружить, что переменные являются фактически инициализированными константами и не сможет заменить нагрузки от переменной на константы.

  • НИКОГДА не используйте goto для внутренней части цикла, цикл больше не будет распознаваться большинством компиляторов, и ни одна из наиболее важных оптимизаций не будет применяться.

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

  • Использовать массивы вместо указателей, когда это возможно, особенно внутри циклов (a [i]). Массив обычно предлагает больше информации для анализа псевдонимов, и после некоторых оптимизаций один и тот же код будет генерироваться в любом случае (поиск любопытства по снижению петли). Это также увеличивает вероятность применения циклического движения кода.

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

  • При написании тестов с несколькими условиями сначала укажите наиболее вероятный. если (a || b || c) должно быть, если (b || a || c), если b, скорее всего, будет истинным, чем остальные. Компиляторы обычно ничего не знают о возможных значениях условий и какие ветки берутся больше (их можно было бы узнать, используя информацию профиля, но мало программистов используют его).

  • Использование переключателя выполняется быстрее, чем выполнение теста, например if (a || b ||... || z). Проверьте сначала, если ваш компилятор делает это автоматически, некоторые делают, и это более читаемо, чтобы иметь, если хотя.

Ответ 11

Тупой маленький наконечник, но тот, который сэкономит вам несколько микроскопических величин скорости и кода.

Всегда передавать аргументы функции в том же порядке.

Если у вас есть f_1 (x, y, z), который вызывает f_2, объявите f_2 как f_2 (x, y, z). Не объявляйте его как f_2 (x, z, y).

Причиной этого является то, что C/С++-платформа ABI (соглашение о вызове AKA) promises передает аргументы в конкретные регистры и местоположения стека. Когда аргументы уже находятся в правильных регистрах, тогда их не нужно перемещать.

При чтении разобранного кода я видел какую-то смешную перестановку в реестре, потому что люди не соблюдали это правило.

Ответ 12

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

Алгоритмы, используемые для управления кучей, на некоторых платформах (например, vxworks) заведомо медленны. Хуже того, время, которое требуется для возврата из вызова в malloc, сильно зависит от текущего состояния кучи. Таким образом, любая функция, вызывающая malloc, собирается получить удар производительности, который нелегко объяснить. Этот удар производительности может быть минимальным, если куча еще чиста, но после этого устройство работает некоторое время, куча может стать фрагментированной. Вызов будет длиться дольше, и вы не можете легко подсчитать, как производительность со временем ухудшится. Вы не можете произвести худшую оценку дела. Оптимизатор не может предоставить вам любую помощь в этом случае. Чтобы ухудшить ситуацию, если куча становится слишком сильно фрагментированной, вызовы начнут вообще терпеть неудачу. Решением является использование пулов памяти (например, glib slices) вместо кучи. Вызов распределения будет намного быстрее и детерминирован, если вы сделаете это правильно.

Ответ 13

Две кодирующие техники, которые я не видел в приведенном выше списке:

Обходной компоновщик, записывая код как уникальный источник

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

Но если вы хорошо проектируете свою программу, вы также можете скомпилировать ее через уникальный общий источник. Это вместо того, чтобы компилировать unit1.c и unit2.c, тогда свяжите оба объекта, скомпилируйте all.c, которые просто #include unit1.c и unit2.c. Таким образом, вы получите выгоду от всех оптимизаций компилятора.

Это очень похоже на запись заголовков только программ на С++ (и еще проще сделать на C).

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

Используя эту простую технику, я сделал несколько программ, которые я написал в десять раз быстрее!

Как и ключевое слово register, этот трюк также может скоро устареть. Оптимизация через компоновщик начинает поддерживаться компиляторами gcc: Оптимизация времени связи.

Отдельные атомарные задачи в циклах

Это сложнее. Это о взаимодействии между дизайном алгоритма и тем, как оптимизатор управляет кешем и распределением регистров. Довольно часто программам приходится перебирать некоторую структуру данных и для каждого элемента выполнять некоторые действия. Довольно часто выполняемые действия могут быть разделены между двумя логически независимыми задачами. Если это так, вы можете написать точно такую ​​же программу с двумя петлями на одной границе, выполняя ровно одну задачу. В некоторых случаях запись этого способа может быть быстрее, чем уникальный цикл (детали более сложны, но объяснение может быть таким, что в случае простой задачи все переменные могут храниться в регистрах процессоров, а с более сложным - это невозможно, а некоторые регистры должны записываться в память и считываться позже, а стоимость выше, чем дополнительное управление потоком).

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

Ответ 14

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

Пример:

int fac2(int x, int cur) {
  if (x == 1) return cur;
  return fac2(x - 1, cur * x); 
}
int fac(int x) {
  return fac2(x, 1);
}

Конечно, этот пример не имеет проверки границ.

Позднее редактирование

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

Ответ 15

Я действительно видел это в SQLite, и они утверждают, что это приводит к увеличению производительности ~ 5%: поместите весь свой код в один файл или используйте препроцессор, чтобы сделать эквивалент этого. Таким образом, оптимизатор будет иметь доступ ко всей программе и может выполнять дополнительные межпроцедурные оптимизации.

Ответ 16

Не делайте ту же самую работу снова и снова!

Общий антипаттерн, который я вижу, идет по этим строкам:

void Function()
{
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomething();
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingElse();
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingCool();
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingReallyNeat();
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingYetAgain();
}

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

void Function()
{
   MySingleton* s = MySingleton::GetInstance();
   AggregatedObject* ao = s->GetAggregatedObject();
   ao->DoSomething();
   ao->DoSomethingElse();
   ao->DoSomethingCool();
   ao->DoSomethingReallyNeat();
   ao->DoSomethingYetAgain();
}

В случае однопользовательского геттера вызовы могут быть не слишком дорогостоящими, но это, безусловно, стоимость (обычно, "проверьте, был ли объект создан, если он этого не сделал, создайте его, а затем верните его). Чем сложнее эта цепочка геттеров, тем больше времени будет упущено.

Ответ 17

  • Используйте самую локальную область, возможную для всех объявлений переменных.

  • Используйте const по возможности

  • Не используйте регистр, если вы не планируете профиль как с ним, так и без него

Первые 2 из них, особенно # 1, помогают оптимизатору проанализировать код. Это особенно поможет ему сделать правильный выбор о том, какие переменные должны храниться в регистрах.

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

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

Ответ 19

Мне вспомнилось что-то, с чем я столкнулся один раз, когда симптом состоял просто в том, что у нас закончилась нехватка памяти, но результат был значительно увеличен (как и значительное сокращение объема памяти).

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

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

Другая часть решения заключалась в замене необузданного использования управляемых вручную элементов char * с строкой SSO (оптимизацией с малой строкой). Минимальное распределение - 32 байта. Я построил строковый класс, у которого был встроенный 28-символьный буфер за char *, поэтому 95% наших строк не нуждались в дополнительном распределении (а затем я вручную заменял почти каждый появление char * в этой библиотеке с этим новым классом, это было весело или нет). Это также помогло тонну с фрагментацией памяти, которая затем увеличила локальность ссылок для других объектов с заостренными объектами, а также увеличилась производительность.

Ответ 20

Яркая техника, которую я узнал из комментария @MSalters о этом ответе, позволяет компиляторам копировать эликсир даже при возвращении разных объектов в соответствии с некоторым условием:

// before
BigObject a, b;
if(condition)
  return a;
else
  return b;

// after
BigObject a, b;
if(condition)
  swap(a,b);
return a;

Ответ 21

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

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

Ответ 22

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

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

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

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

И да, в конечном счете, вы можете столкнуться с комбинаторным взрывом флагов компилятора, поэтому вам нужно иметь script или два, чтобы запустить make с различными флагами компилятора, поставить в очередь задания на большом кластере и собрать время выполнения статистика. Если только вы и Visual Studio на ПК у вас не будет интереса задолго до того, как вы попробуете достаточно комбинаций достаточных флагов компилятора.

Привет

Марк

Когда я впервые забираю кусок кода, я обычно могу получить коэффициент в 1,4 - 2,0 раза большую производительность (т.е. новая версия кода работает в 1/1.4 или 1/2 от времени старой версии ) в течение дня или двух, играя с флагами компилятора. Конечно, это может быть комментарий о недостатке компилятора, который был подкован среди ученых, которые берут на себя большую часть кода, над которым я работаю, а не симптом моего превосходства. Установив флаги компилятора в max (и это редко только -O3), потребуется несколько месяцев тяжелой работы, чтобы получить еще один коэффициент 1,05 или 1,1

Ответ 23

Когда DEC вышла с его альфа-процессорами, была рекомендация сохранить количество аргументов в функции до 7, поскольку компилятор всегда пытался автоматически установить до 6 аргументов в регистрах.

Ответ 24

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

Оптимизатор поможет вашей программе незначительно.

Ответ 25

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

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

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

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

После этого микро-оптимизация (горячих точек) может дать вам хорошую отдачу.

Ответ 26

Я использую компилятор Intel. как в Windows, так и в Linux.

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

если код является вычислительным и содержит большое количество циклов - отчет о векторизации в компиляторе Intel очень полезен - ищите "vec-report" в справке.

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

Ответ 27

Одна оптимизация, которую я использовал в С++, создает конструктор, который ничего не делает. Каждому нужно вручную вызвать init(), чтобы поместить объект в рабочее состояние.

Это имеет преимущество в случае, когда мне нужен большой вектор этих классов.

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

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

Ответ 28

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

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

Ответ 29

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

Ответ 30

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

По этой же идее. Если задавались вопросом, можно ли добиться возможной оптимизации, используя шаблон

for (some silly loop)
if (something)
    if (somthing else)
        if (somthing else)
            if (somthing else)
                /* This is the normal expected case */ 
            else error 4
        else error 3
    else error 2
else error 1

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

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

Комментарии? Мне снится?