Чи можу я натякнути на оптимізатор, давши діапазон цілого числа?


173

Я використовую intтип для зберігання значення. За семантикою програми значення завжди змінюється в дуже малому діапазоні (0 - 36), а int(не а char) використовується лише через ефективність процесора.

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

Отже, чи можна сказати компілятору, що це intзавжди в тому невеликому діапазоні, і чи можна компілятору робити ці оптимізації?


4
Оптимізація діапазону значень існує у багатьох компіляторах, наприклад. llvm, але я не знаю жодного натяку на мову, щоб заявити про це.
Рем Русану

2
Зауважте, що якщо ви ніколи не маєте негативних чисел, у вас можуть бути невеликі вигоди від використання unsignedтипів, оскільки їх компілятору легше міркувати.
user694733

4
@RemusRusanu: Pascal дозволяє визначати типи піддіапазону , наприклад var value: 0..36;.
Едгар Бонет

7
" int (не char) використовується лише тому, що ефективність процесора ". Цей старий фрагмент звичайної мудрості, як правило, не дуже правдивий. Вузькі типи іноді повинні бути розширені нулем або знаком до повної ширини регістра, особливо коли використовується як індекси масиву, але іноді це відбувається безкоштовно. Якщо у вас є масив такого типу, зменшення сліду кешу зазвичай перевищує будь-що інше.
Пітер Кордес

1
Забув сказати: intі unsigned intдля більшості систем з 64-бітовими вказівниками теж потрібно розширити знаки або нулі з 32 до 64 біт. Зауважте, що на x86-64 операції з 32-бітовими регістрами беруть нульове розширення до 64-бітового безкоштовно (не підписуйте розширення, але підписане переповнення - невизначене поведінка, тому компілятор може просто використовувати 64-бітну математику, підписану, якщо він хоче). Таким чином, ви бачите лише додаткові інструкції щодо нульового розширення 32-бітових аргументів функції, а не результати обчислення. Ви б для більш вузьких непідписаних типів.
Пітер Кордес

Відповіді:


230

Так, можливо. Наприклад, gccви можете використовувати, __builtin_unreachableщоб повідомити компілятору про неможливі умови, наприклад:

if (value < 0 || value > 36) __builtin_unreachable();

Ми можемо перетворити умову вище в макрос:

#define assume(cond) do { if (!(cond)) __builtin_unreachable(); } while (0)

І використовуйте його так:

assume(x >= 0 && x <= 10);

Як бачите , gccоптимізація проводиться на основі цієї інформації:

#define assume(cond) do { if (!(cond)) __builtin_unreachable(); } while (0)

int func(int x){
    assume(x >=0 && x <= 10);

    if (x > 11){
        return 2;
    }
    else{
        return 17;
    }
}

Виробляє:

func(int):
    mov     eax, 17
    ret

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

Він не повідомляє вас, коли це відбувається, навіть у налагодженнях. Щоб простіше налагоджувати / тестувати / виловлювати помилки з припущеннями, ви можете використовувати гібридний макрос припускати / затверджувати (кредити до @David Z), як цей:

#if defined(NDEBUG)
#define assume(cond) do { if (!(cond)) __builtin_unreachable(); } while (0)
#else
#include <cassert>
#define assume(cond) assert(cond)
#endif

У налагодженнях (з NDEBUG не визначеним) він працює як звичайне assertповідомлення про помилку друку та abortпрограма 'ing, а в версії версій використовує припущення, створюючи оптимізований код.

Однак зауважте, що він не є заміною для регулярних assert- condзалишається у версії версій, тому не слід робити щось подібне assume(VeryExpensiveComputation()).


5
@Xofo, не отримав цього, на моєму прикладі це вже відбувається, оскільки return 2гілка компілятора вилучила з коду.

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

19
@ user3528438, __builtin_expect- це не суворий натяк. __builtin_expect(e, c)слід читати як " eнайімовірніше оцінити до c", і може бути корисним для оптимізації прогнозування галузей, але це не обмежує eзавжди бути c, тому не дозволяє оптимізатору відкидати інші випадки. Подивіться, як організовуються відділення в зборах .

6
Теоретично замість цього може бути використаний будь-який код, який безумовно спричинює невизначену поведінку __builtin_unreachable().
CodesInChaos

14
Якщо є якась химерність, про яку я не знаю, що робить це поганою ідеєю, це може мати сенс поєднувати це assert, наприклад, визначити, assumeяк assertколи NDEBUGце не визначено, і як, __builtin_unreachable()коли NDEBUGвизначено. Таким чином ви отримуєте перевагу припущення у виробничому коді, але у складі налагодження у вас все одно є чітка перевірка. Звичайно, тоді вам доведеться зробити достатньо тестів, щоб переконатися в тому, що припущення будуть задоволені в дикій природі.
David Z

61

Для цього є стандартна підтримка. Що потрібно зробити, це включити stdint.h( cstdint), а потім використовувати тип uint_fast8_t.

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


2
Ці типи не використовуються майже стільки, скільки повинні бути (я особисто схильний забувати, що вони існують). Вони дають код, який є і швидким, і портативним, досить блискучим. І вони існують з 1999 року.
Лундін

