Понимание ограничителя по примерам

Поведение ключевого слова restrict определено в C99 на 6.7.3.1:

Пусть D - объявление обычного идентификатора, обеспечивающего средство обозначение объекта P в качестве ограничивающего указателя указателя на тип T.

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

В дальнейшем выражение указателя E называется основанным на объекте P if (в некоторой точке последовательности при выполнении B до оценка E), изменяющая P, чтобы указать на копию объекта массива в который он ранее указывал, изменит значение E.119) Примечание. что '' based '' определяется только для выражений с типами указателей.

Во время каждого исполнения B пусть L - любое l-значение, которое имеет & L на основе P. Если L используется для доступа к значению объекта X, то он и X также модифицируется (любыми способами), то следующие применяются требования: T не должно быть const-qualified. Все остальные значения используемый для доступа к значению X, также имеет свой адрес, основанный на P. Каждый доступ, который модифицирует X, рассматривается также для изменения P, для цели этого подпункта. Если P присвоено значение a выражение указателя E, которое основано на другом ограниченном указателе объект P2, связанный с блоком B2, то либо выполнение B2 начинаются до исполнения B, или исполнение B2 должно до назначения. Если эти требования не выполняются, то поведение undefined.

Как и все остальные, мне трудно понять все тонкости этого определения. В качестве ответа на этот вопрос я хотел бы увидеть множество хороших примеров для каждого требования в 4-м абзаце об использовании, которое нарушит это требование. Эта статья:

http://web.archive.org/web/20120225055041/http://developers.sun.com/solaris/articles/cc_restrict.html

делает хорошую работу по представлению правил в терминах "компилятор может предположить..."; расширяя этот шаблон и связывая в предположениях, которые может сделать компилятор, и как они не выдерживают, причем каждый пример будет большим.

Ответ 1

Ниже я буду ссылаться на usecases из бумаги Sun, связанной с этим вопросом.

(относительно) очевидным случаем был бы случай mem_copy(), который подпадает под категорию 2-й категории пользователей в документе Sun (функция f1()). Скажем, мы имеем следующие две реализации:

void mem_copy_1(void * restrict s1, const void * restrict s2, size_t n);
void mem_copy_2(void *          s1, const void *          s2, size_t n);

Поскольку мы знаем, что между двумя массивами, на которые указывают s1 и s2, нет совпадения, код для первой функции будет прямым:

void mem_copy_1(void * restrict s1, const void * restrict s2, size_t n)
{
     // naively copy array s2 to array s1.
     for (int i=0; i<n; i++)
         s1[i] = s2[i];
     return;
}

s2 = '....................1234567890abcde' <- s2 before the naive copy
s1 = '1234567890abcde....................' <- s1 after the naive copy
s2 = '....................1234567890abcde' <- s2 after the naive copy

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

Например, скажем s1 = 100 и s2 = 105. Тогда, если n=15, после копии вновь скопированный массив s1 переполнит первые 10 байтов исходного массива s2. Мы должны убедиться, что сначала скопировали нижние байты.

s2 = '.....1234567890abcde' <- s2 before the naive copy
s1 = '1234567890abcde.....' <- s1 after the naive copy
s2 = '.....67890abcdeabcde' <- s2 after the naive copy

Однако, если, s1 = 105 и s2 = 100, то сначала записывание нижних байтов будет превышать последние 10 байтов источника s2, и мы получим ошибочную копию.

s2 = '1234567890abcde.....' <- s2 before the naive copy
s1 = '.....123451234512345' <- s1 after the naive copy - not what we wanted
s2 = '123451234512345.....' <- s2 after the naive copy

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

void mem_copy_2(void *s1, const void *s2, size_t n)
{
    if (((unsigned) s1) < ((unsigned) s2))
        for (int i=0; i<n; i++)
             s1[i] = s2[i];
    else
        for (int i=(n-1); i>=0; i--)
             s1[i] = s2[i];
    return;
}

Легко видеть, как модификатор restrict дает возможность улучшить оптимизацию скорости, исключая дополнительный код и решение if-else.

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


1-я утилиза (функция init()) может рассматриваться как вариация на втором, описанная выше. Здесь два массива создаются с одним вызовом выделения динамической памяти.

Назначение двух указателей как restrict -ed позволяет оптимизировать, в котором порядок инструкций будет иметь значение в противном случае. Например, если у нас есть код:

a1[5] = 4;
a2[3] = 8;

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

OTOH, если указатели не restrict -ed, то важно, чтобы первое назначение выполнялось до второго. Это связано с тем, что существует возможность того, что a1[5] и a2[3] на самом деле являются тем же самым местом памяти! Легко видеть, что когда это так, тогда конечное значение должно быть 8. Если мы изменим порядок инструкций, то конечное значение будет 4!

Опять же, если непересекающиеся указатели заданы этому restrict -принятому коду, результат undefined.