2009-04-14 8 views
136

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

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

+2

'memcpy' vs' memmove' - это один канонический пример. –

+0

@AlexandreC .: Я не думаю, что это особенно применимо, поскольку отсутствие «ограничивающего» квалификатора не означает, что программная логика будет работать с перегрузкой источника и адресата, а также отсутствие такого классификатора не приведет к вызову метода от определения того, перекрываются ли источник и место назначения, и если да, замените dest на src + (dest-src), который, поскольку он получен из src, будет разрешен для псевдонимов. – supercat

+0

@supercat: Вот почему я назвал это комментарием. Однако 1) «ограничивающие» аргументы «memcpy» позволяют в принципе оптимизировать наивную реализацию, и 2) просто вызов «memcpy» позволяет компилятору предположить, что предоставленные ему аргументы не являются псевдонимами, которые могли бы разрешить некоторую оптимизацию вокруг вызова «memcpy». –

ответ

147

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: У Википедии есть запись на restrict, с другим примером, here.

+2

@Michael - Если я не ошибаюсь, проблема будет только тогда, когда 'dest' перекрывает любой из векторов-источников. Почему возникла бы проблема, если 'src1' и' src2' перекрываются? – ysap

+1

Ограничение обычно имеет эффект только при указании на объект, который был изменен, и в этом случае он утверждает, что никакие скрытые побочные эффекты не должны приниматься во внимание. Большинство компиляторов используют его для облегчения векторизации. Msvc использует проверку времени выполнения для перекрытия данных для этой цели. – tim18

+0

Добавление ключевого слова register в переменную цикла for также делает его быстрее в дополнение к добавлению ограничения. – 2017-05-31 02:12:34

95

Wikipedia example является очень освещающий.

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

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

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) 

Для непосвященных calling convention является:

  • 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): Is it better to use std::memcpy() or std::copy() in terms to performance?

Действительно ли 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 итераций широкий loop unrolling, которые я не собираюсь приводить здесь :-)

Я не имел терпения сравнить их, но я не верю что ограниченная версия будет быстрее.

C99

Давайте посмотрим на стандарт для полноты.

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

Это ограничивает способ вызова функции, но позволяет оптимизировать время компиляции.

Если вызывающий абонент не соблюдает договор restrict, неопределенное поведение.

В C99 N1256 draft 6.7.3/7 «Тип классификаторов» говорит:

Намеченное использование ограничения классификатора (например, класс хранения регистров) является содействие оптимизации и удаление всех экземпляров классификатором из все единицы перевода для предварительной обработки, составляющие соответствующую программу, не меняют своего значения (т. е. наблюдаемого поведения).

и 6.7.3.1 «Формальное определение ограничения» дает детали gory.

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

restrict ключевое слово влияет только указатели совместимых типов (например, два int*), потому что строгие правила наложения спектров говорит, что сглаживание несовместимых типов не определено поведение по умолчанию, и поэтому компиляторы могут предположить, что это делает не происходит и не оптимизируется.

См: What is the strict aliasing rule?

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

+3

«Ограничивающий» квалификатор может фактически обеспечить гораздо большую экономию. Например, данный 'void zap (char * ограничивает p1, char * ограничивает p2) {for (int i = 0; i <50; i ++) {p1 [i] = 4; p2 [i] = 9; }} ', определители ограничений позволят компилятору переписать код как« memset (p1,4,50); memset (p2,9,50); ». Ограничение значительно превосходит псевдонимы на основе типов; это компиляторы позора, которые больше сосредоточены на последнем. – supercat

+0

@supercat отличный пример, добавленный в ответ. –

+0

Рад, что вам понравилось. Такие оптимизации могут быть сделаны людьми, но полезно дать свободу компилятора, поскольку компилятор может знать вещи, которые человек не может (например, комбинирование вещей в цикле иногда уменьшает накладные расходы цикла, но разделение вещей может позволить использовать параллелизм или специализированный инструкции). Ключевое слово 'limit' делает это возможным способом, который не использует псевдонимы на основе типов. – supercat