Реальное использование ключевого слова C99 'restrict'?
-
09-09-2019 - |
Вопрос
Я просматривал некоторую документацию и вопросы / ответы и увидел, что об этом упоминалось.Я прочитал краткое описание, в котором говорилось, что это будет в основном обещание программиста, что указатель не будет использоваться для указания куда-либо еще.
Кто-нибудь может предложить несколько реальных случаев, когда это действительно стоит использовать?
Решение
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
, с другим примером, здесь.
Другие советы
Тот самый Пример из Википедии является очень озаряющий.
Это ясно показывает, как это позволяет сохранить одну инструкцию по сборке.
Без ограничений:
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)
Для непосвященных, соглашение о вызове является:
rdi
= первый параметрrsi
= второй параметрrdx
= третий параметр
Вывод GCC был даже понятнее, чем статья в wiki:4 инструкции против 3 инструкций.
Массивы
Пока у нас есть экономия на одной инструкции, но если указатель представляет собой массивы, подлежащие циклическому перемещению, что является распространенным случаем использования, то можно сохранить кучу инструкций, как упоминалось в суперкат.
Рассмотрим, например,:
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 итераций в ширину раскручивание петли которые я не собираюсь здесь воспроизводить :-)
У меня не хватило терпения сравнить их, но я считаю, что ограниченная версия будет быстрее.
С99
Давайте для полноты картины рассмотрим стандарт.
restrict
говорит, что два указателя не могут указывать на перекрывающиеся области памяти.Наиболее распространенное использование - для аргументов функции.
Это ограничивает способ вызова функции, но допускает больше оптимизаций во время компиляции.
Если вызывающий абонент не выполняет restrict
контракт, неопределенное поведение.
Тот самый Осадка C99 N1256 6.7.3/7 "Определители типов" говорит:
Предполагаемое использование квалификатора restrict (как и класса хранения register) заключается в содействии оптимизации, и удаление всех экземпляров квалификатора из всех единиц предварительной обработки перевода, составляющих соответствующую программу, не меняет его значения (т. е. наблюдаемого поведения).
и 6.7.3.1 "Формальное определение restrict" дает кровавые подробности.
Строгое правило сглаживания
Тот самый restrict
ключевое слово влияет только на указатели совместимых типов (напримердва int*
) потому что строгие правила псевдонимирования гласят, что псевдонимирование несовместимых типов по умолчанию является неопределенным поведением, и поэтому компиляторы могут предположить, что этого не происходит, и оптимизировать его.
Видишь: Каково строгое правило псевдонимирования?
Смотрите также
- C ++ 14 пока не имеет аналога для
restrict
, но GCC имеет__restrict__
в качестве расширения: Что означает ключевое слово restrict в C ++? - Много вопросов, которые задают:судя по кровавым деталям, этот код UB или нет?
- Вопрос "когда использовать": Когда использовать restrict, а когда нет
- Связанный с GCC
__attribute__((malloc))
, который говорит , что возвращаемое значение функции ничему не присваивается псевдонимов: ССАГПЗ:__атрибут__((malloc))