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