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

116

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

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

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

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

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

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

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

[Edit] Ссылка на смещение

EvilTeach
источник
7
Может быть хорошим кандидатом для вики сообщества, имхо, поскольку на этот (интересный) вопрос нет «единого» окончательного ответа ...
ChristopheD
Я скучаю по нему каждый раз. Спасибо, что указали на это.
EvilTeach
Под «лучше» вы имеете в виду просто «быстрее» или у вас есть другие критерии совершенства?
High Performance Mark
1
Достаточно сложно написать хороший распределитель регистров, особенно переносимый, а распределение регистров абсолютно необходимо для производительности и размера кода. registerфактически сделал чувствительный к производительности код более переносимым за счет борьбы с плохими компиляторами.
Potatoswatter
1
@EvilTeach: вики сообщества не означает «нет окончательного ответа», это не синоним субъективного тега. Вики сообщества означает, что вы хотите передать свой пост сообществу, чтобы другие люди могли его редактировать. Не испытывайте давления на вики со своими вопросами, если вам этого не хочется.
Джульетта

Ответы:

54

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

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

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

celion
источник
7
Это имеет дополнительное преимущество, так как часто упрощает чтение / понимание для программистов, поскольку им также не нужно беспокоиться о возможных неочевидных побочных эффектах.
Майкл Берр,
Большинство IDE по умолчанию отображают локальные переменные, поэтому печатать меньше
EvilTeach
9
вы также можете включить эту оптимизацию с помощью ограниченных указателей
Бен Войгт,
4
@Ben - это правда, но я думаю, что так понятнее. Кроме того, если ввод и вывод действительно перекрывались, я считаю, что результат не указан с ограниченными указателями (возможно, будет различное поведение между отладкой и выпуском), тогда как этот способ будет, по крайней мере, согласованным. Не поймите меня неправильно, мне нравится использовать ограничение, но мне даже больше нравится, что он не нужен.
Celion
Вам просто нужно надеяться, что в Foo не определена операция копирования, которая копирует пару
мегабайт
76

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

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

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

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

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

High Performance Mark
источник
20
У разработчиков Compiier, как и у всех, есть конечное время. Не все оптимизации попадут в компилятор. Как &против %для степени двойки (редко, если вообще оптимизируется, но может иметь значительное влияние на производительность). Если вы читали трюк для повышения производительности, единственный способ узнать, работает ли он, - это внести изменения и измерить влияние. Никогда не предполагайте, что компилятор что-то оптимизирует за вас.
Дэйв Джарвис,
22
& и% почти всегда оптимизированы, как и большинство других арифметических трюков, которые не нужны. Что не оптимизируется, так это случай, когда правый операнд представляет собой переменную, которая всегда оказывается степенью двойки.
Potatoswatter
8
Чтобы уточнить, я, кажется, сбил с толку некоторых читателей: совет по практике кодирования, который я предлагаю, состоит в том, чтобы сначала разработать простой код, который не использует инструкции разметки памяти для определения базового уровня производительности. Затем попробуйте что-то по очереди и оцените их влияние. Никаких советов по выполнению операций я не давал.
High Performance Mark
17
Для постоянной степени двойки ngcc заменяет % nна, & (n-1) даже если оптимизация отключена . Это не совсем «редко, если вообще» ...
Porculus
12
% НЕ МОЖЕТ быть оптимизирован как &, когда тип подписан, из-за идиотских правил C для отрицательного целочисленного деления (округление до 0 и отрицательный остаток, а не округление в меньшую сторону и всегда положительный остаток). И в большинстве случаев невежественные программисты используют подписанные типы ...
R .. GitHub НЕ ПОМОГАЕТ ICE
47

Порядок, в котором вы просматриваете память, может иметь сильное влияние на производительность, и компиляторы не очень хороши в выяснении этого и исправлении. Если вы заботитесь о производительности, при написании кода необходимо учитывать проблемы расположения кеша. Например, двумерные массивы в 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];
  }
}
vicatcu
источник
Строго говоря, это не проблема оптимизатора, а проблема оптимизации.
EvilTeach
10
Конечно, проблема с оптимизатором. На протяжении десятилетий люди писали статьи об оптимизации автоматической смены контуров.
Фил Миллер,
20
@Potatoswatter О чем ты говоришь? Компилятор C может делать все, что хочет, до тех пор, пока сохраняется тот же конечный результат, и действительно, в GCC 4.4 есть, -floop-interchangeкоторый будет менять местами внутренний и внешний цикл, если оптимизатор сочтет это выгодным.
ephemient
2
Ха, ну вот и все. Семантика C часто нарушается из-за проблем с псевдонимом. Думаю, настоящий совет здесь - передать этот флаг!
Potatoswatter
36

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

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

