В чем причина строгого правила псевдонимов? - PullRequest
0 голосов
/ 28 декабря 2018

Меня сейчас интересует обоснование строгого правила псевдонимов.Я понимаю, что определенные псевдонимы не разрешены в C и что цель состоит в том, чтобы разрешить оптимизацию, но я удивлен, что это было предпочтительным решением по сравнению с приведением типов трассировки при определении стандарта.

Итак, очевидно, следующеепример нарушает правило строгого псевдонима:

uint64_t swap(uint64_t val)
{
    uint64_t copy = val;
    uint32_t *ptr = (uint32_t*)© // strict aliasing violation
    uint32_t tmp = ptr[0];
    ptr[0] = ptr[1];
    ptr[1] = tmp;
    return copy;
}

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

Итак, какие проблемы со строгим правилом псевдонимов я пропустил, что компилятор не может легко решить автоматическиобнаружить возможные оптимизации)?

Ответы [ 4 ]

0 голосов
/ 09 января 2019

В сноске к N1570 p6.5p7 четко указана цель правила: сказать, когда что-то может быть псевдонимом.Относительно того, почему правило написано так, чтобы запретить такие конструкции, как ваша , которые не включают в себя псевдонимы как написано (поскольку все обращения с использованием uint32_t* выполняются в тех контекстах, где оно визуально только что получено из uint64_t, это, скорее всего, потому, что авторы Стандарта признали, что любой, кто добросовестно пытается создать качественную реализацию, подходящую для низкоуровневого программирования, будет поддерживать такие конструкции, как ваша (как «популярное расширение»), независимо от того, утвержден ли СтандартЭто тот же принцип, более явный в отношении таких конструкций, как:

unsigned mulMod65536(unsigned short x, unsigned short y)
{ return (x*y) & 65535u; }

Согласно Обоснованию, обычные реализации будут обрабатывать операции с короткими беззнаковыми значениями способом, эквивалентным арифметике без знака , даже еслирезультат находится между INT_MAX+1u и UINT_MAX, за исключением случаев, когда применяются определенные условия. Нет необходимости иметь специальное правило, чтобы компилятор обрабатывал выражения, включающие короткие типы без знака, как беззнаковые, когда результаты aприведен к unsigned, потому что - согласно авторам Стандарта - обычные реализации делают это даже без такого правила .

Стандарт никогда не предназначался для полного определения всего, чтоследует ожидать качественной реализации, которая претендует на то, чтобы быть подходящей для любой конкретной цели.Действительно, даже не требуется, чтобы реализации были пригодны для какой-либо полезной цели (в обосновании даже признается возможность «соответствующей» реализации такого низкого качества, которая не может осмысленно обрабатывать что-либо, кроме одной надуманной и бесполезной программы).

0 голосов
/ 28 декабря 2018

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

Пример:

int f(long *L, short *S)
{
    *L=42;
    *S=43;
    return *L;
}

int g(long *restrict L, short *restrict S)
{
    *L=42;
    *S=43;
    return *L;
}

Скомпилировано с gcc -O3 -fno-strict-aliasing на x86_64:

f:
        movl    $43, %eax
        movq    $42, (%rdi)
        movw    %ax, (%rsi)
        movq    (%rdi), %rax ; <<*L reloaded here cuz *S =43 might have changed it
        ret
g:
        movl    $43, %eax
        movq    $42, (%rdi)
        movw    %ax, (%rsi)
        movl    $42, %eax     ; <<42 constant-propagated from *L=42 because *S=43 cannot have changed it  (because of `restrict`)
        ret

Скомпилировано с gcc -O3 (подразумевается -fstrict-alising) на x86_64:

f:
        movl    $43, %eax
        movq    $42, (%rdi)
        movw    %ax, (%rsi)
        movl    $42, %eax   ; <<same as w/ restrict
        ret
g:
        movl    $43, %eax
        movq    $42, (%rdi)
        movw    %ax, (%rsi)
        movl    $42, %eax
        ret

https://gcc.godbolt.org/z/rQDNGt

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

0 голосов
/ 05 января 2019

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

Такой код:

float f(int *pi, float *pf) {
  *pi = 1;
  return *pf;
}

при использовании с pi и pf, имеющими один и тот же адрес, где *pf предназначен для переосмысления битов недавно написанного *pi, считается неразумным Таким образом, уважаемые члены комитета (а до них разработчики языка C) не считали целесообразным требовать от компилятора избегать преобразования программы из здравого смысла в чуть более сложном примере:

float f(int *pi, double *pf) {
  (*pi)++;
  (*pf) *= 2.;
  (*pi)++;
}

Здесь, если разрешить угловой случай, когда оба указателя указывают на один и тот же объект, можно было бы сделать любое упрощение, когда приращения слиты неверно;Предполагая, что такого псевдонима не происходит, код может быть скомпилирован как:

float f(int *pi, double *pf) {
  (*pf) *= 2.;
  (*pi) += 2;
}
0 голосов
/ 28 декабря 2018

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

Подумайте, содержит ли вместо этого код:

foo(&val, ptr)

, где объявление foo равно void foo(uint64_t *a, uint32_t *b);.Тогда внутри foo, который может находиться в другой единице перевода, компилятор не сможет узнать, что a и b указывают (части) на один и тот же объект.

Тогда естьдва варианта: во-первых, язык может разрешить псевдонимы, и в этом случае компилятор, переводя foo, не может выполнить оптимизацию, полагаясь на то, что *a и *b различны.Например, всякий раз, когда что-то записывается в *b, компилятор должен сгенерировать ассемблерный код для перезагрузки *a, поскольку это могло измениться.Оптимизации, такие как хранение копии *a в регистрах при работе с ней, не допускаются.

Второй вариант, два, - запретить алиасы (в частности, не определять поведение, если программа это делает).).В этом случае компилятор может выполнить оптимизацию, полагаясь на то, что *a и *b различаются.

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

...