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

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

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 C ++ 11.00.60610.1.

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

22

Решение

Стандарт четко и недвусмысленно разъясняет, что две декларации 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. Это должно работать, просто и понятно. Если это не так, вы нашли ошибку компилятора.

11

Другие решения

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

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

Локальные внешние объявления (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, он просто будет иметь неопределенную точку завершения (всякий раз, когда upper_bound случается внешне быть установленным в 0 или меньше). И уменьшая переменную (upper_bound_1) бесконечное (или неопределенное) количество раз имеет неопределенное поведение из-за переполнения. Следовательно, компилятор может выбрать ничего не делать, что, очевидно, является допустимым поведением, когда оно не определено. И так, код становится:

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 следующим образом:

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;
}
-2

По вопросам рекламы [email protected]