Реальное использование ключевого слова C99 'restrict'?

StackOverflow https://stackoverflow.com/questions/745870

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

Видишь: Каково строгое правило псевдонимирования?

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top