Реальне використання ключового слова "обмежити" C99?


183

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

Чи може хтось запропонувати реалістичні випадки, коли варто реально використовувати це?


4
memcpyvs memmove- один канонічний приклад.
Олександр К.

@AlexandreC: Я не думаю, що це особливо застосовне, оскільки відсутність "обмежуючого" класифікатора не означає, що логіка програми буде працювати з перевантаженням джерела та призначення, а також наявність такого класифікатора не перешкоджатиме викликаному методу від визначення того, чи перекриваються джерело та місце призначення, і, якщо так, заміни dest на src + (dest-src), яке, оскільки воно походить від src, було б дозволено псевдонімом.
supercat

@supercat: Тому я ставлю це як коментар. Однак 1) restrictаргументація, що кваліфікується, memcpyдозволяє в принципі наївно реалізувати агресивну оптимізацію, і 2) просто виклик memcpyдозволяє компілятору припустити, що наведені йому аргументи не мають псевдоніму, що могло б дозволити деяку оптимізацію навколо memcpyвиклику.
Олександр К.

@AlexandreC: Компілятору на більшості платформ було б дуже важко оптимізувати наївну пам’ять - навіть із "обмеженням" - бути в будь-якому місці, настільки ж ефективним, як і версія, пристосована до цілі. Оптимізація на стороні виклику не потребуватиме ключового слова "обмежити", а в деяких випадках зусилля щодо їх спрощення можуть бути контрпродуктивними. Наприклад, багато реалізацій memcpy можуть, за нульових додаткових витрат, розцінюватися memcpy(anything, anything, 0);як неоперативні, і гарантують, що якщо pце вказівник на принаймні nзаписувані байти memcpy(p,p,n); не матиме несприятливих побічних ефектів. Такі випадки можуть виникнути ...
supercat

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

Відповіді:


182

restrictговорить, що вказівник - це єдине, що має доступ до основного об'єкта. Це виключає потенціал для псевдонімування вказівника, що забезпечує кращу оптимізацію компілятором.

Наприклад, припустимо, що у мене є машина зі спеціалізованими інструкціями, яка може множити вектори чисел у пам'яті, і у мене є такий код:

void MultiplyArrays(int* dest, int* src1, int* src2, int n)
{
    for(int i = 0; i < n; i++)
    {
        dest[i] = src1[i]*src2[i];
    }
}

Потреби компілятор правильно звертатися , якщо dest, src1і src2перекриття, а це означає , що необхідно виконати одну множення в той час, від початку до кінця. Маючи restrictкомпілятор, вільний оптимізувати цей код за допомогою векторних інструкцій.

У Вікіпедії є запис про restrict, з іншого , наприклад, тут .


3
@Michael - Якщо я не помиляюся, проблема буде лише тоді, коли destперекривається будь-який з векторів-джерел. Чому виникала б проблема, якщо src1і src2перетинатися?
ysap

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

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

2
Насправді ключове слово зареєструвати лише дорадче. І в компіляторах приблизно з 2000 року i (і n для порівняння) в прикладі буде оптимізовано в регістр, незалежно від того, використовуєте ви чи не ключове слово регістр.
Марк Фішлер

154

Приклад Вікіпедії є дуже освітлювальним.

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

Без обмежень:

void f(int *a, int *b, int *x) {
  *a += *x;
  *b += *x;
}

Псевдозбірка:

load R1  *x    ; Load the value of x pointer
load R2  *a    ; Load the value of a pointer
add R2 += R1    ; Perform Addition
set R2  *a     ; Update the value of a pointer
; Similarly for b, note that x is loaded twice,
; because x may point to a (a aliased by x) thus 
; the value of x will change when the value of a
; changes.
load R1  *x
load R2  *b
add R2 += R1
set R2  *b

З обмеженням:

void fr(int *restrict a, int *restrict b, int *restrict x);

Псевдозбірка:

load R1  *x
load R2  *a
add R2 += R1
set R2  *a
; Note that x is not reloaded,
; because the compiler knows it is unchanged
; "load R1 ← *x" is no longer needed.
load R2  *b
add R2 += R1
set R2  *b

Чи справді це робить GCC?

GCC 4.8 Linux x86-64:

gcc -g -std=c99 -O0 -c main.c
objdump -S main.o

З -O0, вони однакові.

З -O3:

