Правила использования ключевого слова ограничения в C?

Я пытаюсь понять, когда и когда не использовать ключевое слово restrict в C и в каких ситуациях оно дает ощутимую выгоду.

После прочтения "" Демистификация ограничивающего ключевого слова" (что дает некоторые эмпирические правила использования), создается впечатление, что когда функции передаются указатели, она должна учитывать возможность того, что указанные данные могут перекрываться (псевдоним) с любыми другими аргументами, передаваемыми в функцию. Для функции:

foo(int *a, int *b, int *c, int n) {
    for (int i = 0; i<n; ++i) {
        b[i] = b[i] + c[i];
        a[i] = a[i] + b[i] * c[i];
    } 
}

компилятор должен перезагрузить c во втором выражении, потому что, возможно, b и c указывают на то же место. Он также должен ждать, пока b будет сохранен, прежде чем он сможет загрузить a по той же причине. Затем он должен ждать сохранения a и должен перезагрузить b и c в начале следующего цикла. Если вы вызываете функцию следующим образом:

int a[N];
foo(a, a, a, N);

то вы можете понять, почему компилятор должен это сделать. Использование restrict эффективно сообщает компилятору, что вы никогда этого не сделаете, чтобы он мог сбросить избыточную нагрузку c и загрузить a до того, как будет сохранен b.

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

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

Теперь, когда вещи начинают меняться нечеткими.

В статье Ульриха Дреппера " Что каждый программист должен знать о памяти" он делает выражение, что "если не используется ограничение, все ссылки на указатели являются потенциальными источниками псевдонимов", и он дает конкретный пример кода матрицы подматрицы умножить, где он использует restrict.

Однако, когда я компилирую его пример кода с или без restrict, я получаю идентичные двоичные файлы в обоих случаях. Я использую gcc version 4.2.4 (Ubuntu 4.2.4-1ubuntu4)

То, что я не могу понять в следующем коде, заключается в том, нужно ли его переписывать, чтобы более широко использовать restrict, или если анализ псевдонимов в GCC настолько хорош, что он может выяснить, что ни один из аргументов не псевдоним. Для чисто образовательных целей, как я могу использовать или не использовать значение restrict в этом коде - и почему?

Для restrict скомпилировано с помощью

gcc -DCLS=$(getconf LEVEL1_DCACHE_LINESIZE) -DUSE_RESTRICT -Wextra -std=c99 -O3 matrixMul.c -o matrixMul

Просто удалите -DUSE_RESTRICT, чтобы не использовать restrict.

#include <stdlib.h>
#include <stdio.h>
#include <emmintrin.h>

#ifdef USE_RESTRICT
#else
#define restrict
#endif

#define N 1000
double _res[N][N] __attribute__ ((aligned (64)));
double _mul1[N][N] __attribute__ ((aligned (64)))
    = { [0 ... (N-1)] 
    = { [0 ... (N-1)] = 1.1f }};
double _mul2[N][N] __attribute__ ((aligned (64)))
    = { [0 ... (N-1)] 
    = { [0 ... (N-1)] = 2.2f }};

#define SM (CLS / sizeof (double))

void mm(double (* restrict res)[N], double (* restrict mul1)[N], 
        double (* restrict mul2)[N]) __attribute__ ((noinline));

void mm(double (* restrict res)[N], double (* restrict mul1)[N], 
        double (* restrict mul2)[N])
{
 int i, i2, j, j2, k, k2; 
    double *restrict rres; 
    double *restrict rmul1; 
    double *restrict rmul2; 

    for (i = 0; i < N; i += SM)
        for (j = 0; j < N; j += SM)
            for (k = 0; k < N; k += SM)
                for (i2 = 0, rres = &res[i][j],
                    rmul1 = &mul1[i][k]; i2 < SM;
                    ++i2, rres += N, rmul1 += N)
                    for (k2 = 0, rmul2 = &mul2[k][j];
                        k2 < SM; ++k2, rmul2 += N)
                        for (j2 = 0; j2 < SM; ++j2)
                          rres[j2] += rmul1[k2] * rmul2[j2];
}

