Реалистичное использование ключевого слова "ограничивать" C99?

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

Может ли кто-нибудь предложить некоторые реалистичные случаи, когда его ценность действительно используется?

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

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

См. также