Объявите небольшие функции как 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 */;

Короткое замыкание из Логических И оператора ( &&) предотвращает выполнение тестов , если 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 октета за одно чтение.
Пример:

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префикса.

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

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

Томас Мэтьюз
источник
2
Интересно, можете ли вы привести пример, когда вы получили лучший код с несколькими небольшими операторами вместо более крупного. Можете ли вы показать пример переписывания if с использованием логических значений. Как правило, я бы оставил развертывание цикла компилятору, так как он, вероятно, лучше чувствует размер кеша. Я немного удивлен самой идеей спринтфинга, а затем письма. Я думаю, что fprintf действительно делает это под капотом. Не могли бы вы дать здесь более подробную информацию?
EvilTeach
1
Нет гарантии, что fprintfформатирование в отдельный буфер затем выводит буфер. Оптимизированный (для использования памяти) fprintfбудет выводить весь неформатированный текст, затем форматировать и выводить и повторять до тех пор, пока не будет обработана вся строка формата, таким образом выполняя 1 выходной вызов для каждого типа вывода (форматированный или неформатированный). В других реализациях потребуется динамически выделять память для каждого вызова для хранения всей новой строки (что плохо для среды встроенных систем). Мое предложение уменьшает количество выходов.
Thomas Matthews
3
Однажды я добился значительного улучшения производительности, свернув цикл. Затем я придумал, как скрутить его с помощью косвенного обращения, и программа стала заметно быстрее. (Профилирование показало, что эта конкретная функция занимает 60-80% времени выполнения, и я тщательно тестировал производительность до и после.) Я считаю, что улучшение произошло за счет лучшей локальности, но я не совсем уверен в этом.
Дэвид Торнли
16
Многие из них являются оптимизацией для программистов, а не способами, с помощью которых программисты могут помочь компилятору в оптимизации, на что был направлен исходный вопрос. Например, разворачивание цикла. Да, вы можете выполнить свое собственное развертывание, но я думаю, что более интересно выяснить, какие препятствия существуют на пути компилятора к развертыванию для вас и их удалению.
Адриан Маккарти
26

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

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

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

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

Potatoswatter
источник
14
Это хороший ответ, но обратите внимание, что оптимизация всей программы становится все более популярной и фактически может встраивать функции в единицы перевода.
Фил Миллер,
1
@Novelocrat Ага - разумеется, я был очень удивлен, когда впервые увидел что-то из A.cвстроенного B.c.
Джонатон Рейнхарт
18

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

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

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

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

Фрэнсис Бойвин
источник
4
+1 для загрузки-хитов и различных типов регистров. Я не уверен, насколько это важно для x86, но они проигрывают в PowerPC (например, Xbox360 и Playstation3).
Celion
Большинство статей по методам оптимизации цикла компилятора предполагают идеальную вложенность, что означает, что тело каждого цикла, кроме самого внутреннего, является просто другим циклом. В этих статьях просто не обсуждаются шаги, необходимые для такого обобщения, даже если совершенно ясно, что они могут быть такими. Таким образом, я ожидал, что многие реализации не будут поддерживать эти обобщения из-за дополнительных усилий. Таким образом, многие алгоритмы для оптимизации использования кэша в циклах могут работать намного лучше с идеальными гнездами, чем с несовершенными.
Фил Миллер
11

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

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

