"Наблюдаемое поведение" и свобода компилятора для исключения/преобразования элементов кода С++

После прочтения этого обсуждения я понял, что почти полностью не понимаю:)

Поскольку описание абстрактной машины С++ недостаточно строго (сравнивая, например, с спецификацией JVM), и если точный ответ невозможен, я предпочел бы получить неофициальные разъяснения о правилах, которые были бы разумными "хорошими" ( не вредоносная).

Ключевая концепция части 1.9 свободы реализации Стандартной адресации называется так: если правило:

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

Термин "наблюдаемое поведение", согласно стандарту (I cite n3092), означает следующее:

- Доступ к неустойчивым объектам оценивается строго в соответствии с правила абстрактной машины.

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

- Динамика ввода и вывода интерактивных устройств место таким образом, что на самом деле запрос на вывод перед тем, как программа ждет ввода. Что представляет собой интерактивный устройство определено реализацией.

Итак, грубо говоря, порядок и операнды операций волатильного доступа и операций io должны быть сохранены; реализация может вносить произвольные изменения в программу, которые сохраняют эти инварианты (по сравнению с некоторым допустимым поведением абстрактной машины С++)

  • Можно ли ожидать, что не-вредоносная реализация значительно расширит операции io (например, любой системный вызов из кода пользователя рассматривается как таковая операция)? (Например, блокировка/разблокировка мьютекса RAII не будет выбрасываться компилятором, если оболочка RAII не содержит летучих).

  • Насколько глубоко "поведенческое наблюдение" должно опускаться с пользовательского уровня на уровне С++ в библиотечные/системные вызовы? Вопрос заключается, конечно, только в вызовах библиотеки, которые не предназначены для io/volatile доступа с точки зрения пользователя (например, в качестве новых операций/удаления), но могут (и обычно) получать доступ к летучим или io в реализации библиотеки/системы. Должен ли компилятор обрабатывать такие вызовы с точки зрения пользователя (и рассматривать такие побочные эффекты как не наблюдаемые) или с точки зрения "библиотеки" (и рассматривать побочные эффекты как наблюдаемые)?

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

  • Или я совершенно не прав, и компилятор запрещен для удаления любого кода на С++, за исключением случаев, явно упомянутых стандартом (как удаление копии)

Ответ 1

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

Что касается глубины этого, то это зависит от реализации библиотеки. В gcc стандартная библиотека C использует атрибуты компилятора для информирования компилятора о потенциальных побочных эффектах (или их отсутствии). Например, strlen помечен как чистый атрибут, который позволяет компилятору преобразовать этот код:

char p[] = "Hi there\n";
for ( int i = 0; i < strlen(p); ++i ) std::cout << p[i];

в

char * p = get_string();
int __length = strlen(p);
for ( int i = 0; i < __length; ++i ) std::cout << p[i];

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

То есть, в общем, компилятор не будет удалять код, если он не может доказать, что он не имеет побочных эффектов, то есть не повлияет на результат программы. Обратите внимание, что это относится не только к volatile и io, так как любое изменение переменной может иметь наблюдаемое поведение позднее.

Что касается вопроса 3, компилятор удалит ваш код только в том случае, если программа ведет себя точно так же, как если бы код присутствовал (исключение копии - исключение), поэтому вам даже не нужно заботиться о том, удаляет его компилятор или нет. Что касается вопроса 4, то правило as-if: Если результат неявного рефакторинга, сделанный компилятором, дает тот же результат, тогда он может свободно выполнить изменение. Рассмотрим:

unsigned int fact = 1;
for ( unsigned int i = 1; i < 5; ++i ) fact *= i;

Компилятор может свободно заменить этот код:

unsigned int fact = 120; // I think the math is correct... imagine it is

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

Не беспокойтесь слишком много о том, какое наблюдаемое поведение и правило as-if означает, что они в основном означают, что компилятор должен предоставить результат, который вы запрограммировали в своем коде, даже если он свободен для достижения этого результата другой путь.

ИЗМЕНИТЬ

@Konrad поднимает действительно хорошую точку в отношении исходного примера, который у меня был с strlen: как компилятор знает, что вызовы strlen могут быть удалены? И ответ заключается в том, что в исходном примере он не может, и, следовательно, он не может преодолеть вызовы. Ничто не говорит компилятору, что указатель, возвращаемый из функции get_string(), не относится к памяти, которая изменяется в другом месте. Я исправил пример использования локального массива.

