2016-07-06 1 views
8

Это было вдохновлено этим вопросом/ответом и последующим обсуждением в комментариях: Is the definition of “volatile” this volatile, or is GCC having some standard compliancy problems?. Основываясь на других и моей интерпретации того, что должно происходить, как обсуждалось в комментариях, я отправил его в GCC Bugzilla: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=71793 Другие соответствующие ответы по-прежнему приветствуются.Почему переменная локальная переменная оптимизирована по-разному от изменчивого аргумента и почему оптимизатор генерирует цикл no-op из последнего?

Кроме того, что нить с тех пор породила этот вопрос: Does accessing a declared non-volatile object through a volatile reference/pointer confer volatile rules upon said accesses?

Интро

Я знаю 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 А.К.А. 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 ISN 't объявлено volatile и никогда не читается, что означает, что изменения в нем могут быть закреплены в соответствии с правилом как-если. Хорошо, имеет смысл.

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

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

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

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

Вопросы

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

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

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

+1

TL; DR - Если это не изменяет наблюдаемое поведение программы, действительно ли это имеет значение? –

+1

Стандарт C++ 11 (я также предполагаю, что C++ 14) говорит: «Доступ к неустойчивым объектам оценивается строго в соответствии с правилами абстрактной машины». Другими словами, правило «как-если» не применяется - вы должны строго следовать правилам абстрактной машины. Я думаю, что поведение 'g()' нарушает это; Я предполагаю, что это ошибка оптимизатора. Я также предполагаю, что большинство людей скажут, что это ошибка с низким приоритетом, и что некоторые (многие?) Не согласятся с тем, что это ошибка в первую очередь. –

+0

@MichaelBurr Отличная цитата, не только для ее актуальности, но и для фантастического термина «абстрактная машина». Я имею дело с большим количеством тех. Я склонен думать, что это ошибка в смысле надзора, поскольку можно было бы ожидать, что 2 типа локальной переменной будут вести себя одинаково - как по моему grok, единственное заметное различие - это время, построена. Однако из-за сложности оптимизации и бесчисленных перестановок упорядочения различных этапов и т. Д. Это, очевидно, не так просто. Я бы никогда не осмелился классифицировать его как ошибку '' ittybitty'-priority, но вывод был бы замечательным. –

ответ

2

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

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

+0

Это похоже на то, что Ричард Биенер ответил на мой билет - https://gcc.gnu.org/bugzilla/show_bug.cgi?id=71793 - но его ответ и изменения в билете, главным образом тег 'неправильный код' , указывают, что он не считает, что все в порядке. Вы? Выделение 'g (x)' в регистре похоже на деталь ABI - в этом случае, спасибо за механистическое объяснение, но не разрешение на разрыв 'volatile'. Похоже, что компилятор должен изменить свое поведение, чтобы вести себя правильно в этом случае. –

+0

Ну, какое поведение вы ожидаете? Компилятор не может выполнить чтение по памяти, потому что нет места для чтения памяти. Операция чтения абстрактной машины действительно не нужна здесь. Он мог бы скопировать значение в память и прочитать из этого места, но это имело бы смысл только в том случае, если 'x' действительно может скрыть' g'. – avdgrinten

+0

Снова отбросив сомнительную полезность локальной переменной volatile, я ожидаю то же, что и для любого другого объекта volatile, который был объявлен: чтение и запись должны происходить в памяти и не могут быть отменены или переупорядочены (по сравнению с другими на том же объекте). Поэтому я бы ожидал, что 'x' не будет выделяться в регистре, а не в стеке, как и в других 2 случаях. Может ли компилятор сделать это без нарушения ABI? Если так, я думаю, что это то, что он должен делать. –

Смежные вопросы