Что означает ключевое слово restrict в C++?
Я всегда был не уверен, что означает ключевое слово restrict в C++?
Означает ли это, что два или более указателя на функцию не перекрываются? Что еще это значит?
7 ответов
Кристер Эриксон в своей статье " Оптимизация памяти" говорит, что пока restrict
еще не является частью стандарта C++, так как он поддерживается многими компиляторами, и он рекомендует использовать его, когда это возможно:
ограничить ключевое слово
! Новое в стандарте ANSI/ISO C 1999 года
! Пока не в стандарте C++, но поддерживается многими компиляторами C++
! Только подсказка, поэтому может ничего не делать и при этом соответствовать
Ограниченный указатель (или ссылка)...
!... в основном это обещание компилятору, что для области видимости указателя цель указателя будет доступна только через этот указатель (и указатели скопированы из него).
В компиляторах C++, которые его поддерживают, он, вероятно, должен вести себя так же, как и в C.
Подробности смотрите в этой публикации: Реалистичное использование ключевого слова C99 'restrict'?
Потратьте полчаса, чтобы просмотреть статью Эриксона, это интересно и стоит времени.
редактировать
Я также обнаружил, что компилятор IBM AIX C/C++ поддерживает __restrict__
ключевое слово
g ++ также, кажется, поддерживает это, поскольку следующая программа компилируется чисто на g++:
#include <stdio.h>
int foo(int * __restrict__ a, int * __restrict__ b) {
return *a + *b;
}
int main(void) {
int a = 1, b = 1, c;
c = foo(&a, &b);
printf("c == %d\n", c);
return 0;
}
Я также нашел хорошую статью об использовании restrict
:
Демистификация ограниченного ключевого слова
Edit2
Я наткнулся на статью, в которой конкретно обсуждается использование restrict в программах на C++:
Load-hit-store и ключевое слово __restrict
Кроме того, Microsoft Visual C++ также поддерживает __restrict
ключевое слово
Как говорили другие, если для 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 a may be equal to x.
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
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__
Там нет такого ключевого слова в C++. Список ключевых слов C++ можно найти в разделе 2.11/1 стандарта языка C++. restrict
является ключевым словом в C99 версии языка C, а не в C++.
Поскольку заголовочные файлы из некоторых библиотек C используют ключевое слово, язык C++ должен будет что-то с этим сделать... как минимум, игнорируя ключевое слово, поэтому нам не нужно # определять ключевое слово в пустой макрос для подавления ключевого слова,
С ,
compiler
может делать сложные оптимизации, так как программист гарантировал украшенный
pointers
указывают на диапазоны данных, которые определенно не будут перекрывать друг друга.
Обычно это так, поэтому для достижения высокой производительности в большинстве случаев можно поставить
__restrict__
декоратор
pointers
в вашем коде.