Подтвердить что ты не робот

Предоставляет ли С++ оптимизирующий компилятор игнорировать побочные эффекты при условии состояния?

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

void bar() 
{
   extern int upper_bound;
   upper_bound--;
}

void foo()
{
   extern int upper_bound; // from some other translation unit, initially ~ 10
   for (int i = 0; i < upper_bound; ) {
      bar();
   }
}

В полученном результате есть путь управления, в котором upper_bound сохраняется в регистре, а декремент upper_bound в bar() никогда не вступает в силу.

Мой компилятор - Microsoft Visual С++ 11.00.60610.1.

Честно говоря, я не вижу много места для маневра в 6.5.3 и 6.5.1 N3242, но я хочу быть уверенным, что я не пропущу что-то очевидное.

4b9b3361

Ответ 1

В стандарте четко и однозначно разъясняется, что два объявления upper_bound относятся к одному и тому же объекту.

3.5 Программа и связь [basic.link]

9 Два имени, одинаковые (раздел 3) и объявленные в разных областях, должны обозначать одну и ту же переменную, функцию, тип, перечислитель, шаблон или пространство имен, если

  • оба имени имеют внешнюю связь, или оба имени имеют внутреннюю связь и объявляются в одной и той же единице перевода; и
  • оба имени относятся к членам одного и того же пространства имен или к членам, а не по наследованию того же класса; и
  • когда оба имени обозначают функции, списки параметров типа функций (8.3.5) идентичны; и
  • когда оба имени обозначают функциональные шаблоны, подписи (14.5.6.1) одинаковы.

Оба имени имеют внешнюю связь. Оба имени относятся к члену в глобальном пространстве имен. Ни одно имя не обозначает функцию или шаблон функции. Поэтому оба названия относятся к одному и тому же объекту. Предполагая, что тот факт, что у вас есть отдельные объявления, делает недействительными такие основные факты, - это сказать, что int i = 0; int &j = i; j = 1; return i; может возвращать ноль, потому что компилятор мог забыть, что означает j. Конечно, это должно вернуться 1. Это должно работать, просто и просто. Если это не так, вы обнаружили ошибку компилятора.

Ответ 2

Это поведение кажется правильным, если вы немного подходите к стандарту.

Первый намек в примечании в разделе 3.3.1/4, в котором говорится:

Локальные объявления extern (3.5) могут ввести имя в декларативную область, где появляется декларация, а также ввести (возможно, не видимое) имя в охватываемое пространство имен;

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

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

К счастью, в разделе 3.5/7 есть точность, следующая:

Если объявление области видимости объекта с привязкой не найдено, ссылается на какое-либо другое объявление, то этот объект является членом самого внутреннего пространства имен. Однако такое объявление не вводит имя участника в область его пространства имен.

И они даже предоставляют пример:

namespace X {
  void p() {
    q();              // error: q not yet declared
    extern void q();  // q is a member of namespace X
  }

  void middle() {
    q();              // error: q not yet declared
  }
}

который непосредственно применим к приведенному вами примеру.

Итак, суть проблемы заключается в том, что компилятору требуется не, чтобы сделать связь между первым объявлением upper_bound (в баре) и вторым (в foo).

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

void bar() 
{
   extern int upper_bound_1;
   upper_bound_1--;
}

void foo()
{
   extern int upper_bound_2;
   for (int i = 0; i < upper_bound_2; ) {
      bar();
   }
}

Который становится следующим: из-за функции вставки строки:

void foo()
{
   extern int upper_bound_1;
   extern int upper_bound_2;
   while( 0 < upper_bound_2 ) {
      upper_bound_1--;
   }
}

Это явно бесконечный цикл (насколько это известно компилятору), и даже если upper_bound был объявлен volatile, он просто имел бы точку окончания undefined (всякий раз, когда upper_bound происходит с внешней установкой 0 или менее). При уменьшении переменной (upper_bound_1) бесконечное (или неопределенное) количество раз имеет undefined поведение из-за переполнения. Таким образом, компилятор может ничего не делать, что является допустимым поведением, если это поведение undefined, очевидно. Итак, код становится:

void foo()
{
   extern int upper_bound_2;
   while( 0 < upper_bound_2 ) { };
}

Это именно то, что вы видите в списке сборок для функции, которую производит GCC 4.8.2 (с -O3):

    .globl  _Z3foov
    .type   _Z3foov, @function
_Z3foov:
.LFB1:
   .cfi_startproc
    movl    upper_bound(%rip), %eax
    testl   %eax, %eax
    jle .L6
.L5:
    jmp .L5
    .p2align 4,,10
    .p2align 3
.L6:
    rep ret
    .cfi_endproc
.LFE1:
    .size   _Z3foov, .-_Z3foov

Что можно зафиксировать, добавив объявление глобальной переменной внешней переменной, как таковое:

extern int upper_bound;

void bar() 
{
   extern int upper_bound;
   upper_bound--;
}

void foo()
{
   extern int upper_bound;
   for (int i = 0; i < upper_bound; ) {
      bar();
   }
}

Что создает эту сборку:

_Z3foov:
.LFB1:
    .cfi_startproc
    movl    upper_bound(%rip), %eax
    testl   %eax, %eax
    jle .L2
    movl    $0, upper_bound(%rip)
.L2:
    rep ret
    .cfi_endproc
.LFE1:
    .size   _Z3foov, .-_Z3foov

Каково предполагаемое поведение, т.е. наблюдаемое поведение foo() эквивалентно:

void foo()
{
   extern int upper_bound;
   upper_bound = 0;
}