Це гарна пропозиція для загальної справи. Відповідь Деніса показує більш податливе рішення для конкретних сценаріїв.
Гонки легкості на орбіті

1
Компілятор отримує лише інформацію про діапазон 0-255 в системах, де uint_fast8_tнасправді є 8-розрядний тип (наприклад unsigned char), як на x86 / ARM / MIPS / PPC ( godbolt.org/g/KNyc31 ). На ранньому DEC Alpha до 21164A навантаження / сховища байтів не підтримувалися, тому будь-яка розумна реалізація використовувала б typedef uint32_t uint_fast8_t. AFAIK, немає механізму, щоб тип мав додаткові обмеження діапазону у більшості компіляторів (як, наприклад, gcc), тому я впевнений, що він uint_fast8_tбуде вести себе так само, як unsigned intі будь-що в цьому випадку.
Пітер Кордес

( boolспеціальний і обмежений діапазоном до 0 або 1, але це вбудований тип, не визначений файлами заголовка в термінах char, на gcc / clang. Як я вже сказав, я не думаю, що більшість компіляторів мають механізм це зробило б це можливим.)
Пітер Кордес

1
У будь-якому випадку, uint_fast8_tце хороша рекомендація, оскільки він буде використовувати 8-бітний тип на платформах, де це так само ефективно unsigned int. (Я насправді не впевнений, для яких fastтипів передбачається швидке використання , і чи передбачається компроміс кеш-пам’яті.) x86 має широку підтримку байтових операцій, навіть для байтового додавання з джерелом пам'яті, тому вам навіть не доведеться робити окреме навантаження, що розширює нуль (що також дуже дешево). gcc створює uint_fast16_t64-розрядний тип на x86, що є божевільним для більшості застосувань (проти 32-бітного). godbolt.org/g/Rmq5bv .
Пітер Кордес

8

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

У цьому випадку я знайшов, що ця методика може працювати:

if (x == c)  // assume c is a constant
{
    foo(x);
}
else
{
    foo(x);
}

Ідея полягає у компромісі кодових даних: ви переміщуєте 1 біт даних (чи x == c) у логіку управління .
Це натякає на оптимізатор, який xнасправді є відомою константою c, спонукаючи його вбудувати та оптимізувати перший виклик fooокремо від решти, можливо, досить сильно.

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

Приклад:

Щоб ця методика працювала, вам слід пощастити - бувають випадки, коли компілятор вирішує не оцінювати речі статично, і вони начебто довільні. Але коли це працює, він працює добре:

#include <math.h>
#include <stdio.h>

unsigned foo(unsigned x)
{
    return x * (x + 1);
}

unsigned bar(unsigned x) { return foo(x + 1) + foo(2 * x); }

int main()
{
    unsigned x;
    scanf("%u", &x);
    unsigned r;
    if (x == 1)
    {
        r = bar(bar(x));
    }
    else if (x == 0)
    {
        r = bar(bar(x));
    }
    else
    {
        r = bar(x + 1);
    }
    printf("%#x\n", r);
}

Просто використовуйте -O3та помічайте попередньо оцінені константи 0x20та 0x30eу висновку асемблера .


Ви не хочете if (x==c) foo(c) else foo(x)? Якби тільки зловити constexprреалізацію foo?
MSalters

@MSalters: Я знав, що хтось це запитає !! Раніше я придумав цю техніку constexprі ніколи не намагався її "оновлювати" після цього (хоча я навіть не constexprнамагався хвилюватися навіть після цього), але причина, що я цього не робив, полягала в тому, що я хотів полегшить компілятору виділити їх як загальний код та видалити гілку, якщо він вирішив залишити їх як звичайні виклики методу, а не оптимізувати. Я очікував, що якщо я вкладу, cкомпіляторові справді важко сказати (вибачте, поганий жарт), що два - той самий код, хоча я цього ніколи не перевіряв.
користувач541686

4

Я просто хочу сказати, що якщо ви хочете отримати рішення, яке є більш стандартним C ++, ви можете використовувати [[noreturn]]атрибут, щоб написати свій unreachable.

Тому я повторно призначу прекрасний приклад, щоб продемонструвати:

namespace detail {
    [[noreturn]] void unreachable(){}
}

#define assume(cond) do { if (!(cond)) detail::unreachable(); } while (0)

int func(int x){
    assume(x >=0 && x <= 10);

    if (x > 11){
        return 2;
    }
    else{
        return 17;
    }
}

Що, як бачите , призводить до майже однакового коду:

detail::unreachable():
        rep ret
func(int):
        movl    $17, %eax
        ret

Мінус - це, звичайно, те, що ви отримуєте попередження про те, що [[noreturn]]функція дійсно повертається.


Це спрацьовує clang, коли моє оригінальне рішення не так, приємний трюк і +1. Але вся справа дуже залежить від компілятора (як показав нам Пітер Кордес, iccце може погіршити продуктивність), тому воно все ще не є загальноприйнятим. Також незначне зауваження: unreachableвизначення оптимізатора має бути доступним для оптимізатора та накреслено для цього .
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.