Подтвердить что ты не робот

Нерентабельные условности на целые числа - быстрая, но можно ли их сделать быстрее?

Я экспериментировал со следующим и заметил, что неопределенное "if", определенное здесь (теперь с заменой &-!! *!!), может ускорить некоторый код узкого места на столько же (почти) 2x на 64- бит Intel с clang:

// Produces x if f is true, else 0 if f is false.
#define  BRANCHLESS_IF(f,x)          ((x) & -((typeof(x))!!(f)))

// Produces x if f is true, else y if f is false.
#define  BRANCHLESS_IF_ELSE(f,x,y)  (((x) & -((typeof(x))!!(f))) | \
                                     ((y) & -((typeof(y)) !(f))))

Обратите внимание, что f должно быть достаточно простым выражением без побочных эффектов, чтобы компилятор мог наилучшим образом оптимизировать.

Производительность сильно зависит от процессора и компилятора. Безветровое "если производительность отличная с clang; Я еще не нашел случаев, когда разветвляемый" if/else" быстрее.

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

Пример использования unsignedless if/else

Они вычисляют 64-битный минимум и максимум.

inline uint64_t uint64_min(uint64_t a, uint64_t b)
{
  return BRANCHLESS_IF_ELSE((a <= b), a, b);
}

inline uint64_t uint64_max(uint64_t a, uint64_t b)
{
  return BRANCHLESS_IF_ELSE((a >= b), a, b);
}

Пример использования веткистой, если

Это 64-битное модульное дополнение - оно вычисляет (a + b) % n. Версия ветвления (не показана) ужасно страдает от ошибок предсказания ветвлений, но безветровая версия очень быстрая (по крайней мере, с clang).

inline uint64_t uint64_add_mod(uint64_t a, uint64_t b, uint64_t n)
{
  assert(n > 1); assert(a < n); assert(b < n);

  uint64_t c = a + b - BRANCHLESS_IF((a >= n - b), n);

  assert(c < n);
  return c;
}

Обновление: полный конкретный рабочий пример бездиапазонного, если

Ниже приведена полная работающая программа C11, которая демонстрирует разницу в скорости между ветвями и безветровыми версиями простого условного выражения if, если вы хотите попробовать его в своей системе. Программа вычисляет модульное возведение в степень, то есть (a ** b) % n, для чрезвычайно больших значений.

Чтобы скомпилировать, используйте следующую команду в командной строке:

  • -O3 (или любой высокий уровень оптимизации, который вы предпочитаете)
  • -DNDEBUG (чтобы отключить утверждения, для скорости)
  • Либо -DBRANCHLESS=0, либо -DBRANCHLESS=1, чтобы указать ветвление или ветвление, соответственно

В моей системе, вот что происходит:

$ cc -DBRANCHLESS=0 -DNDEBUG -O3 -o powmod powmod.c && ./powmod
BRANCHLESS = 0
CPU time:  21.83 seconds
foo = 10585369126512366091

$ cc -DBRANCHLESS=1 -DNDEBUG -O3 -o powmod powmod.c && ./powmod
BRANCHLESS = 1
CPU time:  11.76 seconds
foo = 10585369126512366091

$ cc --version
Apple LLVM version 6.0 (clang-600.0.57) (based on LLVM 3.5svn)
Target: x86_64-apple-darwin14.1.0
Thread model: posix

Таким образом, версия без ветвей почти в два раза быстрее, чем версия ветвления в моей системе (3,4 ГГц. Intel Core i7).

// SPEED TEST OF MODULAR MULTIPLICATION WITH BRANCHLESS CONDITIONALS

#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
#include <time.h>
#include <assert.h>

typedef  uint64_t  uint64;

//------------------------------------------------------------------------------
#if BRANCHLESS
  // Actually branchless.
  #define  BRANCHLESS_IF(f,x)          ((x) & -((typeof(x))!!(f)))
  #define  BRANCHLESS_IF_ELSE(f,x,y)  (((x) & -((typeof(x))!!(f))) | \
                                       ((y) & -((typeof(y)) !(f))))
#else
  // Not actually branchless, but used for comparison.
  #define  BRANCHLESS_IF(f,x)          ((f)? (x) : 0)
  #define  BRANCHLESS_IF_ELSE(f,x,y)   ((f)? (x) : (y))
#endif

//------------------------------------------------------------------------------
// 64-bit modular multiplication.  Computes (a * b) % n without division.

