Подтвердить что ты не робот

GCC и строгая сглаживание между массивами того же типа

Контекст

"Строгое псевдонижение", названное в честь оптимизации GCC, является предположением компилятора о том, что значение в памяти не будет доступно через значение l типа ( "объявленный тип" ), сильно отличающийся от типа, написанный с ( "эффективным типом" ). Это предположение допускает неправильные преобразования кода, если необходимо учитывать возможность того, что запись указателю на float может изменить глобальную переменную типа int.

Как GCC, так и Clang, извлекающие наибольшее значение из стандартное описание, полное темных углов, и имеющие предвзятость для выполнения сгенерированного кода на практике предположим, что указатель на int первый член a struct thing не имеет указателя на первый элемент int элемента struct object:

struct thing { int a; };
struct object { int a; };

int e(struct thing *p, struct object *q) {
  p->a = 1;
  q->a = 2;
  return p->a;
}

Оба GCC и Clang выводят, что функция всегда возвращает 1, то есть p и q не могут быть алиасами для та же ячейка памяти:

e:
        movl    $1, (%rdi)
        movl    $1, %eax
        movl    $2, (%rsi)
        ret

До тех пор, пока вы согласитесь с аргументацией для этой оптимизации, неудивительно, что p->t[3] и q->t[2] также считаются непересекающимися lvalues ​​в следующем фрагменте (или, скорее, вызывающий вызывает UB, если они псевдоним):

struct arr { int t[10]; };

int h(struct arr *p, struct arr *q) {
  p->t[3] = 1;
  q->t[2] = 2;
  return p->t[3];
}

GCC оптимизирует указанную выше функцию h:

h:
        movl    $1, 12(%rdi)
        movl    $1, %eax
        movl    $2, 8(%rsi)
        ret

До сих пор так хорошо, если вы видите p->a или p->t[3] как-то доступ к целому struct thing (соответственно struct arr), можно утверждать, что создание псевдонима местоположения нарушит правила изложенных в 6.5: 6-7. Аргументом, что это подход GCC, является это сообщение, часть длинной нити, которая также обсуждала роль профсоюзов в строгих правилах псевдонимов.

Вопрос

У меня есть сомнения, однако, в следующем примере, в котором нет struct:

int g(int (*p)[10], int (*q)[10]) {
  (*p)[3] = 1;
  (*q)[4] = 2;
  return (*p)[3];
}

GCC версии 4.4.7 через текущий снимок версии 7 на Matt Godbolt полезный веб-сайт оптимизирует функцию g, как если бы (*p)[3] и (*q)[4] не могли быть псевдонимом (точнее, как если бы программа вызывала UB, если они это сделали ):

g:
        movl    $1, 12(%rdi)
        movl    $1, %eax
        movl    $2, 16(%rsi)
        ret

Есть ли какое-либо чтение стандарта, которое оправдывает этот очень строгий подход к строгому псевдониму? Если оптимизация GCC здесь может быть оправдана, применимы ли аргументы также к оптимизации функций f и k, которые не оптимизированы GCC?

int f(int (*p)[10], int (*q)[9]) {
  (*p)[3] = 1;
  (*q)[3] = 2;
  return (*p)[3];
}

int k(int (*p)[10], int (*q)[9]) {
  (*p)[3] = 1;
  (*q)[2] = 2;
  return (*p)[3];
}

Я готов обсудить это с разработчиками GCC, но я должен сначала решить, не сообщая об ошибке правильности для функции g или пропущенной оптимизации для f и k.

4b9b3361

Ответ 1

В:

int g(int (*p)[10], int (*q)[10]) {
  (*p)[3] = 1;
  (*q)[4] = 2;
  return (*p)[3];
}