В модифицированном примере массив является локальным, и компилятор может проверить, нет ли других указателей, которые ссылаются на одну и ту же память. strlen принимает указатель на const и поэтому promises не изменяет содержащуюся память, а функция чиста, поэтому promises не изменять какое-либо другое состояние. Массив не модифицируется внутри конструкции цикла и не собирает всю информацию, которую компилятор может определить, что достаточно одного вызова strlen. Без чистого спецификатора компилятор не может знать, будет ли результат strlen отличаться в разных вызовах и должен его вызвать.

Ответ 2

Абстрактная машина, определенная стандартом, будет иметь конкретную ввода, выведите один из набора конкретных результатов. В общем, все, что есть гарантировано, что для этого конкретного ввода скомпилированный код будет производят один из возможных конкретных результатов. Дьявол находится в подробности, однако, есть несколько моментов, о которых нужно помнить.

Наиболее важным из них является, вероятно, тот факт, что если программа undefined, компилятор может делать абсолютно что угодно. Все ставки отключены. Компиляторы могут использовать и использовать потенциальное поведение undefined для оптимизация: например, если код содержит что-то вроде *p = (*q) ++, компилятор может заключить, что p и q не являются псевдонимами для того же переменная.

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

Что касается volatile, то стаднард говорит, что доступ к неустойчивые объекты - наблюдаемое поведение, но оно оставляет смысл "доступ" до реализации. На практике вы не можете рассчитывать многое на volatile в эти дни; фактический доступ к неустойчивым объектам может кажутся внешнему наблюдателю в другом порядке, чем они встречаются в программа. (Это, возможно, является нарушением стандартным, по крайней мере. Однако реальная ситуация с самые современные компиляторы, работающие на современной архитектуре.)

Большинство реализаций обрабатывают все системные вызовы как "IO". С относится к мьютексам, конечно: что касается С++ 03, как только вы начинаете второй поток, у вас есть поведение undefined (из С++ точка зрения, Posix или Windows это определяют), а в С++ 11, примитивы синхронизации являются частью языка и ограничивают набор возможных выходов. (Компилятор, конечно, может устранить если это может доказать, что они не нужны.)

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

Что касается третьего вопроса: я думаю, вы смотрите на него из неправильный угол. Компиляторы не "исключают" код, и нет конкретный оператор в программе привязан к определенному блоку код. Ваша программа (полная программа) определяет конкретную семантики, и компилятор должен сделать что-то, что создает исполняемая программа, имеющая эти семантики. Наиболее очевидное решение для составителя компилятора необходимо принимать каждое выражение отдельно и генерировать код для него, но что точка зрения автора компилятора, не ваш. Вы вставляете исходный код и получаете исполняемый файл; но много заявлений не приводят к какому-либо коду, и даже для тех, кто это делает, нет необходимости в отношениях друг к другу. В этом смысле идея "предотвращения исключения кода" не делает смысл: ваша программа имеет семантику, указанную стандартом, и все вы можете попросить (и все, что вас должно заинтересовать), заключается в том, что конечный исполняемый файл имеет эти семантики. (Ваша четвертая точка похожа: компилятор не "удаляет" какой-либо код.)

Ответ 3

Я не могу говорить за то, что должны делать компиляторы, но вот что делают некоторые компиляторы

#include <array>
int main()
{
    std::array<int, 5> a;
    for(size_t p = 0; p<5; ++p)
        a[p] = 2*p;
}

сборка с gcc 4.5.2:

main:
     xorl    %eax, %eax
     ret

замена массива на вектор показывает, что new/delete не подлежат устранению:

#include <vector>
int main()
{
    std::vector<int> a(5);
    for(size_t p = 0; p<5; ++p)
        a[p] = 2*p;
}

сборка с gcc 4.5.2:

main:
    subq    $8, %rsp
    movl    $20, %edi
    call    _Znwm          # operator new(unsigned long)
    movl    $0, (%rax)
    movl    $2, 4(%rax)
    movq    %rax, %rdi
    movl    $4, 8(%rax)
    movl    $6, 12(%rax)
    movl    $8, 16(%rax)
    call    _ZdlPv         # operator delete(void*)
    xorl    %eax, %eax
    addq    $8, %rsp
    ret

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

