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

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

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

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

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

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

Вот связанный вопрос

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

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

Ответы [ 32 ]

72 голосов
/ 15 января 2010

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

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

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

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

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

54 голосов
/ 16 января 2010

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

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;
}

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

47 голосов
/ 15 января 2010

Порядок, в котором вы перебираете память, может сильно повлиять на производительность, а компиляторы не очень хорошо это понимают и исправляют. Вы должны быть внимательны к проблемам локальности кэша, когда пишете код, если заботитесь о производительности. Например, двумерные массивы в 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];
  }
}
36 голосов
/ 16 января 2010

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

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

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

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

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

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

Упрощение разработки алгоритмов

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

Развертывание петли

У каждого цикла есть издержки инкремента и проверки завершения. Чтобы получить оценку фактора производительности, подсчитайте количество инструкций в служебной информации (минимум 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 */;

Короткое замыкание оператора Logical AND (&&) предотвращает выполнение тестов, если 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;
};

Выделение переменной фактора вне циклов

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

Коэффициент константного выражения вне циклов

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

Ввод / вывод в блоках

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

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. Это также позволяет разбивать макет данных на «постоянные секции» и переменные секции. Подумайте о mail-merge .

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

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

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

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

26 голосов
/ 15 января 2010

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

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

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

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

18 голосов
/ 15 января 2010

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

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

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

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

11 голосов
/ 15 января 2010

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

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

11 голосов
/ 15 января 2010

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

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

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

  • использовать списки инициализации конструктора
  • использовать префиксные операторы
  • использовать явные конструкторы
  • встроенные функции
  • избегать временных объектов
  • знать о стоимости виртуальных функций
  • возвращать объекты через ссылочные параметры
  • учитывается распределение по классам
  • рассмотрим распределители контейнера stl
  • Оптимизация 'Пустой элемент'
  • и т.д.
9 голосов
/ 25 марта 2010

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

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

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

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, ...) помните, что они возвращают - место назначения! Зачастую вы можете получить более качественный код, «забыв» свою копию указателя на место назначения и просто забрав его из возврата этих функций.

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

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;
}

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

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

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

9 голосов
/ 07 августа 2012

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

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

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

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

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

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

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...