мин
источник
7
Признаюсь, что был своеобразным - я работаю над большими научными кодами, которые вычисляют числа, которые ограничены пропускной способностью памяти. Что касается большинства программ, я согласен с Нилом.
High Performance Mark
6
Правда; но очень большая часть этого кода, связанного с вводом-выводом, в настоящее время написана на языках, которые практически являются пессимизаторами, - на языках, у которых даже нет компиляторов. Я подозреваю, что области, в которых все еще используются C и C ++, будут, как правило, областями, где более важно что-то оптимизировать (использование ЦП, использование памяти, размер кода ...)
Porculus
3
Большую часть последних 30 лет я работал над кодом с очень небольшим количеством операций ввода-вывода. Сохраните 2 года работы с базами данных. Графика, системы управления, моделирование - все это не связано с вводом / выводом. Если бы ввод-вывод был узким местом для большинства людей, мы бы не стали уделять Intel и AMD много внимания.
phkahler
2
Да, я действительно не верю этому аргументу - иначе мы (на моей работе) не искали бы способов тратить больше вычислительного времени на ввод-вывод. Кроме того, большая часть программного обеспечения, связанного с вводом-выводом, с которым я столкнулся, была привязана к вводу-выводу, потому что ввод-вывод выполнялся небрежно; если оптимизировать шаблоны доступа (как и в случае с памятью), можно получить огромный прирост производительности.
dash-tom-bang
3
Недавно я обнаружил, что почти ни один код, написанный на языке C ++, не привязан к вводу-выводу. Конечно, если вы вызываете функцию ОС для массовой передачи диска, ваш поток может перейти в режим ожидания ввода-вывода (но с кешированием, даже это сомнительно). Но обычные функции библиотеки ввода-вывода, которые все рекомендуют, потому что они стандартные и переносимые, на самом деле ужасно медленны по сравнению с современными дисковыми технологиями (даже с умеренными ценами). Скорее всего, ввод-вывод является узким местом, только если вы полностью сбрасываете на диск после записи всего нескольких байтов. OTOH, UI - другое дело, мы люди медленные.
Бен Фойгт,
11

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

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

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

  • использовать списки инициализации конструктора
  • использовать префиксные операторы
  • использовать явные конструкторы
  • встроенные функции
  • избегать временных объектов
  • быть в курсе стоимости виртуальных функций
  • возвращать объекты через ссылочные параметры
  • учитывать распределение по классам
  • рассмотрите распределители контейнеров stl
  • оптимизация "пустого члена"
  • и т.д
Жаба
источник
8
Не много, редко. Тем не менее, это улучшает фактическую правильность.
Potatoswatter
5
В C и C ++ компилятор не может использовать константу для оптимизации, потому что ее отбрасывание - это четко определенное поведение.
dsimcha
+1: const - хороший пример того, что напрямую влияет на скомпилированный код. Комментарий re @ dsimcha - хороший компилятор проверит, происходит ли это. Конечно, хороший компилятор «найдет» константные элементы, которые все равно не объявлены таким образом ...
Хоган
@dsimcha: Изменение const и restrict квалифицированного указателя, однако, не определен. Таким образом, в этом случае компилятор может оптимизировать иначе.
Дитрих Эпп
6
@dsimcha отбрасывая constна constссылку или constуказатель на не- constобъекта хорошо определена. изменение фактического constобъекта (т. е. того, который был объявлен constизначально) - нет.
Стивен Лин
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, ...) помните, что они возвращают - адрес назначения! Часто можно улучшить код, «забыв» свою копию указателя на пункт назначения и просто забрав ее обратно из возврата этих функций.

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

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

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

(приемы вспомнил позже)

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

nategoose
источник
2
На самом деле нет необходимости использовать вычитание при тестировании диапазонов, LLVM, GCC и мой компилятор, по крайней мере, делают это автоматически. Мало кто, вероятно, поймет, что делает код с вычитанием, а еще меньше - почему он действительно работает.
Gratian Lup
в приведенном выше примере нельзя вызвать b (), потому что если (x <0), то будет вызываться a ().
EvilTeach
@EvilTeach Нет, не будет. Сравнение, которое приводит к вызову a (), это! X
nategoose
@nategoose. если x равен -3, то! x истинно.
EvilTeach
@EvilTeach В C 0 ложно, а все остальное верно, поэтому -3 верно, поэтому! -3 неверно
nategoose
9

Я написал оптимизирующий компилятор 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. Использование переключателя быстрее, чем выполнение теста, такого как if (a || b || ... || z). Сначала проверьте, делает ли ваш компилятор это автоматически, некоторые делают, и, тем не менее, будет удобнее иметь if .

Gratian Lup
источник
7

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

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

оборота фигурасса
источник
Мое эмпирическое правило: если вам нужно динамически распределять, получите массив, чтобы вам не нужно было делать это снова. Предварительно распределите им векторы.
EvilTeach
7

Небольшой тупой совет, но он сэкономит вам микроскопическое количество скорости и кода.

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

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

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

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

Zan Lynx
источник
2
Ни C, ни C ++ не дают никаких гарантий и даже не упоминают передачу определенных регистров или местоположений стека. Именно ABI (например, Linux ELF) определяет детали передачи параметров.
Emmet
5

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

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

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

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

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

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

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

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

Разделяйте атомарные задачи в циклах

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

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

крис
источник
2
Да, к настоящему времени LTO сделал первую половину этого поста излишней и, вероятно, плохим советом.
underscore_d
@underscore_d: все еще есть некоторые проблемы (в основном связанные с видимостью экспортируемых символов), но с точки зрения производительности, вероятно, больше нет необходимости.
Крисс
4

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

