Почему изменяемая локальная переменная оптимизируется иначе, чем изменяемый аргумент, и почему оптимизатор генерирует цикл 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 (Debianstable 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 и никогда не читается, что означает, что изменения к нему могут быть отменены подкак будто править. ОК, имеет смысл.

Ну, я имею в виду ...своего рода, Мол, не совсем, потому чтоvolatile действительно для аппаратных регистров, и ясно, что локальное значение не может быть одним из них - и иначе не может быть изменено вvolatile Кстати, если его адрес не передан ... что это не так. Слушай, просто не так много смыслаvolatile местные ценности. Но C ++ позволяет нам объявлять их и пытается сделатьчто-то с ними. И вот, как всегда растерянные, мы спотыкаемся вперед.

g(): Какие. Перемещаяvolatile источник в параметр передачи по значению, который все еще является еще одной локальной переменной, GCC почему-то решает, что это не так илиМеньше volatileи поэтому ему не нужно читать его каждую итерацию ... но он все еще выполняет цикл,несмотря на то, что его тело сейчас ничего не делает.

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

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

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

ВопросыПочему локальное значение, объявленное в теле, приводит к отличным результатам от параметра по значению, при этом первое чтение разрешается оптимизировать? Оба объявленыvolatile, Ни один адрес не выдан - и не имеютstatic адрес, исключающий любой inline-ASMPOKEry - поэтому они никогда не могут быть изменены без функции. Компилятор может видеть, что каждый из них является константой, его никогда не нужно перечитывать, иvolatile просто не правда -так (А) либопозволил быть избранным при таких ограничениях? (игра актеровкак будто они не были объявленыvolatile) -и (B) почему только один получает право? Некоторыеvolatile локальные переменные большеvolatile чем другие?Оставим это несоответствие на мгновение: после того, как чтение было оптимизировано, почему компилятор все еще генерирует цикл?Это ничего не делает! Почему оптимизатор не исключает этокак будто петля не была закодирована?

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

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

Ответы на вопрос(1)

Ваш ответ на вопрос