void f(int *a, int *b, int *x) {
    *a += *x;
   0:   8b 02                   mov    (%rdx),%eax
   2:   01 07                   add    %eax,(%rdi)
    *b += *x;
   4:   8b 02                   mov    (%rdx),%eax
   6:   01 06                   add    %eax,(%rsi)  

void fr(int *restrict a, int *restrict b, int *restrict x) {
    *a += *x;
  10:   8b 02                   mov    (%rdx),%eax
  12:   01 07                   add    %eax,(%rdi)
    *b += *x;
  14:   01 06                   add    %eax,(%rsi) 

Для неусвідомлених закликає конвенція :

  • rdi = перший параметр
  • rsi = другий параметр
  • rdx = третій параметр

Вихід з GCC був ще чіткішим, ніж стаття Вікі: 4 інструкції проти 3 інструкцій.

Масиви

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

Розглянемо для прикладу:

void f(char *restrict p1, char *restrict p2) {
    for (int i = 0; i < 50; i++) {
        p1[i] = 4;
        p2[i] = 9;
    }
}

Через те restrict, що смарт-компілятор (або людина) може оптимізувати це для:

memset(p1, 4, 50);
memset(p2, 9, 50);

що потенційно набагато ефективніше, оскільки це може бути оптимізована збірка на гідній реалізації libc (наприклад, glibc): чи краще використовувати std :: memcpy () або std :: copy () з точки зору продуктивності?

Чи справді це робить GCC?

GCC 5.2.1.Linux x86-64 Ubuntu 15.10:

gcc -g -std=c99 -O0 -c main.c
objdump -dr main.o

З -O0, обидва однакові.

З -O3:

  • з обмеженням:

    3f0:   48 85 d2                test   %rdx,%rdx
    3f3:   74 33                   je     428 <fr+0x38>
    3f5:   55                      push   %rbp
    3f6:   53                      push   %rbx
    3f7:   48 89 f5                mov    %rsi,%rbp
    3fa:   be 04 00 00 00          mov    $0x4,%esi
    3ff:   48 89 d3                mov    %rdx,%rbx
    402:   48 83 ec 08             sub    $0x8,%rsp
    406:   e8 00 00 00 00          callq  40b <fr+0x1b>
                            407: R_X86_64_PC32      memset-0x4
    40b:   48 83 c4 08             add    $0x8,%rsp
    40f:   48 89 da                mov    %rbx,%rdx
    412:   48 89 ef                mov    %rbp,%rdi
    415:   5b                      pop    %rbx
    416:   5d                      pop    %rbp
    417:   be 09 00 00 00          mov    $0x9,%esi
    41c:   e9 00 00 00 00          jmpq   421 <fr+0x31>
                            41d: R_X86_64_PC32      memset-0x4
    421:   0f 1f 80 00 00 00 00    nopl   0x0(%rax)
    428:   f3 c3                   repz retq

    Два memsetдзвінки, як очікувалося.

  • без обмежень: жодних дзвінків stdlib, просто розгортання циклу з 16 ітерацій, який я не збираюся відтворювати тут :-)

У мене не було терпіння їх орієнтувати, але я вірю, що обмежена версія буде швидшою.

C99

Давайте розглянемо стандарт заради повноти.

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

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

Якщо абонент не дотримується restrictдоговору, не визначена поведінка.

У проекті 6.7.3 / 7 "Кваліфікатори типу" C99 N1256 написано:

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

та 6.7.3.1 "Формальне визначення обмеження" містить детальні відомості.

Суворе правило дозволу

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

Дивіться: Що таке суворе правило псевдоніму?

Дивитися також


9
Класифікатор "обмеження" насправді дозволяє значно збільшити економію. Наприклад, дані void zap(char *restrict p1, char *restrict p2) { for (int i=0; i<50; i++) { p1[i] = 4; p2[i] = 9; } }, обмежуючі класифікатори дозволяють компілятору переписати код як "memset (p1,4,50); memset (p2,9,50);". Обмеження значно перевершує псевдонім на основі типу; соромно, що компілятори зосереджуються більше на останньому.
supercat

@supercat чудовий приклад, доданий для відповіді.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

2
@ tim18: Ключове слово "обмежити" може забезпечити безліч оптимізацій, які навіть агресивна оптимізація на основі типу не може. Далі, існування "обмеження" в мові - на відміну від агресивного типового псевдоніму - ніколи не унеможливлює виконання завдань максимально ефективно, як було б можливо за їх відсутності (оскільки код, який би порушився на "обмежити", може просто не використовувати його, тоді як код, порушений агресивним TBAA, повинен часто переписуватися менш ефективно.
supercat

2
@ tim18: об'ємні речі, які містять подвійне підкреслення у задній частині, як у __restrict. Інакше подвійне підкреслення може неправильно трактуватися як ознака того, що ви кричите.
supercat

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