«Если рабочий хочет хорошо выполнять свою работу, он должен сначала заточить свои инструменты» — Конфуций, «Аналитики Конфуция. Лу Лингун»
титульная страница > программирование > Непонятное ключевое слово «restrict» в C

Непонятное ключевое слово «restrict» в C

Опубликовано 4 ноября 2024 г.
Просматривать:412

The Obscure “restrict” Keyword in C

Введение

Среди прочего, в C99 добавлено ключевое слово ограничения, позволяющее программисту указать, что указатель является единственным указателем на данный объект в области видимости и, следовательно, дать компилятору «подсказку». », что он может выполнять дополнительную оптимизацию при доступе к объекту через этот указатель.

Проблема

Чтобы проиллюстрировать проблему, которую должно было решить ограничение, рассмотрим такую ​​функцию:

void update_ptrs( int *p, int *q, int const *v ) {
  *p  = *v;
  *q  = *v;
}

для которого компилятор сгенерирует код x86-64 типа:

mov eax, [rdx]  ; tmp = *v   // 1
add [rdi], eax  ; *p  = tmp
mov eax, [rdx]  ; tmp = *v   // 3
add [rsi], eax  ; *q  = tmp

Вы можете задаться вопросом, почему он генерирует строку 3, поскольку она кажется избыточной для строки 1. Проблема в том, что компилятор не может знать, что вы не сделали что-то вроде этого:

int x = 1, v = 2;
update_ptrs( &v, &x, &v );   // x = 5, v = 4

В update_ptrs(), p и v будут alias того же int, поэтому компилятор должен перестраховаться и предположить, что значение *v может меняться между чтениями, отсюда и дополнительная инструкция mov.

В общем, указатели в C затрудняют оптимизацию, поскольку компилятор не может знать, являются ли два указателя псевдонимами друг друга. В коде, критичном к производительности, исключение операций чтения из памяти может стать огромной победой, , если компилятор сможет сделать это безопасно.

Решение

Чтобы решить вышеупомянутую проблему, в C было добавлено ограничение, позволяющее указать, что данный указатель является только указателем на объект в области действия указателя, т. е. в области действия указателя нет другого указателя в той же области псевдонимов. это.

Чтобы использовать ограничение, вы вставляете его между * и именем указателя в объявлении. Функция update_ptrs(), переписанная для использования ограничения, будет выглядеть так:

void update_ptrs_v2( int *restrict p, int *restrict q,
                     int const *restrict v ) {
  *p  = *v;
  *q  = *v;
}

(Чтение справа налево, например, v — ограниченный указатель на константу int; или используйте cdecl.)

Добавив ограничение, компилятор теперь может генерировать такой код:

mov eax, [rdx]  ; tmp = *v
add [rdi], eax  ; *p  = tmp
add [rsi], eax  ; *q  = tmp

Теперь компилятор смог исключить предыдущую строку 3 дополнительной инструкции mov.

Возможно, самым известным примером использования ограничения является стандартная библиотечная функция memcpy(). Это самый быстрый способ скопировать фрагмент памяти, , если адреса источника и назначения не перекрываются. Немного более медленная функция memmove() предназначена для использования, когда адреса перекрываются.

Подводные камни

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

Обратите внимание, что ограничение действует для определенной области. Назначение одного ограниченного указателя другому в той же области действия приводит к неопределенному поведению:

void f( int *restrict d, int *restrict s ) {
  int *restrict p = s;    // undefined behavior

Однако вы можете легко назначить ограниченный указатель неограниченному указателю:

void f( int *restrict d, int *restrict s ) {
  int *p = s;             // OK

Несмотря на то, что p не имеет ограничений, компилятор все равно может выполнять ту же оптимизацию.

Также можно назначить ограниченный указатель во внутренней области другому во внешней области (но не наоборот):

void f( int *restrict d, int *restrict s ) {
  {                       // inner scope
    int *restrict p = s;  // OK
    // ...
    s = p;                // undefined behavior
  }
}

Когда (и когда нет) использовать ограничение

Во-первых, вам обязательно следует профилировать свой код (и, возможно, даже посмотреть на сгенерированный ассемблерный код), чтобы увидеть, действительно ли использование ограничения приводит к значительному повышению производительности, чтобы оправдать риск потенциальных ошибок. Диагностировать ошибки, вызванные неправильным использованием ограничения, очень сложно.

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

void safer( unsigned n ) {
  n  = n % 2 != 0;  // make even by rounding up
  int *const array = malloc( n * sizeof(unsigned) );
  unsigned *restrict half_1st = array;
  unsigned *restrict half_2nd = array   n/2;
  // ...
  free( array );
}

код может безопасно работать с первой и второй половинами массива, поскольку они не перекрываются (при условии, что вы никогда не получаете доступ к half_1st[n/2] или дальше).

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

Разнообразный

Только указатели на объекты (или void) могут быть квалифицированы с помощью ограничения:

restrict int x;       // error: can't restrict object
int restrict *p;      // error: pointer to restrict object
int (*restrict f)();  // error: pointer-to-function

Вы можете использовать ограничение для членов структуры, например:

struct node {
   void *restrict data;
   struct node *restrict left;
   struct node *restrict right;
};

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

Наконец, в C нет ограничений. Почему нет? Есть длинный ответ, но версия TL;DR такова:

  • Это может быть источником труднообнаружимых ошибок, которые комитет C не хотел импортировать из C.
  • Расширенное использование указателей в C, например, this, еще больше усложняет безопасное использование ограничения.

Однако многие компиляторы имеют расширение __restrict__.

Заключение

В ограниченных случаях использование ограничения может привести к повышению производительности, но есть несколько существенных ошибок. Если вы планируете использовать ограничение, сначала профилируйте свой код.

Используйте с умом.

Заявление о выпуске Эта статья воспроизведена по адресу: https://dev.to/pauljlucas/the-obscure-restrict-keyword-in-c-2541. Если есть какие-либо нарушения, свяжитесь с [email protected], чтобы удалить их.
Последний учебник Более>

Изучайте китайский

Отказ от ответственности: Все предоставленные ресурсы частично взяты из Интернета. В случае нарушения ваших авторских прав или других прав и интересов, пожалуйста, объясните подробные причины и предоставьте доказательства авторских прав или прав и интересов, а затем отправьте их по электронной почте: [email protected]. Мы сделаем это за вас как можно скорее.

Copyright© 2022 湘ICP备2022001581号-3