Разве разыменовывает a char * запрещает строгую оптимизацию псевдонимов?
В качестве примера рассмотрим следующий фрагмент:
*pInt = 0xFFFF;
*pFloat = 5.0;
Поскольку они являются указателями int
и float
, компилятор предположит, что они не являются псевдонимами и могут их обменять, например.
Теперь предположим, что мы придаем этому следующее:
*pInt = 0xFFFF;
*pChar = 'X';
*pFloat = 5.0;
Так как char*
разрешено псевдоним что-либо, он может указывать на *pInt
, поэтому присвоение *pInt
не может быть перемещено за пределы назначения *pChar
, поскольку оно может на законных основаниях указывать на *pInt
и устанавливать его первый байт на "X".
Точно так же pChar
может указывать на *pFloat
, назначение *pFloat
не может быть перенесено перед назначением char, потому что код может намереваться свести на нет эффекты предыдущей настройки байта, переназначив *pFloat
.
Означает ли это, что я могу писать и читать через char*
для создания барьеров для перегруппировки и других оптимизаций, связанных с строгим псевдонимом?
Ответы
Ответ 1
Сглаживание указателя в основном имеет смысл в сценариях, когда компилятор не может знать, имеет ли переменная указателя псевдоним другого указателя или нет. Как и в случае, когда вы компилируете функцию, расположенную в другой единицы перевода, чем вызывающий.
void func (char* pChar, float* pFloat)
{
*pChar = 'X';
*pFloat = 5.0;
}
Здесь назначение pFloat
действительно не может быть секвенировано до pChar
, потому что компилятор не может вычесть, что pChar
не указывает в том же месте, что и pFloat
.
Однако, столкнувшись с этим сценарием, компилятор может (и, вероятно, будет) добавлять проверку времени выполнения, чтобы увидеть, могут ли адреса указывать на перекрывающуюся память или нет. Если да, то код должен быть упорядочен в данном порядке. Если нет, тогда код может быть реорганизован и оптимизирован.
Это означает, что вы получите только поведение, подобное барьерной памяти, в случае, если указатели фактически выполняют псевдоним/точку в перекрывающейся памяти. Если нет, то все ставки, касающиеся упорядочения инструкций, будут отключены. Так что это, вероятно, не механизм, на который вы должны положиться.
Ответ 2
Я думаю, что в целом вы не можете использовать это как своего рода барьер последовательности. Причина в том, что компилятор мог выполнить некоторую версию вашего кода
if (pInt == pChar || pFloat == pChar) {
// be careful
} else {
// no aliasing
}
Очевидно, что для простого случая, когда вы представляете это, нет никаких преимуществ, но может быть полезно, если ваши указатели не изменяются в большой части кода.
Если вы просто используете это как средство для "барьера" с помощью манекена pChar
, часть else
всегда будет побеждать. Но там компилятор может предположить, что не существует псевдонимов и всегда может изменить порядок назначений.
Единственными данными, которые иначе не связаны, для которых стандарт C предоставляет гарантии переупорядочения, являются атомарные объекты, которые работают с последовательной согласованностью.
Ответ 3
Если программе нужно использовать пинирование с использованием указателей, единственным надежным способом обеспечения ее работы с gcc и, вероятно, также с clang является использование `-fno-strict-aliasing '. "Современные" компиляторы будут агрессивно выделять код, который не может изменить биты, которые хранятся объектом, а затем использовать полученный недостаток такого кода для "оправдания" оптимизаций, которые иначе не были бы законными. Например,
struct s1 {unsigned short x;};
struct s2 {unsigned short x;};
int test(struct s1 *p1, struct s2 *p2)
{
if (p1->x)
{
p2->x = 12;
unsigned char *cp = (unsigned char*)p1;
unsigned char c0=cp[0] ^ 1,c1=cp[1] ^ 2;
cp[0]=c0 ^ 1; cp[1]=c1 ^ 2;
}
return p1->x;
}
и clang, и gcc генерируют код, который возвращает значение, которое p1 имел, когда выполнялся оператор if. Я ничего не вижу в стандарте, который бы оправдывал это (если p1 == p2, все содержимое * p2 будет считываться через типы символов в дискретные объекты типа "char", что определяется поведением, а содержимое этих дискретные объекты будут использованы для перезаписи всего содержимого * p1, которое также определяется поведением), но gcc и clang будут решать, что, поскольку значения, которые записываются в cp [0] и cp [1], будут соответствовать тому, что уже существует, он должен опустить эти операции.