dsimcha
источник
5
Размещение функций, которые используются вместе, в непосредственной физической близости в источнике, увеличивает вероятность того, что они будут находиться рядом друг с другом в объектных файлах и рядом друг с другом в вашем исполняемом файле. Эта улучшенная локальность инструкций может помочь избежать промахов кэша инструкций во время работы.
paxos1977
В компиляторе AIX есть переключатель компилятора, поддерживающий такое поведение -qipa [= <suboptions_list>] | -qnoipa Включает или настраивает класс оптимизаций, известный как межпроцедурный анализ (IPA).
EvilTeach
4
Лучше всего иметь способ развития, который этого не требует. Использование этого факта в качестве предлога для написания немодульного кода в целом приведет к тому, что код будет медленным и имеет проблемы с обслуживанием.
Hogan,
3
Думаю, эта информация немного устарела. Теоретически функции оптимизации всей программы, встроенные сейчас во многие компиляторы (например, «Оптимизация времени компоновки» в gcc), дают те же преимущества, но с полностью стандартным рабочим процессом (плюс более быстрое время перекомпиляции, чем при помещении всего этого в один файл. !)
Ponkadoodle
@Wallacoloo Конечно, это уже давно устарело. FWIW, я только что впервые использовал LTO GCC сегодня, и - при прочих равных -O3- он выбросил 22% от исходного размера моей программы. (Это не связано с процессором, поэтому мне особо нечего сказать о скорости.)
underscore_d
4

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

Пример:

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 были специально разработаны, чтобы его можно было оптимизировать с помощью конечной рекурсии.

Hogan
источник
1
вопрос касается C. C не удаляет хвостовую рекурсию, поэтому хвостовая или другая рекурсия, стек может взорваться, если рекурсия заходит слишком глубоко.
Toad
1
Я избежал проблемы соглашения о вызовах, используя goto. Таким образом меньше накладных расходов.
EvilTeach
2
@hogan: для меня это в новинку. Не могли бы вы указать на какой-нибудь компилятор, который это делает? И как вы можете быть уверены, что он действительно оптимизирует его? Если это произойдет, нужно быть уверенным, что он это сделает. Вы не надеетесь, что оптимизатор компилятора уловит это (например, встраивание, которое может работать, а может и не работать)
Toad
6
@hogan: Я поправляюсь. Вы правы, что и Gcc, и MSVC оптимизируют хвостовую рекурсию.
Toad
5
Этот пример не является хвостовой рекурсией, поскольку это не последний рекурсивный вызов, а умножение.
Брайан Янг,
4

Не выполняйте одну и ту же работу снова и снова!

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

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

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

штрих-кот-бэнг
источник
3
  1. Используйте максимально локальную область видимости для всех объявлений переменных.

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

  3. Не используйте регистрацию, если вы не планируете профилировать как с ней, так и без нее.

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

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

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

Джон Кнеллер
источник
3

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

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

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

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

штрих-кот-бэнг
источник
3

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

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

// after
BigObject a, b;
if(condition)
  swap(a,b);
return a;
оборота Xeo
источник
2

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

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

Remy
источник
Преобразование рекурсии в стек - это предполагаемая оптимизация на ompf.org для людей, разрабатывающих трассировщики лучей и пишущих другие алгоритмы рендеринга.
Том
... Я должен добавить к этому, что самые большие накладные расходы в моем личном проекте трассировщика лучей - это рекурсия на основе vtable через иерархию ограничивающего объема с использованием шаблона Composite. На самом деле это просто набор вложенных блоков, структурированных как дерево, но использование шаблона вызывает раздувание данных (указатели виртуальных таблиц) и снижает согласованность инструкций (то, что может быть маленьким / плотным циклом, теперь является цепочкой вызовов функций)
Том,
2

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

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

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

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

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

С уважением

отметка

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

Знак высокой эффективности
источник
2

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

EvilTeach
источник
Бит x86-64 также позволяет передавать множество параметров, передаваемых через регистр, что может существенно повлиять на накладные расходы на вызов функции.
Том
1

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

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

Ariel
источник
3
Это работает только в том случае, если сами «интерфейсы» связи поддаются оптимизации. Интерфейс может быть по своей сути "медленным", например, из-за принудительного выполнения избыточных поисков или вычислений или принудительного доступа к плохому кешу.
Том
1

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

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

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

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

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

оборота Майк Данлэйви
источник
1

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

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

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

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

