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

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

Смотрите также

Следующий код 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.

Другие вопросы по тегам