Реалистичное использование ключевого слова C99 «Restrict»? - PullRequest
166 голосов
/ 14 апреля 2009

Я просматривал некоторую документацию и вопросы / ответы и видел, что это упомянуто. Я прочитал краткое описание, в котором говорится, что программист обещает, что указатель не будет использоваться для указания другого места.

Кто-нибудь может предложить некоторые реалистичные случаи, когда на самом деле стоит использовать это?

Ответы [ 2 ]

164 голосов
/ 14 апреля 2009

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, с другим примером, здесь .

124 голосов

Пример Википедии - очень подсветка.

Наглядно показано, как позволяет сохранить одну инструкцию по сборке .

Без ограничений:

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*), поскольку строгие правила псевдонимов говорят, что псевдонимы несовместимых типов по умолчанию являются неопределенным поведением, и поэтому компиляторы могут предположить, что этого не происходит, и оптимизировать их.

См .: Что такое строгое правило наложения имен?

См. Также

...