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


116

Багато років тому компілятори C не були особливо розумними. Як обхід K&R винайшов ключове слово регістр , щоб натякнути компілятору, що, можливо, було б хорошою ідеєю зберегти цю змінну у внутрішньому реєстрі. Вони також змусили третинного оператора допомогти генерувати кращий код.

Ішов час, укладачі дозрівали. Вони стали дуже розумними в тому, що їх аналіз потоку дозволяє їм приймати кращі рішення щодо того, які значення потрібно мати в регістрах, ніж ви могли зробити. Ключове слово регістр став неважливим.

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

Які методи кодування доступні, що може дозволити компілятору / оптимізатору генерувати швидший код?

  • Ідентифікація платформи та компілятора, якими ви користуєтесь, буде вдячна.
  • Чому, здається, працює техніка?
  • Зразок коду рекомендується.

Ось відповідне питання

[Редагувати] Це питання не стосується загального процесу профілювання та оптимізації. Припустимо, що програма була написана правильно, складена з повною оптимізацією, перевірена і введена у виробництво. У вашому коді можуть бути конструкції, які забороняють оптимізатору робити найкращу роботу, яку він може. Що ви можете зробити для рефактора, який видалить ці заборони та дозволить оптимізатору генерувати ще швидший код?

[Редагувати] Зсув пов’язаного посилання


7
Може бути хорошим кандидатом на вікі-спільноту imho, оскільки немає жодної остаточної відповіді на це (цікаве) питання ...
ChristopheD

Я сумую за цим щоразу. Дякую, що вказали на це.
EvilTeach

Під поняттям "краще" ви маєте на увазі просто "швидше" чи у вас є на увазі інші критерії досконалості?
Марка високої продуктивності

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

Це звучить нерозумно, але компілятор може набагато розумніше мати справу з локальною змінною, оскільки він не може перекрити пам'ять жодним із аргументів. Це допоможе вам уникнути жахливого магазину, про який потрапляє вантаж (про що в цій темі згадував Френсіс Бовін).


7
Це має додаткову перевагу, що часто полегшує читання та розуміння програмістам, оскільки вони також не повинні турбуватися про можливі не очевидні побічні ефекти.
Майкл Берр

Більшість IDE за замовчуванням відображають локальні змінні, тому
вводиться

9
ви також можете включити цю оптимізацію, використовуючи обмежені покажчики
Бен Войгт,

4
@Ben - це правда, але я вважаю, що цей спосіб зрозуміліший. Крім того, якщо вхід і вихід перетинаються, я вважаю, що результат не визначений з обмеженими покажчиками (ймовірно, отримувати різну поведінку між налагодженням і випуском), тоді як цей спосіб буде принаймні послідовним. Не зрозумійте мене неправильно, мені подобається використовувати обмеження, але мені подобається не потребувати цього ще більше.
celion

Ви просто сподіваєтесь, що Foo не встановив операцію копіювання, яка копіює пару
мег

76

Ось практика кодування, яка допоможе компілятору створити швидкий код - будь-яка мова, будь-яка платформа, будь-який компілятор, будь-яка проблема:

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

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

Тоді, і лише тоді, можливо, ви захочете почати досліджувати ефекти розповісти компілятору, як використовувати пам'ять. Зробіть 1 зміну за один раз і виміряйте її вплив.

Очікуйте, що ви розчаруєтесь і доведеться дуже багато працювати, щоб покращити ефективність. Сучасні компілятори для зрілих мов, такі як Fortran і C, дуже і дуже хороші. Якщо ви читаєте аккаунт "трюку", щоб покращити ефективність коду, майте на увазі, що автори-компілятори також читали про це, і, якщо це варто зробити, ймовірно, його реалізували. Вони, напевно, написали те, що ви прочитали в першу чергу.


20
Розробники Compiier мають обмежений час, як і всі інші. Не всі оптимізації перетворять їх у компілятор. Як і &порівняно %з двома силами (рідко, якщо взагалі, оптимізованими, але можуть мати значні наслідки для продуктивності). Якщо ви читаєте трюк для продуктивності, єдиний спосіб дізнатися, чи працює він - це внести зміни та виміряти вплив. Ніколи не вважайте, що компілятор оптимізує щось для вас.
Дейв Джарвіс