JF.
источник
Вы приближаетесь к ответу на вопрос ... какие действия вы делаете с кодом, чтобы компилятор мог выполнять такие виды оптимизации?
EvilTeach
1
Пытаться писать больше в стиле C (по сравнению с C ++), например, избегать виртуальных функций без абсолютной необходимости, особенно если они будут часто вызываться, избегайте AddRefs ... и всего остального (опять же, если это действительно не нужно). Напишите код, который будет легко встраивать - меньше параметров, меньше «если» -ов. Не используйте глобальные переменные без крайней необходимости. В структуре данных - сначала помещайте более широкие поля (double, int64 идет перед int) - поэтому компилятор выравнивает структуру по естественному размеру первого поля - выравнивание хорошо для perf.
jf.
1
Расположение данных и доступ к ним абсолютно необходимы для производительности. Итак, после профилирования - я иногда разбиваю структуру на несколько в зависимости от местоположения доступа. Еще одна общая уловка - используйте int или size-t вместо char - даже значения данных малы - избегайте различных перфомансов. штрафы хранить до блокировки загрузки, проблемы с частичными остановками регистров. конечно, это не применимо, когда нужны большие массивы таких данных.
jf.
Еще одно - избегайте системных вызовов, если в этом нет реальной необходимости :) - они ОЧЕНЬ дороги
jf.
2
@jf: Я поставил +1 к вашему ответу, но не могли бы вы переместить ответ из комментариев в тело ответа? Будет легче читать.
Крисс
1

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

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

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

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

EvilTeach
источник
6
Я считаю, что типичная реализация std :: vector на самом деле не создает больше объектов, когда вы резервируете () больше емкости. Он просто выделяет страницы. Конструкторы вызываются позже с использованием нового размещения, когда вы фактически добавляете объекты в вектор, что (предположительно) происходит непосредственно перед вызовом init (), поэтому вам действительно не нужна отдельная функция init (). Также помните, что даже если ваш конструктор «пуст» в исходном коде, скомпилированный конструктор может содержать код для инициализации таких вещей, как виртуальные таблицы и RTTI, поэтому страницы все равно будут затронуты во время построения.
Wyzard
1
Ага. В нашем случае мы используем push_back для заполнения вектора. У объектов нет никаких виртуальных функций, так что это не проблема. Когда мы впервые попробовали это с конструктором, мы были поражены количеством ошибок страниц. Я понял, что произошло, и мы дернули конструктор за живое, и проблема с ошибкой страницы исчезла.
EvilTeach
Это меня скорее удивляет. Какие реализации C ++ и STL вы использовали?
Дэвид Торнли
3
Я согласен с остальными, это звучит как плохая реализация std :: vector. Даже если у ваших объектов есть vtables, они не будут созданы до вашего push_back. Вы должны иметь возможность проверить это, объявив конструктор по умолчанию частным, потому что весь вектор, который понадобится, - это конструктор копирования для push_back.
Том
1
@David - Реализация была на AIX.
EvilTeach
1

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

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

Дэвид Торнли
источник
0

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

оборота EvilTeach
источник
6
Раньше это было правдой, теперь это больше. На самом деле верно как раз обратное. Если вы объявляете свои массивы с степенями двойки, вы, скорее всего, столкнетесь с ситуацией, когда вы работаете с двумя указателями в памяти с разницей в два. Проблема в том, что кеш-память ЦП организована именно так, и вы можете столкнуться с тем, что два массива будут сражаться вокруг одной строки кэша. Так вы получите ужасную производительность. Наличие одного из указателей на пару байтов впереди (например, не в степени двойки) предотвращает эту ситуацию.
Нильс Пипенбринк
+1 Нильс, и один конкретный случай этого - "псевдоним 64k" на оборудовании Intel.
Том
Это, кстати, легко опровергнуть, посмотрев на разборку. Много лет назад я был поражен, увидев, как gcc оптимизирует всевозможные постоянные умножения с помощью сдвигов и сложений. Например, val * 7превратился в то, что иначе выглядело бы (val << 3) - val.
dash-tom-bang
0

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

Марк Рэнсом
источник
В самом деле? Вы можете привести обоснование и примеры этого? Я не говорю, что это неправда, просто кажется нелогичным, что местоположение имеет значение.
underscore_d
@underscore_d он не может что-то встроить, пока не будет известно определение функции. Хотя современные компиляторы могут выполнять несколько проходов, чтобы определение было известно во время генерации кода, я этого не предполагаю.
Марк Рэнсом,
Я предполагал, что компиляторы работают с абстрактными графами вызовов, а не с физическим порядком функций, то есть это не имеет значения. Конечно, я полагаю, не повредит быть особенно осторожным - особенно когда, помимо производительности, IMO просто кажется более логичным определять функции, которые вызываются перед теми, которые их вызывают. Мне пришлось бы проверить производительность, но я был бы удивлен, если бы это имело значение, но до тех пор я открыт для удивления!
underscore_d