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

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

p    q    p → q
T    T      T
T    F      F
F    T      T
F    F      T

это, согласно Википедии, называется "логическая импликация"

Я давно пытаюсь понять, как сделать это с побитовыми операциями в c/" class="blnk">C без использования условных обозначений. Может кто-то имеет мысли по этому поводу.

спасибо

5 ответов


FYI, с gcc-4.3.3:

int foo(int a, int b) { return !a || b; }
int bar(int a, int b) { return ~a | b; }

дает (от objdump-d):

0000000000000000 <foo>:
   0:   85 ff                   test   %edi,%edi
   2:   0f 94 c2                sete   %dl
   5:   85 f6                   test   %esi,%esi
   7:   0f 95 c0                setne  %al
   a:   09 d0                   or     %edx,%eax
   c:   83 e0 01                and    x1,%eax
   f:   c3                      retq   

0000000000000010 <bar>:
  10:   f7 d7                   not    %edi
  12:   09 fe                   or     %edi,%esi
  14:   89 f0                   mov    %esi,%eax
  16:   c3                      retq   

итак, нет ветвей, но в два раза больше инструкций.

а еще лучше, с _Bool (спасибо @litb):

_Bool baz(_Bool a, _Bool b) { return !a || b; }
0000000000000020 <baz>:
  20:   40 84 ff                test   %dil,%dil
  23:   b8 01 00 00 00          mov    x1,%eax
  28:   0f 45 c6                cmovne %esi,%eax
  2b:   c3                      retq   

Итак, используя _Bool вместо int - это хорошая идея.

поскольку я обновляю сегодня, я подтвердил, что gcc 8.2.0 производит аналогичные, хотя и не идентичные, результаты для _Bool:

0000000000000020 <baz>:
  20:   83 f7 01                xor    x1,%edi
  23:   89 f8                   mov    %edi,%eax
  25:   09 f0                   or     %esi,%eax
  27:   c3                      retq   

!p || q

достаточно быстро. серьезно, не волнуйся об этом.


~p | q

для наглядности:

perl -e'printf "%x\n", (~0x1100 | 0x1010) & 0x1111'
1011

в плотном коде это должно быть быстрее, чем "!p / / q", потому что у последнего есть ветвь, которая может вызвать сбой в ЦП из-за ошибки прогнозирования ветви. Побитовая версия детерминирована и, в качестве бонуса, может выполнять в 32 раза больше работы в 32-разрядном целочисленном, чем булева версия!


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


другое решение для булевых c (немного грязное, но работает):

((unsigned int)(p) <= (unsigned int)(q))

он работает с стандарта C,0 - значение false, а любое другое значение true (1 возвращается для true логическими операторами,int type).

"грязность" заключается в том, что я использую booleans (p и q) как целые числа, что противоречит некоторым сильным политикам ввода (например, MISRA), ну, это вопрос оптимизации. Вы всегда можете #define это как макрос, чтобы скрыть грязные вещи.

для правильного boolean p и q (либо 0 или 1 двоичные представления) он работает. В противном случае T->T может не произвести T если p и q имеют произвольные ненулевые значения для представления true.

Если вам нужно сохранить результат только, так как Pentium II, есть cmovcc (условного перехода) инструкция (как показано в ответ по Derobert). Для булевых, однако, даже 386 вариант без ветвей,setcc инструкция, которая производит 0 или 1 в месте байта результата (регистр или память байта). Вы также можете увидеть, что в ответ Derobert, и это решение также компилируется в результате с участием setcc (setbe: установить, если ниже или равна).

Derobert и Крис Долан ~p | q вариант должен быть самым быстрым для обработки больших объемов данных, так как он может обрабатывать импликацию на всех битах p и q индивидуально.

обратите внимание, что даже !p || q решение компилируется в разветвленный код на x86: он использует setcc инструкция. Это лучшее решение, если p или q может содержать произвольные ненулевые значения, представляющие правда. Если вы используете _Bool type, он будет генерировать очень мало инструкций.

при компиляции для x86 я получил следующие цифры:

__attribute__((fastcall)) int imp1(int a, int b)
{
 return ((unsigned int)(a) <= (unsigned int)(b));
}

__attribute__((fastcall)) int imp2(int a, int b)
{
 return (!a || b);
}

__attribute__((fastcall)) _Bool imp3(_Bool a, _Bool b)
{
 return (!a || b);
}

__attribute__((fastcall)) int imp4(int a, int b)
{
 return (~a | b);
}

сборка результат:

00000000 <imp1>:
   0:   31 c0                   xor    %eax,%eax
   2:   39 d1                   cmp    %edx,%ecx
   4:   0f 96 c0                setbe  %al
   7:   c3                      ret    

00000010 <imp2>:
  10:   85 d2                   test   %edx,%edx
  12:   0f 95 c0                setne  %al
  15:   85 c9                   test   %ecx,%ecx
  17:   0f 94 c2                sete   %dl
  1a:   09 d0                   or     %edx,%eax
  1c:   0f b6 c0                movzbl %al,%eax
  1f:   c3                      ret    

00000020 <imp3>:
  20:   89 c8                   mov    %ecx,%eax
  22:   83 f0 01                xor    x1,%eax
  25:   09 d0                   or     %edx,%eax
  27:   c3                      ret    

00000030 <imp4>:
  30:   89 d0                   mov    %edx,%eax
  32:   f7 d1                   not    %ecx
  34:   09 c8                   or     %ecx,%eax
  36:   c3                      ret    

при использовании _Bool тип, компилятор явно использует, что он имеет только два возможных значения (0 для false и 1 для true), производя очень похожий результат на ~a | b решение (единственная разница в том, что последний выполняет дополнение на всех битах, а не только самый низкий бит).

компиляция для 64 бит дает примерно те же результаты.

в любом случае, ясно, что метод на самом деле не имеет значения с точки зрения избежания создания условий.