атрибуты gcc для функций init-on-first-use - PullRequest
7 голосов
/ 29 июля 2011

Я использовал атрибуты gcc const и pure для функций, которые возвращают указатель на «постоянные» данные, которые выделяются и инициализируются при первом использовании, т.е. где функция будет возвращать одно и то же значение каждый раз, когда онаназывается.В качестве примера (не мой случай использования, а хорошо известный пример) рассмотрим функцию, которая выделяет и вычисляет триггерные таблицы поиска при первом вызове и просто возвращает указатель на существующие таблицы после первого вызова.

Проблема: мне сказали, что это использование некорректно, потому что эти атрибуты запрещают побочные эффекты, и что компилятор может даже полностью оптимизировать вызов в некоторых случаях, если возвращаемое значение не используется.Безопасно ли мое использование атрибутов const / pure или есть какой-либо другой способ сообщить компилятору, что N>1 вызовы функции эквивалентны 1 вызову функции, но что 1 вызов функции не являетсяэквивалентно 0 вызовов функции?Или другими словами, что функция имеет побочные эффекты только при первом вызове?

1 Ответ

7 голосов
/ 29 июля 2011

Я говорю, что это правильно, основываясь на моем понимании pure и const , но если у кого-то есть точное определение двух, пожалуйста, говорите.Это сложно, потому что в документации GCC не указано, что означает, что функция не имеет «никаких эффектов, кроме возвращаемого значения» (для pure ) или «не проверяет никакие значения, кроме их аргументов»(для const ).Очевидно, что все функции имеют некоторые эффекты (они используют циклы процессора, изменяют память) и проверяют некоторые значения (код функции, константы).

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

Простите, если что-то из перечисленного является слишком базовым ...

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

z = f(x);
y = f(x);

становится:

z = y = f(x);

Или полностью исключается, если z и y не используются.

Поэтому мое лучшее предположение состоит в том, что рабочее определение «pure» - это «любая функция, которую можно вызывать меньше раз без изменения семантикипрограмма».Однако вызовы функций не могут быть перемещены, например,

size_t l = strlen(str); // strlen is pure
*some_ptr = '\0';
// Obviously, strlen can't be moved here...

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

// Assuming x and y not aliased, sin can be moved anywhere
*some_ptr = '\0';
double y = sin(x);
*other_ptr = '\0';

Таким образом, мое лучшее предположение состоит в том, что рабочим определением «const» является «любая функция, которая может быть вызвана в любой момент без изменения семантики программы».Однако существует опасность:

__attribute__((const))
double big_math_func(double x, double theta, double iota)
{
    static double table[512];
    static bool initted = false;
    if (!initted) {
        ...
        initted = true;
    }
    ...
    return result;
}

Поскольку это const, компилятор может переупорядочить его ...

pthread_mutex_lock(&mutex);
...
z = big_math_func(x, theta, iota);
...
pthread_mutex_unlock(&mutex);
// big_math_func might go here, if the compiler wants to

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

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

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

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

Редактировать: Я не смог заставить GCC или Clang переместить отдельный вызов функции __attribute__((const)) через другой вызов функции, но это кажется полностьюВозможно, что в будущем что-то подобное случится.Помните, когда -fstrict-aliasing стал значением по умолчанию, и у всех внезапно появилось намного больше ошибок в их программах? Подобные вещи заставляют меня быть осторожнее.

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

Тем не менее, я нашел способ вывести функцию const из критической секции, хотяЭто можно было бы назвать обманом.

__attribute__((const))
extern int const_func(int x);

int func(int x)
{
    int y1, y2;
    y1 = const_func(x);
    pthread_mutex_lock(&mutex);
    y2 = const_func(x);
    pthread_mutex_unlock(&mutex);
    return y1 + y2;
}

Компилятор переводит это в следующий код (из сборки):

int func(int x)
{
    int y;
    y = const_func(x);
    pthread_mutex_lock(&mutex);
    pthread_mutex_unlock(&mutex);
    return y * 2;
}

Обратите внимание, что этого не произойдет только с __attribute__((pure)), атрибутом const и только атрибутом const, вызывающим такое поведение.

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

Заключение 2: Действуйте осторожно, потому что, если вы не знаете, какие обещания вы даете компилятору, будущая версия компилятора может вас удивить.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...