22
& і% майже завжди оптимізовано, поряд із більшістю інших арифметичних прийомів із дешевою безкоштовністю. Що не оптимізується, це те, що правий операнд є змінною, яка завжди буває потужністю два.
Potatoswatter

8
Для уточнення, я, мабуть, збентежив деяких читачів: порада в практиці кодування, яку я пропоную, - спершу розробити прямий код, який не використовує інструкцій щодо компонування пам'яті для встановлення базової лінії продуктивності. Потім спробуйте речі по черзі і виміряйте їх вплив. Я не пропонував жодних порад щодо виконання операцій.
Марка високої продуктивності

17
Для постійних потужності через два n, GCC Замінює % 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];
  }
}

Власне кажучи, це не проблема оптимізатора, а проблема оптимізації.
EvilTeach

10
Звичайно, це проблема оптимізатора. Люди десятиліттями писали документи про оптимізацію автоматичного обміну петлями.
Філ Міллер

20
@Potatoswatter Про що ти говориш? Компілятор C може робити все, що завгодно, доки спостерігається той самий кінцевий результат, і дійсно GCC 4.4 має, -floop-interchangeякий переверне внутрішню і зовнішню петлю, якщо оптимізатор вважатиме це вигідним.
ефемієнт

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

Виділення факторів змінної за межами петель

Якщо змінна створена під час руху всередині циклу, перемістіть створення / розподіл до циклу. У більшості випадків змінну не потрібно виділяти під час кожної ітерації.

Постійні вирази фактора поза петлями

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

Введення / виведення в блоках

Читання та запис даних великими фрагментами (блоками). Чим більше, тим краще. Наприклад, читання одного octect в той час , менш ефективно , ніж читання тисячі двадцять чотири октетів з одним читанням.
Приклад:

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 вихідний виклик для кожного типу виводу (відформатованого проти неформатованого). Іншим реалізаціям потрібно буде динамічно розподіляти пам'ять для кожного виклику, щоб утримувати всю нову рядок (що погано у вбудованому системному середовищі). Моя пропозиція зменшує кількість результатів.
Томас Меттьюз

3
Одного разу я отримав значне поліпшення продуктивності, накопичивши цикл. Тоді я придумав, як згорнути це за допомогою деякої непрямості, і програма стала помітно швидшою. (Профілювання показало, що ця функція становить 60-80% часу виконання, і я ретельно перевіряв продуктивність до і після.) Я вважаю, що поліпшення відбулося завдяки кращій локальності, але я не зовсім впевнений у цьому.
Девід Торнлі

16
Багато з них - це оптимізація програміста, а не способи, які допоможуть програмісту допомогти компілятору в оптимізації, що було основним питанням оригінального питання. Наприклад, розгортання циклу. Так, ви можете зробити власну розгортання, але я думаю, що цікавіше з’ясувати, які дорожні блоки існують для того, щоб компілятор розкручував вас і видаляв їх.
Адріан Маккарті

26

Оптимізатор насправді не контролює продуктивність вашої програми. Використовуйте відповідні алгоритми та структури та профіль, профіль, профіль.

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

По можливості уникайте прийому адреси змінної. Прохання про вказівник не є "вільним", оскільки це означає, що змінну потрібно зберігати в пам'яті. Навіть масив може зберігатися в регістрах, якщо уникнути покажчиків - це важливо для векторизації.

Що призводить до наступного пункту, прочитайте інструкцію ^ # $ @ ! GCC може векторизувати звичайний код C, якщо ви посипаєте __restrict__тут і __attribute__( __aligned__ )там. Якщо ви хочете чогось дуже конкретного від оптимізатора, можливо, вам доведеться бути конкретним.


14
Це хороша відповідь, але зауважте, що всепрограмна оптимізація стає все більш популярною, і насправді вона може вбудовувати функції в усіх підрозділах перекладу.
Філ Міллер