*p и *q - lvalues ​​типа массива; Если они могут перекрываться, доступ к ним регулируется параграфом 7 раздела 6.5 (так называемое "строгое правило псевдонимов" ). Однако, поскольку их тип тот же, что не представляет проблемы для этого кода. Стандарт, однако, весьма смутно относится к ряду соответствующих проблем, которые потребуются для всестороннего ответа на этот вопрос, например:

  • Do (*p) и (*q) действительно требуют "доступа" (как этот термин используется в 6.5p7) к массивам, на которые они указывают? Если они этого не делают, возникает соблазн предположить, что выражения (*p)[3] и (*q)[4] существенно деградируют до арифметики указателя и разыгрывают два int *, которые могут быть явно псевдонимом. (Это не совсем необоснованная точка зрения: 6.5.2.1 Subscribing Array Subscribping говорит, что одно из выражений должно иметь тип '' указатель на полный тип объекта, другое выражение должно иметь целочисленный тип, а результат имеет тип '' type - поэтому значение lvalue массива обязательно ухудшается до указателя в соответствии с обычными правилами преобразования, единственный вопрос заключается в том, был ли доступ к массиву до того, как произошло преобразование).

  • Однако для защиты представления, что (*p)[3] является чисто эквивалентным *((int *)p + 3), нам нужно было бы показать, что (*p)[3] не требует оценки (*p), или если это произойдет, доступ не имеет поведения undefined (или определенного, но нежелательного поведения). Я не верю, что в точной формулировке стандарта есть обоснование, позволяющее (*p) не оцениваться; это означает, что выражение (*p) не должно иметь поведения undefined, если определено поведение (*p)[3]. Таким образом, вопрос действительно сводится к тому, что *p и *q определили поведение, если они относятся к частично перекрывающимся массивам того же типа, и действительно, возможно ли, что они могут одновременно это сделать.

Для определения оператора * в стандарте указано:

если он указывает на объект, результатом является значение l, обозначающее объект

  • означает ли это, что указатель должен указывать на начало объекта? (Похоже, что это то, что имеется в виду). Должен ли какой-либо объект быть установлен каким-либо образом до его доступа (и не устанавливает ли объект какой-либо перекрывающийся объект)? Если оба случая, *p и *q не могут пересекаться - поскольку установление любого объекта приведет к аннулированию другого - и поэтому (*p)[3] и (*q)[4] не могут быть псевдонимом.

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

В частности, формулировка "эффективного типа" в 6.5 предлагает средство, с помощью которого может быть установлен объект определенного типа. Похоже, хорошая ставка, что это должно быть окончательным; то есть вы не можете установить объект, отличный от установки его эффективного типа (в том числе посредством его объявленного типа), и что доступ к другим типам ограничен; далее, установление объекта не устанавливает любой существующий перекрывающийся объект (чтобы быть ясным, это экстраполяция, а не фактическая формулировка). Таким образом, если (*p)[3] и (*q)[4] могут быть псевдонимом, то либо p, либо q не указывает на объект, поэтому один из *p или *q имеет поведение undefined.

Ответ 2

IMHO, стандарт не позволяет массивам определенного размера перекрываться в одно и то же время (*). Проект n1570 говорит в 6.2.7 Совместимый тип и составной тип (выделение мое):

§2 Все объявления, относящиеся к одному и тому же объекту или функции, должны иметь совместимый тип; в противном случае поведение undefined.

§3 Составной тип может быть построен из двух совместимых типов; это тип, который совместим с обоими двумя типами и удовлетворяет следующим условиям:

  • Если оба типа являются типами массивов, применяются следующие правила:
    • Если один тип представляет собой массив с известным постоянным размером, то составным типом является массив этот размер.
      ...

Поскольку объект должен иметь сохраненное значение, доступ к которому можно получить только с помощью выражения lvalue, имеющего совместимый тип (упрощенное чтение 6.5 выражений §7), вы не можете создавать массивы с разными размерами и не иметь массивы того же размера, что и перекрытия. Таким образом, в функции g, p и q должны либо указывать один и тот же массив, либо непересекающиеся массивы, что позволяет оптимизировать.

Для функций f и k я понимаю, что оптимизация будет разрешена по стандарту, но разработчики не будут реализованы. Мы должны помнить, что как только один из параметров является простым указателем, он может указывать на любой элемент другого массива, и никакая оптимизация не может произойти. Поэтому я считаю, что отсутствие оптимизации - всего лишь пример известного правила для UB: все может случиться, включая ожидаемый результат.