int main (void)
{

    mm(_res, _mul1, _mul2);

 return 0;
}

Ответ 1

Кроме того, GCC 4.0.0-4.4 имеет ошибку регрессии, которая приводит к игнорированию ключевого слова ограничения. Эта ошибка была зафиксирована как фиксированная в 4.5 (я потерял номер ошибки, хотя).

Ответ 2

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

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

Ответ 3

(Я не знаю, действительно ли использование этого ключевого слова дает вам значительное преимущество. Очень просто программисту ошибиться с этим квалификатором, поскольку нет никакого принудительного исполнения, поэтому оптимизатор не может быть уверен, что программист не "ложь".)

Когда вы знаете, что указатель A является единственным указателем на некоторую область памяти, то есть он не имеет псевдонимов (то есть любой другой указатель B обязательно будет неравным с A, B!= A), вы можете сообщить об этом фактуристу оптимизатору, присвоив тип A ключевым словом "ограничить".

Я написал об этом здесь: http://mathdev.org/node/23 и попытался показать, что некоторые ограниченные указатели фактически являются "линейными" (как упоминалось в этот пост).

Ответ 4

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

Это явно зависит от экстентов данных, указывающих на то, что они бросаются в глаза компилятору, - как это было бы в примере выше.

Я считаю, что основное обоснование касается программ, в которых используется STL, и особенно <algorithm>, где сложно или невозможно ввести квалификатор __restrict.

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

Я был бы удивлен, если бы GCC не получил эту оптимизацию.

Ответ 5

Может быть, оптимизация, сделанная здесь, не полагается на указатели, которые не являются псевдонимом? Если вы не предварительно загрузите несколько элементов mul2 перед тем, как записать результат в res2, я не вижу проблемы с псевдонимом.

В первом фрагменте кода, который вы показываете, совершенно ясно, какая проблема с псевдонимами может произойти. Здесь это не так понятно.

Перечитывая статью Dreppers, он конкретно не говорит, что ограничение может решить что угодно. Существует даже эта фраза:

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

В этом коде оптимизация доступа к памяти уже выполнена в рамках алгоритма. Остаточная оптимизация, по-видимому, выполняется в векторизованном коде, представленном в приложении. Так что для кода, представленного здесь, я думаю, что нет никакой разницы, потому что оптимизация, основанная на ограничении, не выполняется. Каждый доступ указателя является источником сглаживания, но не каждая оптимизация зависит от aliassing.

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

Ответ 6

Если есть разница вообще, перемещение mm в отдельный DSO (такой, что gcc больше не может знать все о вызывающем коде), будет способ продемонстрировать это.

Ответ 7

Вы используете 32 или 64-битный Ubuntu? Если 32-разрядный, то вам нужно добавить -march=core2 -mfpmath=sse (или любую другую архитектуру вашего процессора), иначе он не будет использовать SSE. Во-вторых, для того, чтобы включить векторию с GCC 4.2, вам нужно добавить опцию -ftree-vectorize (по 4.3 или 4.4 она включена по умолчанию в -O3). Также может потребоваться добавить -ffast-math (или другой вариант, обеспечивающий расслабленную семантику с плавающей запятой), чтобы позволить компилятору переупорядочить операции с плавающей запятой.

Кроме того, добавьте параметр -ftree-vectorizer-verbose=1, чтобы увидеть, удается ли ему векторизовать цикл или нет; что простой способ проверить эффект добавления ключевого слова ограничения.

Ответ 8

Проблема с вашим примером кода заключается в том, что компилятор просто ввел вызов и увидит, что в вашем примере не существует псевдонимов. Я предлагаю вам удалить функцию main() и скомпилировать ее с помощью -c.