1
@Novelocrat Так - не треба говорити, що я був дуже здивований, коли вперше побачив щось із того, що A.cпотрапляє в нього B.c.
Джонатан Райнхарт

18

У більшості сучасних процесорів найбільшим вузьким місцем є пам'ять.

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

Кеш-пропуск: Не дуже впевнений, як можна допомогти компілятору, оскільки це здебільшого алгоритмічно, але є внутрішні елементи для попереднього вибору пам’яті.

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


4
+1 для завантажених вантажів-магазинів та різних типів реєстру. Я не впевнений, наскільки велика угода в x86, але вони погіршують PowerPC (наприклад, Xbox360 та Playstation3).
celion

Більшість робіт з методик оптимізації циклу компілятора передбачають ідеальне вкладення, а це означає, що тіло кожної петлі, за винятком внутрішньої, є лише іншим циклом. Ці документи просто не обговорюють кроків, необхідних для узагальнення таких, навіть якщо дуже зрозуміло, що вони можуть бути. Таким чином, я очікував, що багато реалізацій фактично не підтримують ці узагальнення через додаткові зусилля. Таким чином, безліч алгоритмів оптимізації використання кешу в петлях може працювати набагато краще на ідеальних гніздах, ніж на недосконалих гніздах.
Філ Міллер

11

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

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


7
Я визнаю, що це своєрідно - я працюю над великими науковими кодами, що розбивають число, які пов'язані з пропускною здатністю пам'яті. Щодо загальної сукупності програм, я згоден з Нілом.
Марка високої продуктивності

6
Правда; але надзвичайно багато цього коду, пов'язаного з входом / виводом, написано на мовах, які є практично песимізаторами - мовами, які навіть не мають компіляторів. Я підозрюю, що області, де C і C ++ все ще використовуються, як правило, будуть сферами, де важливіше щось оптимізувати (використання процесора, використання пам'яті, розмір коду ...)
Porculus

3
Я провів більшу частину останніх 30 років, працюючи над кодом з дуже маленьким введенням-виведенням. Економте на 2 роки, роблячи бази даних. Графіка, системи управління, моделювання - жодна з них не пов'язана з входом / виводом. Якби введення / виведення було вузьким місцем для більшості людей, ми б не приділяли особливої ​​уваги Intel та AMD.
phkahler

