Опубликован: 01.03.2016 | Доступ: свободный | Студентов: 586 / 62 | Длительность: 03:55:00
Лекция 5:

Команды ассемблера

Булевы выражения

Рассмотрим такой код на языке Си:

if(((a  > 5)  & & (b  < 10)) || (c == 0))
{
  do_something();
}

В принципе, булево выражение можно вычислять как обычное арифметическое, то есть в такой последовательности:

  • a > 5
  • b < 10
  • (a > 5) & & (b < 10)
  • c == 0
  • ((a > 5) & & (b < 10)) || (c == 0)

Такой способ вычисления называется полным. Можем ли мы вычислить значение этого выражения быстрее? Смотрите, если c == 0, то всё выражение будет иметь значение true в любом случае, независимо от a и b. А вот если c != 0, то приходится проверять значения a и b. Таким образом, наш код (фактически) превращается в такой:

if(c == 0)
{
  goto do_it;
}
if((a  > 5)  & & (b  < 10))
{
  goto do_it;
}
goto dont_do_it;

do_it:
  do_something();

dont_do_it:

В принципе, можно пойти дальше: если a <= 5, нас не интересует сравнение b < 10: всё равно выражение равно false.

if(c == 0)
{
  goto do_it;
}
if(a  > 5)
{
  if(b  < 10)
  {
    goto do_it;
  }
}
goto dont_do_it;

do_it:
  do_something();

dont_do_it:

Такой способ вычисления выражений называется сокращённым (от англ. short-circuit evaluation), потому что позволяет вычислить выражение, не проверяя всех входящих в него подвыражений. Можно вывести такие формальные правила:

  • если у оператора OR хотя бы один операнд имеет значение true, всё выражение имеет значение true;
  • если у оператора AND хотя бы один операнд имеет значение false, всё выражение имеет значение false.

В принципе, сокращённое вычисление булевых выражений помогает написать более быстрый (а часто и более простой) код. С другой стороны, возникают проблемы, если одно из подвыражений при вычислении вызывает побочные эффекты (англ. side effects), например вызов функции:

if((c == 0) || foo())
{
  do_something();
}

Если мы используем сокращённое вычисление и оказывается, что c == 0, то функция foo() вызвана не будет, потому что от её результата значение выражения уже не зависит. Хорошо это или плохо, зависит от конкретной ситуации, но, без сомнения, способ выполнения такого кода становится не очевидным.

Во многих языках высокого уровня сокращённое вычисление выражений требуется от компилятора стандартом языка (например, в Си). Однако, обычно задаются более строгие правила вычислений. В большинстве стандартов языков требуется, чтобы выражения соединённые оператором OR (или AND) вычислялись строго слева направо, и если очередное значение будет true (соответственно, false для AND), то вычисление данной цепочки OR-ов (AND-ов) прекращается. Но нужно отметить, что первый пример в этой главе всё равно является корректным с точки зрения стандарта Си (хотя c == 0 стоит в конце выражения, а вычисляется первым), так как сравнение локальных переменных не вызывает побочных эффектов и компилятор вправе реорганизовать код таким образом.

Теперь перейдём к тому, как это реализовывается на ассемблере. Начнём с полного вычисления:

cmpl  $5, a
/* так, а что дальше? */

Действительно, нам нужно сохранить результат сравнения в переменную. Из команд, анализирующих флаги, мы знаем только семейство jcc, но они нам не подходят. Кроме jcc, существует семейство setcc. Они проверяют состояние флагов точно так же, как и jcc. На основе флагов операнд устанавливается в 1, если проверяемое условие cc истинно, и в 0, если условие ложно.

setcc операнд

Требуется заметить, что команды setcc работают только с операндами (хранящимися в регистрах и памяти) размером один байт.

Тогда полное вычисление будет выглядеть так:

        cmpl  $5, a
        seta  %al
        cmpl  $10, b
        setb  %bl
        andb  %bl, %al
        cmpl  $0, c
        sete  %bl
        orb   %bl, %al
        jz    is_false
is_true:
        ...
is_false:
        ...

Обратите внимание, что команда or устанавливает флаги, и нам не нужно отдельно сравнивать %al с нулём.

Сокращённое вычисление:

        cmpl  $0, c
        je    is_true
        cmpl  $5, a
        jbe   is_false
        cmpl  $10, b
        jae   is_false
is_true:
        ...
is_false:
        ...

Как видите, этот код является не только более коротким, но и завершает своё исполнение, как только результат становится известен. Таким образом, сокращённое вычисление намного быстрее полного.

См. также