Разве разыменовывает 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], будут соответствовать тому, что уже существует, он должен опустить эти операции.