2
Так, я не купую цього аргументу, інакше ми (на своїй роботі) не шукатимемо способів витратити більше часу на обчислення, роблячи також введення / виведення. Крім того, значна частина програмного забезпечення, пов'язаного з входом / виводом, яке я натрапила, було пов'язане вводу / виводу, оскільки введення / виведення було зроблено неохайно; якщо оптимізувати схеми доступу (як і пам'ять), можна отримати величезні результати в роботі.
Даш-Том-Банг

3
Нещодавно я виявив, що майже жоден код, написаний мовою C ++, не пов'язаний введенням / виводом. Звичайно, якщо ви викликаєте функцію ОС для масової передачі диска, ваш потік може перейти в очікування вводу / виводу (але з кешуванням, навіть це сумнівно). Але звичайні функції бібліотеки вводу-виводу, ті, які всі рекомендують, оскільки вони стандартні та портативні, насправді жалюгідно повільні порівняно з сучасними дисковими технологіями (навіть за помірними цінами). Швидше за все, введення-вивід - це вузьке місце лише в тому випадку, якщо ви проскакуєте весь диск, записавши лише кілька байтів. ОТО, інший інтерфейс - справа інша, ми, люди, повільні.
Ben Voigt

11

максимально використовувати правильність const у своєму коді. Це дозволяє компілятору оптимізувати набагато краще.

У цьому документі є безліч інших порад щодо оптимізації: CPP-оптимізація (хоч трохи старий документ)

основні моменти:

  • використовувати списки ініціалізації конструктора
  • використовувати оператори префікса
  • використовувати явні конструктори
  • вбудовані функції
  • уникати тимчасових об’єктів
  • бути в курсі вартості віртуальних функцій
  • повернути об'єкти за допомогою опорних параметрів
  • враховувати розподіл за класом
  • розглянемо розподільники контейнерів stl
  • оптимізація "порожнього члена"
  • тощо

8
Не багато, рідко. Це, однак, покращує фактичну коректність.
Potatoswatter

5
У C і C ++ компілятор не може використовувати const для оптимізації, оскільки відкидання його - чітко визначена поведінка.
dimimcha

+1: const - хороший приклад того, що безпосередньо вплине на складений код. коментар re @ dsimcha - хороший компілятор перевірить, чи не відбудеться це. Звичайно, хороший компілятор "знайде" елементи const, які так чи інакше не оголошені ...
Hogan

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

Звичайно, ви можете перетворити логіку на те, якщо і матимете лише одну точку повернення.

(хитрощі, про які я згадував пізніше)

Оголошення функцій статичними, коли можна, завжди є хорошою ідеєю. Якщо компілятор може довести собі, що він враховував кожного абонента певної функції, то він може порушити умови виклику для цієї функції в ім'я оптимізації. Компілятори часто можуть уникати переміщення параметрів у регістри чи позиції стеків, у яких викликані функції зазвичай очікують, що їх параметри будуть (для цього потрібно відхилятися як у виклику функції, так і в місці розташування всіх абонентів). Компілятор також може часто скористатися тим, що знає, яка пам'ять і реєструє викликану функцію, і уникати генерування коду для збереження змінних значень, що знаходяться в регістрах або місцях пам'яті, які викликана функція не турбує. Це особливо добре спрацьовує, коли дзвінків до функції мало.


2
Насправді не потрібно використовувати віднімання при тестуванні діапазонів, LLVM, GCC і мій компілятор принаймні роблять це автоматично. Мало хто, можливо, зрозуміє, що робить код з відніманням, а ще менше, чому він насправді працює.
Gratian Lup

у наведеному вище прикладі b () не можна викликати, тому що якщо (x <0), тоді буде викликано ().
EvilTeach

@EvilTeach Ні, це не буде. Порівняння, що призводить до виклику a (), є! X
nategoose

@nategoose. якщо x дорівнює -3, то! x вірно.
EvilTeach

@EvilTeach In C 0 - це неправда, а все інше - правда, так -3 - це правда, так! -3 - це неправда
nategoose

9

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

  1. Зробіть більшість функцій статичними. Це дозволяє міжпроцедурному постійному розповсюдженню та аналізу псевдонімів виконувати свою роботу, інакше компілятору потрібно припустити, що функцію можна викликати поза блоком перекладу з абсолютно невідомими значеннями для параметрів. Якщо ви подивитеся на відомі бібліотеки з відкритим кодом, всі вони позначають функції статичних, за винятком тих, які справді потребують зовнішнього використання.

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

  3. НІКОЛИ не використовуйте goto до внутрішньої частини циклу, цикл більше не буде розпізнаний більшістю компіляторів, і жодна з найважливіших оптимізацій не буде застосована.

  4. Використовуйте параметри вказівника лише за потреби та позначте їх, якщо можливо, обмежте. Це дуже допомагає аналізу псевдонімів, оскільки програміст гарантує відсутність псевдоніму (міжпроцедурний псевдонім аналіз зазвичай дуже примітивний). Дуже малі об’єкти структури повинні передаватися за значенням, а не за посиланням.

  5. Використовуйте масиви замість покажчиків, коли це можливо, особливо всередині циклів (a [i]). Масив, як правило, пропонує більше інформації для аналізу псевдоніму, і після деяких оптимізацій той самий код все одно буде сформований (пошук скорочення сили циклу, якщо цікаво). Це також збільшує шанси застосувати рух циклу-інваріантного руху.

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

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

  8. Використовувати перемикач швидше, ніж робити тест, наприклад, якщо (a || b || ... || z). Спочатку перевірте , якщо ваш компілятор робить це автоматично, деякі , і це більш зручним для читання , щоб мати , якщо хоча.


7

Що стосується вбудованих систем та коду, записаних на C / C ++, я намагаюся максимально уникати динамічного розподілу пам'яті . Основна причина, чому я це роблю, - це не обов'язково виконання, але це правило має наслідки для продуктивності.

Алгоритми, що використовуються для управління купами, на деяких платформах, як відомо, повільно (наприклад, vxworks). Ще гірше, що час, який потрібно для повернення з дзвінка на malloc, сильно залежить від поточного стану купи. Тому будь-яка функція, яка викликає malloc, спричинить показник продуктивності, який неможливо легко врахувати. Цей показник ефективності може бути мінімальним, якщо купа все ще чиста, але після цього пристрій працює на деякий час, купа може роздробитися. Виклики триватимуть більше часу, і ви не можете легко розрахувати, як продуктивність знизиться з часом. По-справжньому не можна скласти гіршу оцінку. Оптимізатор не може надати вам жодної допомоги і в цьому випадку. Щоб зробити речі ще гіршими, якщо купа стане занадто сильно роздробленою, дзвінки почнуть припинятися взагалі. Рішення полягає у використанні пулів пам'яті (наприклад,шматочки глібу ) замість купи. Якщо ви зробите це правильно, розподільні дзвінки будуть набагато швидшими та детермінованішими.


Моє правило - якщо вам потрібно динамічно розподілити, отримайте масив, тому вам не потрібно робити це знову. Попередньо розподіліть їх вектори.
EvilTeach

7

Тупий маленький наконечник, але той, який заощадить вам мікроскопічну кількість швидкості та коду.

Завжди передайте аргументи функції в одному порядку.

Якщо у вас є f_1 (x, y, z), який викликає f_2, оголосіть f_2 як f_2 (x, y, z). Не оголошуйте це як f_2 (x, z, y).

Причиною цього є те, що платформа CI C ++ ABI (AKA call convention) обіцяє передавати аргументи в конкретні регістри та локації стеків. Коли аргументи вже знаходяться у правильних регістрах, тоді не потрібно їх переміщувати.

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


2
Ні C, ні C ++ не дають жодних гарантій щодо, або навіть згадування, проходження у конкретних регістрах чи розташуваннях стеків. Саме ABI (наприклад, Linux ELF) визначає деталі проходження параметра.
Еммет

5

Дві технології кодування, яких я не бачив у наведеному вище списку:

Обхід лінкера, написавши код як унікальне джерело

Хоча окрема компіляція дуже приємна для збирання часу, це дуже погано, коли ви говорите про оптимізацію. По суті, компілятор не може оптимізувати його за межі одиниці компіляції, тобто домену, який зарезервований лінкером.

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

Це дуже схоже на написання заголовків лише програм на C ++ (а ще простіше це зробити на C).

Цей прийом досить простий, якщо ви пишете програму, щоб включити її з самого початку, але ви також повинні знати, що вона змінює частину C семантики, і ви можете зіткнутися з деякими проблемами, як статичні змінні або макро зіткнення. Для більшості програм досить просто подолати невеликі проблеми, що виникають. Також пам’ятайте, що компіляція як унікальне джерело проходить повільніше і може зайняти величезну кількість пам’яті (зазвичай це не є проблемою для сучасних систем).

Використовуючи цю просту техніку, мені трапилось зробити кілька програм, які я написав у десять разів швидше!

Як і ключове слово регістр, цей фокус також може скоро застаріти. Оптимізація через лінкер починають підтримуватися компіляторами gcc: Оптимізація часу зв’язку .

Окремі атомні завдання в петлях

Цей більш хитрий. Йдеться про взаємодію між дизайном алгоритму та способом управління оптимізатором кешу та розподілу регістрів. Досить часто програмам доводиться перебирати певну структуру даних і для кожного елемента виконувати деякі дії. Досить часто виконувані дії можна розділити між двома логічно незалежними завданнями. Якщо це так, ви можете написати одну і ту ж програму з двома петлями на одному кордоні, виконуючи точно одне завдання. У деяких випадках написання цього способу може бути швидшим, ніж унікальний цикл (деталі складніші, але поясненням може бути те, що в простому випадку завдання всі змінні можуть зберігатися в регістрах процесорів, а з більш складним - це неможливо, а деякі регістри повинні бути записані в пам'ять і прочитані пізніше, і вартість вище, ніж додаткове регулювання потоку).

Будьте обережні з цим (профільні виступи, використовуючи цей трюк чи ні), як, наприклад, використання реєстрації, воно також може давати менші показники, ніж покращені.


2
Так, до цього часу LTO зробила першу половину цієї посади зайвою і, ймовірно, поганою порадою.
підкреслюйте_d

@underscore_d: Є ще деякі проблеми (в основному пов'язані із видимістю експортованих символів), але з точки зору простої продуктивності більше, мабуть, немає.
kriss

4

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


5
Зміщення функцій, які використовуються разом у тісній фізичній близькості до джерела, збільшує ймовірність того, що вони будуть поруч один з одним в об’єктних файлах та поруч один з одним у вашому виконуваному файлі. Ця покращена локальність інструкцій може допомогти уникнути пропусків кешу інструкцій під час роботи.
paxos1977

У компіляторі AIX є перемикач компілятора для заохочення такої поведінки -qipa [= <suboptions_list>] | -qnoipa Включає або налаштовує клас оптимізацій, відомий як міжпроцедурний аналіз (IPA).
EvilTeach

4
Найкраще - мати спосіб розвитку, який цього не потребує. Використання цього факту як привід для написання немодульного коду в цілому призведе до того, що код повільний і має проблеми з обслуговуванням.
Хоган

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


1
питання стосується C. C не усуває хвостову рекурсію, тому хвіст чи інша рекурсія, стек може підірватися, якщо рекурсія піде занадто глибоко.
Жаба

1
Я уникнув питання з викликом конвенції за допомогою goto. Таких витрат менше.
EvilTeach

2
@hogan: це для мене нове. Чи можете ви вказати на будь-який компілятор, який робить це? І як ви можете бути впевнені, що насправді це оптимізує? Якщо це зробити це, справді потрібно бути впевненим, що він це робить. Ви не сподіваєтесь, що оптимізатор компілятора підходить (наприклад, вбудова, яке може працювати, а може і не працювати)
Жаба

6
@hogan: Я виправлений. Ви праві, що Gcc і MSVC обидва оптимізації хвостової рекурсії.
Жаба

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. Не використовуйте реєстр, якщо ви не плануєте профайлювати і без, і без нього

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

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

Є й інші речі, які важливі для отримання хорошої продуктивності від коду; проектування ваших структур даних для максимальної узгодженості кешу, наприклад. Але питання стосувалося оптимізатора.



3

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

Проблема в цьому випадку полягала в тому, що програмне забезпечення, яке ми використовували, зробило багато невеликих асигнувань. Мовляв, виділіть тут чотири байти, шість байтів і т. Д. Багато маленьких об’єктів теж працює в діапазоні 8-12 байт. Проблема була не стільки в тому, що програмі було потрібно багато дрібниць, а в тому, що вона виділяла багато дрібниць окремо, що роздувало кожне виділення (на цій конкретній платформі) 32 байти.

Частина рішення полягала в тому, щоб зібрати невеликий пул об’єктів у стилі Олександреску, але розширити його, щоб я міг виділити масиви невеликих об'єктів, а також окремі елементи. Це надзвичайно допомогло в продуктивності, оскільки в кеш за один раз помістилося більше елементів.

Інша частина рішення полягала в заміні невпинного використання членів char *, керованих вручну, рядком SSO (оптимізація дрібних рядків). Мінімальний розмір - 32 байти, я створив клас рядків, який мав вбудований 28-символьний буфер за символом *, тож 95% наших рядків не потребували додаткового виділення (і тоді я вручну заміняв майже кожен вигляд char * у цій бібліотеці з цим новим класом, це було весело чи ні). Це допомогло тоні з фрагментацією пам’яті, що потім збільшило локальність орієнтації на інші об’єкти, що вказували на спостереження, і так само було покращено продуктивність.


3

Акуратний прийом, який я навчився від коментаря @MSalters до цієї відповіді, дозволяє компіляторам робити копіювання elision навіть при поверненні різних об'єктів відповідно до певної умови:

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

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

2

Якщо у вас є невеликі функції, які ви дзвоните неодноразово, я в минулому отримував великі вигоди, вводячи їх у заголовки як "статичні вбудовані". Функціональні дзвінки на ix86 напрочуд дорогі.

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


Перетворення рекурсії в стек - це припущенна оптимізація на ompf.org для людей, що розробляють промінь-трекер та записують інші алгоритми візуалізації.
Том

... До цього слід додати, що найбільший обсяг мого персонального проекту tratracer - це рекурсія на основі Vtable через ієрархію обмеженого обсягу з використанням композиційного шаблону. Це дійсно лише купа вкладених коробок, структурованих як дерево, але використання шаблону спричиняє роздуття даних (віртуальні вказівники таблиці) та зменшує когерентність інструкцій (що може бути невеликим / тугим циклом, зараз це ланцюжок викликів функцій)
Том

2

Ось мій другий фрагмент поради щодо оптимізації. Як і в моїй першій пораді, це загальна мета, а не мова чи процесор.

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

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

Так, автори-компілятори не з раси кодуючих гігантів, а компілятори містять помилки, і те, що, відповідно до посібника та відповідно до теорії компілятора, повинно робити речі швидшими, іноді робить речі повільнішими. Тому вам доведеться робити крок за часом і вимірювати продуктивність до і після налаштування.

І так, зрештою, ви можете зіткнутися з комбінаторним вибухом прапорів компілятора, тому вам потрібно мати сценарій або два для запуску make з різними прапорами компілятора, встановити черги на завдання у великому кластері та зібрати статистику часу виконання. Якщо це лише ви та Visual Studio на ПК, у вас виникне інтерес задовго до того, як ви спробуєте достатньо комбінацій достатньої кількості прапорів компілятора.

З повагою

Позначити

Коли я вперше підбираю фрагмент коду, зазвичай я можу отримати коефіцієнт продуктивності в 1,4 - 2,0 рази (тобто нова версія коду працює в 1 / 1,4 або 1/2 часу старої версії) протягом день чи два, обмацуючи прапорами компілятора. Зрозуміло, що це може бути коментарем щодо відсутності підбору компілятора серед науковців, які є значною мірою коду, над яким я працюю, а не симптомом моєї майстерності. Встановивши прапорці компілятора до максимуму (а рідко це просто -О3), може знадобитися кілька місяців наполегливої ​​роботи, щоб отримати ще один коефіцієнт 1,05 або 1,1


2

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


x86-64 біт також дозволяє безліч параметрів переданих регістрів, що може драматично впливати на накладні виклики функцій.
Том

1

Для продуктивності спочатку зосередьтесь на написанні коду, що підтримується - компонентний, нещільно пов'язаний тощо, тож, коли вам доведеться ізолювати частину або переписати, оптимізувати чи просто профіль, ви можете це зробити без особливих зусиль.

Оптимізатор допоможе покращити продуктивність вашої програми.


3
Це працює лише в тому випадку, якщо самі "інтерфейси" з'єднання піддаються оптимізації. Інтерфейс може бути по своїй суті "повільним", наприклад, примушуючи зайві пошуки чи обчислення, або примушуючи поганий доступ до кешу.
Том

1

Тут ви отримуєте хороші відповіді, але вони припускають, що ваша програма досить близька до оптимальної для початку, і ви кажете

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

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

Якщо я можу навести приклад, ця відповідь показує, як ідеально розумна на вигляд програма була зроблена в 40 разів швидше за допомогою макрооптимізації . Великі прискорення не можна робити в кожному програмі, як було написано вперше, але в багатьох (за винятком дуже малих програм), на мій досвід.

Після цього мікрооптимізація («гарячих точок») може принести вам хорошу користь.


1

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

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

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

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


Ви наближаєтесь до відповіді на запитання ..... які речі ви робите з кодом, щоб компілятор міг робити такі види оптимізацій?
EvilTeach

1
Намагаючись писати більше в C-стилі (порівняно з C ++), наприклад, уникаючи віртуальних функцій без абсолютної потреби, особливо якщо вони будуть називатися часто, уникайте AddRefs .. і всіх крутих матеріалів (знову ж таки, якщо це дійсно не потрібно). Написати код легко для вбудовування - менше параметрів, менше "if" -s. Не використовуйте глобальні змінні, якщо це абсолютно не потрібно. У структурі даних - покладіть спочатку більш широкі поля (подвійний, int64 переходить до int) - так компілятор вирівняє структуру за натуральним розміром першого поля - вирівнювання добре для перф.
jf.

1
Макет даних та їх доступ є абсолютно важливими для продуктивності. Тож після профілювання - я іноді розбиваю структуру на декілька, слідуючи за локальністю доступу. Ще один загальний трюк - використовувати int або size-t порівняно з char - навіть значення даних невеликі - уникайте різних perf. штрафи зберігаються для завантаження блокування, випуски з частковими реєстрами кіосків. Звичайно, це не застосовується, коли потрібні великі масиви таких даних.
jf.

Ще одне - уникайте системних дзвінків, якщо немає реальної потреби :) - вони ДУЖЕ дорогі
jf.

2
@jf: Я поставив +1 для вашої відповіді, але будь ласка, чи можете ви перенести відповідь з коментарів до органу відповідей? Це буде простіше читати.
kriss

1

Однією з оптимізацій, які я використав у C ++, є створення конструктора, який нічого не робить. Треба вручну викликати init (), щоб перевести об'єкт у робочий стан.

Це має користь у тому випадку, коли мені потрібен великий вектор цих класів.

Я закликаю резерв () виділити простір для вектора, але конструктор фактично не торкається сторінки пам'яті, на якій знаходиться об'єкт. Тому я витратив деякий адресний простір, але фактично не спожив багато фізичної пам’яті. Я уникаю помилок сторінки, пов’язаних із пов'язаними витратами на будівництво.

Коли я генерую об'єкти для заповнення вектора, я встановлюю їх за допомогою init (). Це обмежує мою загальну кількість помилок сторінки і уникає необхідності зміни розміру () вектора під час його заповнення.


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, як кількість елементів, дає можливість оптимізатору зменшити силу , замінивши множення на зсув на кількість бітів, коли дивишся вгору окремі елементи.


6
Це раніше було правдою, а сьогодні вже є. Зачіпає саме навпаки. Якщо ви оголосите свої масиви з двома повноваженнями, ви, швидше за все, зіткнетеся з ситуацією, що ви працюєте за двома вказівниками потужністю два в пам'яті. Проблема полягає в тому, що кеші процесора організовані саме так, і ви можете виявити, що два масиви воюють навколо однієї кеш-лінії. Таким чином ви отримуєте жахливі показники. Наявність одного з покажчиків на пару байтів попереду (наприклад, не два потужності) запобігає цій ситуації.
Нільс Піпенбрінк

+1 Nils, і одним із конкретних явищ цього є "64k псевдонім" на апаратному забезпеченні Intel.
Том

Це те, що легко спростувати, дивлячись на розбирання, до речі. Я був вражений роками тому, коли бачив, як gcc оптимізує всілякі постійні множення зі змінами та додаваннями. Наприклад, val * 7перетворилося на те, що інакше виглядатиме (val << 3) - val.
dash-tom-bang

0

Покладіть невеликі та / або часто викликані функції вгорі вихідного файлу. Це полегшує компілятору пошук можливостей для вбудовування.


Дійсно? Чи можете ви навести обґрунтування та приклади цього? Не кажучи, що це неправда, просто звучить неінтуїтивно, що розташування мало б значення.
підкреслюй_d

@underscore_d він не може щось вбудувати, поки не стане відомо визначення функції. Хоча сучасні компілятори можуть робити кілька проходів, щоб визначення було відомо під час генерації коду, я не припускаю цього.
Марк Рансом

Я вважав, що компілятори працюють над абстрактними графіками викликів, а не фізичним порядком функцій, тобто це не має значення. Звичайно, я вважаю, що це не завадить бути особливо обережними - особливо, коли, відмовляючись від виконання, IMO, просто логічніше визначати функції, які викликаються перед тими, хто їх викликає. Я повинен був би перевірити продуктивність, але був би здивований, якби він мав значення, але до цього часу я відкритий для здивування!
підкреслюй_d
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.