Понимание квалификатора limited на примерах

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

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

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

В дальнейшем говорят, что выражение указателя E основано на объекте P, если (в какой-то точке последовательности выполнения B перед вычислением E) P модифицируется так, чтобы он указывал на копию объекта массива, на который он ранее указывал. изменит значение E.119) Обратите внимание, что «основанный» определен только для выражений с типами указателей.

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

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

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

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


person R.. GitHub STOP HELPING ICE    schedule 04.09.2012    source источник
comment
Связанная (Sun) страница очень хорошо читается по этому вопросу.   -  person ysap    schedule 04.09.2012
comment
Кажется, это признак того, что Оратрол демонтирует свои приобретения. Исправлено это с помощью машины обратного пути.   -  person R.. GitHub STOP HELPING ICE    schedule 01.04.2014


Ответы (1)


Ниже я буду ссылаться на варианты использования из документа 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, во 2-й функции может быть перекрытие. В этом случае нам нужно проверить, расположен ли исходный массив перед целевым или наоборот, и соответственно выбрать границы индекса цикла.

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


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

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

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

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

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

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

person ysap    schedule 04.09.2012
comment
В качестве ответа на этот вопрос я хотел бы увидеть набор хороших примеров для каждого требования в 4-м абзаце способов использования, которые нарушают это требование. Я не думаю, что этот ответ дает хотя бы один такой пример. - person Nemo; 04.09.2012
comment
@Немо - правда? Кажется, я привел 2 примера, относящихся к двум случаям использования, когда предоставление непересекающихся массивов коду, предполагающему ограниченные квалифицированные указатели, может дать ошибочный результат. - person ysap; 04.09.2012
comment
Хотя не исключаю, что не понял вопроса. ;-) - person ysap; 04.09.2012
comment
Я не возражаю против этого ответа, потому что он информативен, но я думаю, что то, что вы сделали, больше объясняет, каково предполагаемое использование / цель ограничения. Цель моего вопроса состоит в том, чтобы выяснить, что на самом деле означают правила использования указателей ограничения, и возможные (и в идеале тонкие) способы, которыми код, пытающийся использовать ограничение, может в конечном итоге нарушить их. - person R.. GitHub STOP HELPING ICE; 04.09.2012
comment
Итак, если это неясно (возможно, мне следовало отредактировать ответ), пример перекрывающихся массивов в двух случаях показывает, как использование этих указателей с кодом restrict нарушает правила. Но опять же - поздний час - может быть, я просто не правильно понял вопрос. - person ysap; 04.09.2012
comment
Я думаю, он хочет по одному примеру для каждого пункта в четвертом абзаце ... Но вы правы, мой отрицательный голос был крайним. Внесите тривиальное изменение в этот ответ, и я отменю отрицательный голос. - person Nemo; 05.09.2012
comment
Ars В Technica есть очень хорошее обсуждение алиасинга и того, как оно ограничивает возможную оптимизацию, при сравнении Fortran и C. - person Yann-Gaël Guéhéneuc; 12.05.2014
comment
В вашем примере эффективно обсуждается разница между memcpy/memmove. Использование restrict заключается в том, чтобы позволить циклу считывать группу данных из источника, а затем записывать группу данных в место назначения, а не читать и записывать байты по отдельности. Без restrict поведение показанного цикла for было бы определено даже в тех случаях, когда dest указывало на один байт дальше src, но оптимизатор, который пытался использовать инструкции многократного чтения для его ускорения, изменил бы его поведение. - person supercat; 23.06.2015