Ответ 4

1. Разумно ли ожидать, что не-злонамеренная реализация значительно расширяет операции io

Да. Предполагая, что побочные эффекты по умолчанию. Помимо параметров по умолчанию, компиляторы должны доказывать что-то (кроме исключения копирования).

2. Насколько глубоко "поведенческое наблюдение" должно опускаться с пользовательского уровня на уровне С++ в библиотечные/системные вызовы?

Как можно глубже. Используя текущий стандартный С++, компилятор не может искать библиотеку со значением static library, то есть вызовы, которые нацелены на функцию внутри некоторых вызовов ".a-" или ".lib file" поэтому побочные эффекты.

Используя традиционную модель компиляции с несколькими объектными файлами, компилятор даже не может смотреть внешние вызовы. Оптимизация единицы компиляции могут выполняться на link-time.

Btw, некоторые компиляторы имеют расширение, чтобы рассказать об чистых функциях. Из документации gcc:

Многие функции не имеют эффектов, кроме возвращаемого значения, а их возвращаемое значение зависит только от параметров и/или глобальных переменных. Такая функция может быть подвержена общему исключению подвыражения и оптимизации цикла, как и в случае с арифметическим оператором. Эти функции должны быть объявлены с атрибутом pure. Например,

      int square (int) __attribute__ ((pure));

говорит, что гипотетический квадрат функции безопасен для вызова меньше раз, чем говорит программа.   Некоторые из общих примеров чистых функций - strlen или memcmp. Интересные нечистые функции   являются функциями с бесконечными циклами или теми, которые зависят от энергозависимой памяти или другого системного ресурса,   который может меняться между двумя последовательными вызовами (например, feof в многопотоковой среде).

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

блок компиляции A:

int foo() {
    extern int x;
    return x;
}

блок компиляции B:

int x;
int bar() {
    for (x=0; x<10; ++x) {
        std::cout << foo() << '\n'; 
    }
}

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

3. Если мне нужно предотвратить исключение какого-либо кода компилятором

За исключением того, что посмотрев на объект-дамп, как вы могли судить, было ли что-то удалено?

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

В этом отношении расширения компилятора (например, OpenMP) помогают вам судить. Некоторые встроенные механизмы также существуют как переменные volatile.

Существует ли дерево, если его никто не может наблюдать? Et hop, мы находимся в квантовой механике.

4. Или я совершенно не прав, и компилятор не разрешает удалить любой код С++, за исключением случаев, явно указанных стандартом (как удаление копии)

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

Ответ 5

Одно отличие состоит в том, что Java предназначен для работы только на одной платформе - JVM. Это значительно упрощает "достаточно строгое" в спецификации, так как существует только платформа, и вы можете точно документировать, как она работает.

С++ предназначен для работы на широком спектре платформ и делает это изначально, без промежуточного уровня абстракции, но напрямую использует базовые аппаратные функции. Поэтому он решил разрешить функциональность, которая фактически существует на разных платформах. Например, результат некоторых операций сдвига, таких как int(1) << 33, может быть разным в разных системах, поскольку это работает на оборудовании.

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

Например, на мэйнфрейме IBM никто не ожидает, что с плавающей точкой будет совместимость с IEEE, поскольку серия мэйнфреймов намного старше, чем стандарт IEEE. Тем не менее С++ позволяет использовать базовое оборудование, а Java - нет. Является ли это преимуществом или дискомфортом для любого языка? Это зависит!


В рамках ограничений и допущений языка разумная реализация должна вести себя так, как если бы она была похожа на вашу кодировку в вашей программе. Если вы выполняете системные вызовы, такие как блокировка мьютекса, у компилятора есть опции not, которые знают, что делают вызовы, и поэтому не могут их удалить или знать точно, что они делают и поэтому также знаю, могут ли они быть удалены или нет. Результат тот же!

Если вы звоните в стандартную библиотеку, компилятор может очень хорошо знать точно, что делает вызов, как это описано в стандарте. Затем он имеет возможность действительно вызвать функцию, заменить ее каким-либо другим кодом или полностью пропустить ее, если она не действует. Например, std::strlen("Hello world!") можно заменить на 12. Некоторые компиляторы делают это, и вы не заметите.