Wikipedia example является очень освещающий.
Он ясно показывает, как позволяет сохранить одну инструкцию по сборке.
Без ограничения:
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 это делает?
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)
Для непосвященных calling convention является:
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): Is it better to use std::memcpy() or std::copy() in terms to performance?
Действительно ли 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 итераций широкий loop unrolling, которые я не собираюсь приводить здесь :-)
Я не имел терпения сравнить их, но я не верю что ограниченная версия будет быстрее.
C99
Давайте посмотрим на стандарт для полноты.
restrict
говорит, что два указателя не могут указывать на перекрывающиеся области памяти.Наиболее распространенное использование для аргументов функции.
Это ограничивает способ вызова функции, но позволяет оптимизировать время компиляции.
Если вызывающий абонент не соблюдает договор restrict
, неопределенное поведение.
В C99 N1256 draft 6.7.3/7 «Тип классификаторов» говорит:
Намеченное использование ограничения классификатора (например, класс хранения регистров) является содействие оптимизации и удаление всех экземпляров классификатором из все единицы перевода для предварительной обработки, составляющие соответствующую программу, не меняют своего значения (т. е. наблюдаемого поведения).
и 6.7.3.1 «Формальное определение ограничения» дает детали gory.
Строгие правила наложения спектров
restrict
ключевое слово влияет только указатели совместимых типов (например, два int*
), потому что строгие правила наложения спектров говорит, что сглаживание несовместимых типов не определено поведение по умолчанию, и поэтому компиляторы могут предположить, что это делает не происходит и не оптимизируется.
См: What is the strict aliasing rule?
Смотрите также
'memcpy' vs' memmove' - это один канонический пример. –
@AlexandreC .: Я не думаю, что это особенно применимо, поскольку отсутствие «ограничивающего» квалификатора не означает, что программная логика будет работать с перегрузкой источника и адресата, а также отсутствие такого классификатора не приведет к вызову метода от определения того, перекрываются ли источник и место назначения, и если да, замените dest на src + (dest-src), который, поскольку он получен из src, будет разрешен для псевдонимов. – supercat
@supercat: Вот почему я назвал это комментарием. Однако 1) «ограничивающие» аргументы «memcpy» позволяют в принципе оптимизировать наивную реализацию, и 2) просто вызов «memcpy» позволяет компилятору предположить, что предоставленные ему аргументы не являются псевдонимами, которые могли бы разрешить некоторую оптимизацию вокруг вызова «memcpy». –