Какова семантика C99 "ограничивается" в отношении указателей на указатели?

Я делаю много матричной арифметики и хотел бы воспользоваться классификатором указателей C99 restrict.

Я хотел бы настроить мои матрицы как указатели на указатели, чтобы упростить подписи, например:

int **A = malloc (ncols * sizeof(int *));
A[0] = malloc (nrows * ncols * sizof(int));
for (int i=1; i < ncols; i++) {
    A[i] = A[0] + i*nrows;
}

Теперь для функции умножения матрицы

void mmultiply ( int nrows, int ncols, int **Out, int **A, int **B);

Должен ли я квалифицировать оба указателя аргументов как ограниченные? Это действительный синтаксис, но мне сложно определить, будет ли int *restrict *restrict вести себя иначе, чем int **restrict.

Затем, если указатели правильно ограничены, происходит доступ к элементам через A[0][col*nrows + row] undefined? (т.е. будет ли компилятор предполагать, что я получаю доступ к матрице только через A[col][row] для значений row, таких, что row < nrow)? Или я просто остаюсь последовательным?

Ответ 1

Для первого вопроса "да" это будет означать что-то другое, если вы используете оба квалификатора restrict, в частности, что указатели также не будут сглажены. Что касается того, имеет ли значение какое-либо значение: теоретически да, на практике это зависит от оптимизатора.

Во втором вопросе "да" предполагается, что доступ к любому доступу через указатель строки осуществляется только через указатель строки.

Вы тоже можете бросить const.

Наконец, если это gcc в -O2, -O3 или -Os, компилятор уже выполняет анализ псевдонимов на основе типов. Я уверен, что другие компиляторы тоже это делают. Это означает, что ограничение указателей на ints уже понято, оставляя только массивы, которые могут храниться друг для друга.

В сумме оптимизатор будет считать, что указатели не хранятся в качестве ints, и он знает, что он не пишет записи указателя во время цикла.

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

Ответ 2

Внешний (второй) ограничитель сообщает компилятору, что ни один из массивов указателей (A, B и out) не является псевдонимом. Внутренний (первый) ограничитель сообщает компилятору, что ни один из массивов ints (указываемый элементами массивов указателей) псевдоним.

Если вы получаете доступ к A [0] [col * nrows + row] и A [col] [row], тогда вы нарушаете внутренний ограничитель, поэтому все может сломаться.

Ответ 3

int **restrict только утверждает, что память, адресованная Out, A и B, не перекрывается (за исключением того, что A и B могут перекрываться, если ваша функция не изменяет ни одну из них). Это означает массивы указателей. Он ничего не утверждает о содержании памяти, на которое указывают Out, A и B. В примечании 117 в n1124 говорится:

если идентификатор p имеет тип (int ** ограничить), то выражения указателя p и p + 1 основаны на ограниченный объект указателя по p, но выражения указателя * p и p [1] не являются.

По аналогии с const, я подозреваю, что квалификация с restrict дважды будет утверждать то, что вы хотите, а это значит, что ни одно из значений в массиве не указывает на перекрывающуюся память. Но, читая стандарт, я не могу доказать себе, что это действительно так. Я считаю, что "Пусть D является объявлением обычного идентификатора, который предоставляет средство для обозначения объекта P в качестве ограничивающего ограничения указателя на тип T", действительно означает, что для int *restrict *restrict A, тогда A [0] и A [1 ] - объекты, обозначенные как ограничитель-указатель на int. Но это довольно тяжелая легализация.

Я понятия не имею, способен ли ваш компилятор делать что-либо с этими знаниями, заметьте. Ясно, что это может быть вопрос о том, реализовано ли оно.

Поэтому я не знаю, что вы приобрели в обычном массиве C 2-D, где вы просто выделили rows * cols * sizeof(int) и указали с помощью A[cols*row + col]. Тогда вам явно нужно только одно использование ограничения, и любой компилятор, который делает что-либо с restrict, сможет переупорядочить чтения из A и B через записи в Out. Без restrict, конечно, он не может, поэтому, делая то, что вы делаете, вы бросаете себя на милость компилятора. Если он не может справиться с двойным ограничением, только с одним ограничивающим случаем, то ваша двойная направленность обойдется вам в оптимизации.

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

обращается к элементам через A [0] [col * nrows + row] undefined?

Да, если элемент изменен одним из способов доступа, поскольку это делает A [0] псевдоним для памяти также доступным через A [col]. Это было бы хорошо, если бы только A и B были ограничивающими указателями указателями, но не если A [0] и A [col].

Я предполагаю, что вы не изменяете A в этой функции, так что на самом деле этот псевдоним прекрасен. Однако, если вы сделали то же самое с Out, поведение было бы undefined.