При отладке некоторого устаревшего кода я наткнулся на удивительное (для меня) поведение компилятора. Теперь я хотел бы знать, не имеет ли какое-либо предложение в спецификации С++ следующую оптимизацию, где игнорируются побочные эффекты от вызова функции при условии:
Мой компилятор - Microsoft Visual С++ 11.00.60610.1.
Ответ 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;
}