static uint64 uint64_mul_mod(uint64 a, uint64 b, const uint64 n)
{
  assert(n > 1); assert(a < n); assert(b < n);

  if (a < b) { uint64 t = a; a = b; b = t; }  // Ensure that b <= a.

  uint64 c = 0;
  for (; b != 0; b /= 2)
  {
    // This computes c = (c + a) % n if (b & 1).
    c += BRANCHLESS_IF((b & 1), a - BRANCHLESS_IF((c >= n - a), n));
    assert(c < n);

    // This computes a = (a + a) % n.
    a += a - BRANCHLESS_IF((a >= n - a), n);
    assert(a < n);
  }

  assert(c < n);
  return c;
}

//------------------------------------------------------------------------------
// 64-bit modular exponentiation.  Computes (a ** b) % n using modular
// multiplication.

static
uint64 uint64_pow_mod(uint64 a, uint64 b, const uint64 n)
{
  assert(n > 1); assert(a < n);

  uint64 c = 1;

  for (; b > 0; b /= 2)
  {
    if (b & 1)
      c = uint64_mul_mod(c, a, n);

    a = uint64_mul_mod(a, a, n);
  }

  assert(c < n);
  return c;
}

//------------------------------------------------------------------------------
int main(const int argc, const char *const argv[const])
{
  printf("BRANCHLESS = %d\n", BRANCHLESS);

  clock_t clock_start = clock();

  #define SHOW_RESULTS 0

  uint64 foo = 0;  // Used in forcing compiler not to throw away results.

  uint64 n = 3, a = 1, b = 1;
  const uint64 iterations = 1000000;
  for (uint64 iteration = 0; iteration < iterations; iteration++)
  {
    uint64 c = uint64_pow_mod(a%n, b, n);

    if (SHOW_RESULTS)
    {
      printf("(%"PRIu64" ** %"PRIu64") %% %"PRIu64" = %"PRIu64"\n",
             a%n, b, n, c);
    }
    else
    {
      foo ^= c;
    }

    n = n * 3 + 1;
    a = a * 5 + 3;
    b = b * 7 + 5;
  }

  clock_t clock_end = clock();
  double elapsed = (double)(clock_end - clock_start) / CLOCKS_PER_SEC;
  printf("CPU time:  %.2f seconds\n", elapsed);

  printf("foo = %"PRIu64"\n", foo);

  return 0;
}

Второе обновление: производительность Intel против ARM

  • Тестирование по 32-битным ARM-целям (iPhone 3GS/4S, iPad 1/2/3/4, как составлено Xcode 6.1 с clang) показывает, что безветровое "если" здесь на самом деле примерно в 2-3 раза медленнее, чем trernary ?: для модульного кода возведения в степень в этих случаях. Таким образом, кажется, что эти ветвящиеся макросы не являются хорошей идеей, если необходима максимальная скорость, хотя они могут быть полезны в редких случаях, когда необходима постоянная скорость.
  • В 64-битных целях ARM (iPhone 6+, IPad 5) нераспространяемая "if" работает с той же скоростью, что и тройная ?:, снова скомпилированная Xcode 6.1 с помощью clang.
  • Для Intel и ARM (как скомпилировано clang) нераспределенная "if/else" была примерно в два раза медленнее, чем тройная ?: для вычисления min/max.
4b9b3361

Ответ 1

Уверенный, что это портативно, оператор ! гарантированно дает в результате 0 или 1. Затем это повышается до любого типа, который требуется другому операнду.

Как наблюдали другие, ваша версия if-else имеет недостаток, чтобы оценивать дважды, но вы уже знаете это, и если нет побочного эффекта, вы в порядке.

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

Изменить: я тестировал это с двумя компиляторами (gcc и clang) и двумя значениями для конфигурации.

На самом деле, если вы не забудете установить -DNDEBUG=1, версия 0 с ?: намного лучше подходит для gcc и делает то, что я ожидал от нее. Он в основном использует условные ходы, чтобы иметь замкнутый контур. В этом случае clang не находит такого рода оптимизацию и выполняет некоторые условные скачки.

Для версии с арифметикой производительность для gcc ухудшается. На самом деле видеть то, что он делает, это не удивительно. Он действительно использует инструкции imul, и они медленны. clang здесь лучше. "Арифметика" фактически оптимизировала умножение и заменила их условными ходами.

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