Реалистичное использование ключевого слова C99 "Restrict"?
Я просматривал некоторую документацию и вопросы / ответы и видел упомянутое. Я прочитал краткое описание, в котором говорится, что программист обещает, что указатель не будет использоваться для указания другого места.
Может ли кто-нибудь предложить некоторые реалистичные случаи, когда это действительно стоит использовать?
3 ответа
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 был даже более ясным, чем статья в вики: 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
контракт, неопределенное поведение.
Проект C99 N1256 6.7.3/7 " Спецификаторы типов" гласит:
Предполагаемое использование квалификатора restrict (например, класса хранения регистров) состоит в содействии оптимизации, и удаление всех экземпляров классификатора из всех блоков предварительной обработки, составляющих соответствующую программу, не меняет его значения (т. Е. Наблюдаемое поведение).
и 6.7.3.1 "Формальное определение ограничения" дает кровные детали.
Строгое правило алиасинга
restrict
ключевое слово влияет только на указатели совместимых типов (например, два int*
) потому что строгие правила псевдонимов говорят, что псевдонимы несовместимых типов являются неопределенным поведением по умолчанию, и поэтому компиляторы могут предположить, что этого не происходит, и оптимизировать их.
Смотрите: что такое строгое правило наложения имен?
Смотрите также
- C++14 еще не имеет аналога для
restrict
, но GCC имеет__restrict__
как расширение: что означает ключевое слово restrict в C++? - Много вопросов, которые задают: согласно кровавым деталям, этот код UB или нет?
- Вопрос "когда использовать": когда использовать ограничение, а когда нет
- Связанный GCC
__attribute__((malloc))
, который говорит, что возвращаемое значение функции ни к чему не привязано: GCC: __attribute __ ((malloc))
Следующий код C99 возвращает либо 0, либо 1, в зависимости от квалификатора ограничения :
__attribute__((noinline))
int process(const int * restrict const a, int * const b) {
*b /= (*a + 1) ;
return *a + *b ;
}
int main(void) {
int data[2] = {1, 2};
return process(&data[0], &data[0]);
}
Вы можете построить реалистичные примеры, используя фрагмент, особенно когда * a является условием цикла.
Компилировать сgcc -std=c99 -Wall -pedantic -O3 main.c
.