Короткое замыкание на булевых операндах без побочных эффектов - PullRequest
7 голосов
/ 09 сентября 2011

Для вознаграждения: Как это поведение можно отключить в каждом конкретном случае без отключения или снижения уровня оптимизации?

Следующее условное выражение было скомпилировано в MinGW GCC 3.4.5, где a имеет тип signed long, а m имеет тип unsigned long.

if (!a && m > 0x002 && m < 0x111)

CFLAGS были использованы -g -O2. Вот соответствующий вывод сборки GCC (сбрасывается с objdump)

120:    8b 5d d0                mov    ebx,DWORD PTR [ebp-0x30]
123:    85 db                   test   ebx,ebx
125:    0f 94 c0                sete   al
128:    31 d2                   xor    edx,edx
12a:    83 7d d4 02             cmp    DWORD PTR [ebp-0x2c],0x2
12e:    0f 97 c2                seta   dl
131:    85 c2                   test   edx,eax
133:    0f 84 1e 01 00 00       je     257 <_MyFunction+0x227>
139:    81 7d d4 10 01 00 00    cmp    DWORD PTR [ebp-0x2c],0x110
140:    0f 87 11 01 00 00       ja     257 <_MyFunction+0x227>

120 - 131 можно легко отследить как первую оценку !a, а затем оценку m > 0x002. Первый условный переход не происходит до 133. К этому времени были вычислены два выражения, независимо от результата первого выражения: !a. Если a было равно нулю, выражение можно (и нужно) заключить немедленно, что здесь не делается.

Как это относится к стандарту C, который требует от логических операторов короткого замыкания, как только может быть определен результат?

Ответы [ 5 ]

10 голосов
/ 09 сентября 2011

Стандарт C определяет только поведение «абстрактной машины»; это не указывает поколение сборки. Пока наблюдаемое поведение программы совпадает с наблюдаемым на абстрактной машине, реализация может использовать любой физический механизм, который ей нравится, для реализации языковых конструкций. Соответствующий раздел в стандарте (C99): 5.1.2.3 Выполнение программы.

6 голосов
/ 09 сентября 2011

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

Например, попробуйте

if (printf("a") || printf("b")) {
    printf("c\n");
}

и должно быть напечатано ac

5 голосов
/ 21 сентября 2011

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

Точки последовательности являются управляющими выражениями (оценки в if, switch, while, do и во всех трех разделах for), логические ИЛИ и И, условия (?:), запятые и оператор return.

Чтобы предотвратить оптимизацию компилятора в этих точках, вы должны объявить вашу переменную volatile. В вашем примере вы можете указать

volatile long a;
unsigned long m;
{...}
if (!a && m > 0x002 && m < 0x111) {...}

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

4 голосов
/ 09 сентября 2011

Оптимизация компилятора - он получает результат в EBX, перемещает его в AL, часть EAX, выполняет вторую проверку в EDX, а затем разветвляет на основе сравнения EAX и EDX.Это сохраняет ветку и делает код работающим быстрее, без каких-либо различий с точки зрения побочных эффектов.

Если вы компилируете с -O0 вместо -O2, я думаю, что это даст более наивную сборкуэто более соответствует вашим ожиданиям.

3 голосов
/ 25 сентября 2011

Код ведет себя корректно (т.е. в соответствии с требованиями языкового стандарта) в любом случае.

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

Единственный действительно надежный способ гарантировать удовлетворительную последовательность кода сборки - это явное написание кода сборки. GCC поддерживает встроенную сборку.

Код C определяет поведение. Код сборки указывает машинный код.

Но все это поднимает вопрос: почему это важно для вас? (Я не говорю, что это не должно, я просто не понимаю, почему это должно быть.)

РЕДАКТИРОВАТЬ: Как точно определены a и m? Если, как вы предлагаете, они связаны с отображаемыми в памяти устройствами, то они должны быть объявлены volatile - и это может быть именно решением вашей проблемы. Если они просто обычные переменные, то компилятор может делать с ними все, что ему нравится (при условии, что это не влияет на видимое поведение программы) , потому что вы не просили его не .

...