Я просматривал некоторые документы и вопросы/ответы и видел, как это упоминалось. Я прочитал краткое описание, заявив, что это будет в основном обещание программиста, что указатель не будет использоваться, чтобы указать куда-то еще.
Может ли кто-нибудь предложить некоторые реалистичные случаи, когда его ценность действительно используется?
Ответ 1
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
, компилятор может оптимизировать этот код с помощью векторных инструкций.
EDIT: Wikipedia имеет запись на restrict
, с другим примером здесь.
Ответ 2
Пример Википедии очень освещает.
Он четко показывает, как он позволяет сохранить одну инструкцию сборки.
Без ограничений:
void f(int *a, int *b, int *x) {
*a += *x;
*b += *x;
}
Pseudo assembly:
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);
Pseudo assembly:
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 это делает?
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 был еще более ясным, чем статья wiki: 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
, поведение undefined.
C99 N1256 черновик 6.7.3/7 "Типификаторы типов" говорит:
Предполагаемое использование ограничителя (например, класс хранения регистров) заключается в продвижении оптимизации, а удаление всех экземпляров квалификатора из всех блоков трансляции обработки, составляющих соответствующую программу, не изменяет его значения (т.е. наблюдаемого поведения).
и 6.7.3.1 "Формальное определение ограничения" дает детали gory.
Строгое правило сглаживания
Ключевое слово restrict
влияет только на указатели совместимых типов (например, два int*
), потому что правила строгих правил псевдонимов говорят о том, что по умолчанию несовместимые типы слияния являются undefined, и поэтому компиляторы могут предположить, что этого не происходит и оптимизируется прочь.
Смотрите: Что такое строгое правило псевдонимов?
См. также