Почему изменчивая локальная переменная оптимизирована по-разному от изменчивого аргумента и почему оптимизатор генерирует цикл no-op из последнего?

Фон

Это было вдохновлено этим вопросом/ответом и последующим обсуждением в комментариях: Является ли определение "изменчивым" это изменчивым, или это GCC имеет некоторые стандартные проблемы соответствия?. Основываясь на других и моей интерпретации того, что должно происходить, как обсуждалось в комментариях, я отправил его в GCC Bugzilla: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=71793 Другое соответствующие ответы по-прежнему приветствуются.

Кроме того, этот поток с тех пор породил этот вопрос: Ли доступ к объявленному энергонезависимому объекту посредством волатильной ссылки/указателя вызывает изменчивые правила при упомянутых обращений?

Введение

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

Мое предположение заключается в том, что это объясняется либо очень тонкой интерпретацией стандартных, либо (более вероятных?) просто угловых случаев для используемого оптимизатора. В любом случае, хотя и более академичный, чем практический, я надеюсь, что это будет полезно для анализа, особенно учитывая, как обычно неправильно понимается volatile. Еще несколько точек данных - или, возможно, более вероятно, указывают на это - должны быть хорошими.

Ввод

С учетом этого кода:

#include <cstddef>

void f(void *const p, std::size_t n)
{
    unsigned char *y = static_cast<unsigned char *>(p);
    volatile unsigned char const x = 42;
    // N.B. Yeah, const is weird, but it doesn't change anything

    while (n--) {
        *y++ = x;
    }
}

void g(void *const p, std::size_t n, volatile unsigned char const x)
{
    unsigned char *y = static_cast<unsigned char *>(p);

    while (n--) {
        *y++ = x;
    }
}

void h(void *const p, std::size_t n, volatile unsigned char const &x)
{
    unsigned char *y = static_cast<unsigned char *>(p);

    while (n--) {
        *y++ = x;
    }
}

int main(int, char **)
{
    int y[1000];
    f(&y, sizeof y);
    volatile unsigned char const x{99};
    g(&y, sizeof y, x);
    h(&y, sizeof y, x);
}

Выход

g++ из gcc (Debian 4.9.2-10) 4.9.2 (Debian stable a.k.a. Jessie) с командной строкой g++ -std=c++14 -O3 -S test.cpp создает ниже ASM для main(). Версия Debian 5.4.0-6 (текущий unstable) создает эквивалентный код, но сначала мне нужно запустить старшую, так вот:

main:
.LFB3:
    .cfi_startproc

# f()
    movb    $42, -1(%rsp)
    movl    $4000, %eax
    .p2align 4,,10
    .p2align 3
.L21:
    subq    $1, %rax
    movzbl  -1(%rsp), %edx
    jne .L21

# x = 99
    movb    $99, -2(%rsp)
    movzbl  -2(%rsp), %eax

# g()
    movl    $4000, %eax
    .p2align 4,,10
    .p2align 3
.L22:
    subq    $1, %rax
    jne .L22

# h()
    movl    $4000, %eax
    .p2align 4,,10
    .p2align 3
.L23:
    subq    $1, %rax
    movzbl  -2(%rsp), %edx
    jne .L23

# return 0;
    xorl    %eax, %eax
    ret
    .cfi_endproc

Анализ

Все 3 функции встроены, и оба, которые выделяют локальные переменные volatile, делают это в стеке по довольно очевидным причинам. Но это единственное, что они разделяют...

  • f() обеспечивает чтение с x на каждой итерации, предположительно из-за ее volatile, но просто сбрасывает результат на edx, предположительно потому, что пункт назначения y не объявляется volatile и никогда не читается, что означает, что изменения в нем могут быть сжаты в соответствии с правилом as-if. Хорошо, имеет смысл.

    • Ну, я имею в виду... любопытное. Например, не так, потому что volatile действительно для аппаратных регистров, и, очевидно, локальное значение не может быть одним из них - и в противном случае оно не может быть изменено способом volatile, если его адрес не будет удален... которого нет. Послушайте, из-за локальных значений volatile не должно быть большого смысла. Но С++ позволяет нам объявить их и пытается что-то с ними сделать. И поэтому, смутившись, как всегда, мы спотыкаемся вперед.
  • g(): Что. Перемещая источник volatile в параметр pass-by-value, который по-прежнему является еще одной локальной переменной, GCC каким-то образом решает это или не менее volatile, и поэтому ему не нужно читать каждую итерацию... но он все еще выполняет цикл, несмотря на то, что его тело ничего не делает.

  • h(): Принимая пройденный volatile в качестве передачи по ссылке, восстанавливается такое же эффективное поведение, как f(), поэтому цикл volatile читает.

    • Этот случай сам по себе имеет для меня практический смысл по причинам, изложенным выше, против f(). Чтобы уточнить: Представьте, что x относится к аппаратным регистрам, из которых каждый прочитанный имеет побочные эффекты. Вы не захотите пропустить ни один из них.

Добавление #define volatile /**/ приводит к тому, что main() является no-op, как и следовало ожидать. Итак, когда присутствует, даже на локальной переменной volatile что-то делает... Я просто не знаю, что в случае g(). Что там происходит на Земле?

Вопросы

  • Почему локальное значение, объявленное в теле, производит разные результаты по параметру по значению, при этом первая версия чтения будет оптимизирована? Оба объявлены volatile. У вас также нет адреса с адресом и не имеют адреса static, исключая любой встроенный ASM POKE ry, поэтому они никогда не могут быть изменены с помощью функции. Компилятор может видеть, что каждый из них является постоянным, его никогда не нужно перечитывать, а volatile просто неверно -
    • так (A) либо разрешено отклоняться при таких ограничениях? (действующие как если бы они не были объявлены volatile) -
    • и (B) почему только кто-то уходит? Являются ли некоторые локальные переменные volatile более volatile чем другие?
  • Отбросить эту несогласованность на мгновение: после того, как чтение было оптимизировано, почему компилятор все еще генерирует цикл? Он ничего не делает! Почему оптимизатор не делает это так: если никакой цикл не был закодирован?

Является ли это странным угловым случаем из-за порядка оптимизации анализов или такого? Поскольку код - это глупый мысленный эксперимент, я бы не стал критиковать GCC за это, но было бы хорошо знать наверняка. (Или это g(), о которых люди мечтали во все эти годы?) Если мы не заключим ни одного стандартного отношения ни к одному из этих случаев, я переведу его в свою Bugzilla только для их информации.

И, конечно, более важный вопрос с практической точки зрения, хотя я не хочу, чтобы это затмило потенциал для компилятора geekery... Который, если какой-либо из них, четко определен или правильный в соответствии со стандартом

Ответ 1

Для f: GCC исключает энергонезависимые хранилища (но не нагрузки, которые могут иметь побочные эффекты, если исходное местоположение является аппаратным регистром памяти). Здесь нет ничего удивительного.

Для g: из-за x86_64 ABI параметр x of g выделяется в регистре (т.е. rdx) и не имеет места в памяти. Чтение регистра общего назначения не имеет наблюдаемых побочных эффектов, поэтому мертвое чтение получает исключение.