Как уже говорили другие, если для C ++ 14 ничего не значит , давайте рассмотрим __restrict__
расширение GCC, которое делает то же самое, что и C99 restrict
.
C99
restrict
говорит, что два указателя не могут указывать на перекрывающиеся области памяти. Наиболее распространенное использование для аргументов функции.
Это ограничивает способ вызова функции, но позволяет оптимизировать компиляцию.
Если вызывающая сторона не выполняет restrict
договор, неопределенное поведение.
Проект C99 N1256 6.7.3 / 7 « Классификаторы типов» гласит:
Предполагаемое использование квалификатора restrict (например, класса хранения регистров) состоит в том, чтобы способствовать оптимизации, и удаление всех экземпляров классификатора из всех блоков предварительной обработки, составляющих соответствующую программу, не меняет его значения (т. Е. Наблюдаемое поведение).
и 6.7.3.1 «Формальное определение ограничения» дает кровные детали.
Возможная оптимизация
Пример Википедии является очень осветительным.
Наглядно видно, как это позволяет сохранить одну инструкцию по сборке .
Без ограничений:
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 действительно делает это?
g++
4.8 Linux x86-64:
g++ -g -std=gnu++98 -O0 -c main.cpp
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 и michael .
Рассмотрим для примера:
void f(char *restrict p1, char *restrict p2, size_t size) {
for (size_t i = 0; i < size; i++) {
p1[i] = 4;
p2[i] = 9;
}
}
Из-за restrict
этого умный компилятор (или человек) может оптимизировать это так:
memset(p1, 4, size);
memset(p2, 9, size);
Что потенциально гораздо более эффективно, так как может быть оптимизировано для сборки при достойной реализации libc (например, glibc). Лучше ли использовать std :: memcpy () или std :: copy () с точки зрения производительности? , возможно с SIMD инструкциями .
Без ограничения эта оптимизация не может быть выполнена, например, рассмотрим:
char p1[4];
char *p2 = &p1[1];
f(p1, p2, 3);
Тогда for
версия делает:
p1 == {4, 4, 4, 9}
пока memset
версия делает:
p1 == {4, 9, 9, 9}
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 итераций, которое я не собираюсь воспроизводить здесь :-)
У меня не хватило терпения их тестировать, но я считаю, что ограниченная версия будет быстрее.
Строгое правило алиасинга
restrict
Ключевое слово влияет только указатели совместимых типов (например , два int*
) , поскольку строгие правила наложения спектров говорят , что сглаживание несовместимых типов не определенно поведение по умолчанию, и поэтому компиляторы могут предположить , что это не произойдет и оптимизирует прочь.
Смотрите: что такое строгое правило наложения имен?
Это работает для ссылок?
Согласно документам GCC он делает: https://gcc.gnu.org/onlinedocs/gcc-5.1.0/gcc/Restricted-Pointers.html с синтаксисом:
int &__restrict__ rref
Существует даже версия для this
функций-членов:
void T::fn () __restrict__
restrict
это ключевое слово c99. Да, Рпберт С. Барнс, я знаю, что большинство компиляторов поддерживают__restrict__
. Вы заметите, что все с двойным подчеркиванием, по определению, зависит от конкретной реализации и, следовательно, НЕ от C ++ , а от конкретной версии компилятора.