Рядом постоянное время вращается, что не нарушает стандарты

У меня чертовски время, пытаясь придумать постоянное вращение во времени, которое не нарушает стандарты C/С++.

Проблема - это край/угловые случаи, в которых операции вызывается в алгоритмах, и эти алгоритмы не могут быть изменены. Например, из Crypto ++ и выполняется тестовый жгут в GCC ubsan (т.е. g++ fsanitize=undefined):

$ ./cryptest.exe v | grep runtime
misc.h:637:22: runtime error: shift exponent 32 is too large for 32-bit type 'unsigned int'
misc.h:643:22: runtime error: shift exponent 32 is too large for 32-bit type 'unsigned int'
misc.h:625:22: runtime error: shift exponent 32 is too large for 32-bit type 'unsigned int'
misc.h:637:22: runtime error: shift exponent 32 is too large for 32-bit type 'unsigned int'
misc.h:643:22: runtime error: shift exponent 32 is too large for 32-bit type 'unsigned int'
misc.h:637:22: runtime error: shift exponent 32 is too large for 32-bit type 'unsigned int'

И код в misc.h:637:

template <class T> inline T rotlMod(T x, unsigned int y)
{
    y %= sizeof(T)*8;
    return T((x<<y) | (x>>(sizeof(T)*8-y)));
}

Intel ICC был особенно беспощаден, и он удалил весь вызов функции с помощью y %= sizeof(T)*8. Мы исправили это несколько лет назад, но оставили другие ошибки на месте из-за отсутствия постоянного решения времени.

Там осталась одна болевая точка. Когда y = 0, я получаю условие где 32 - y = 32, и он устанавливает поведение undefined. Если я добавлю проверку на if(y == 0) ..., тогда код не сможет выполнить требование времени.

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

Как я могу выполнить поворот почти в постоянное время с минимальным количеством инструкций?

РЕДАКТИРОВАТЬ: почти постоянным временем я имею в виду избегать ветки, так что всегда выполняются одни и те же инструкции. Я не беспокоюсь о таймингах микрокода процессора. Хотя предсказание ветвления может быть велико на x86/x64, оно может не работать также на других платформах, например вложенных.


Ни один из этих трюков не потребуется, если GCC или Clang предоставил возможность выполнять вращение в почти постоянное время. Я даже согласился на "выполнить поворот", так как у них даже этого нет.

Ответы

Ответ 1

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

Я нашел сообщение в блоге об этой проблеме, и похоже, что это окончательно решена проблема (с новыми версиями компилятора).

Джон Реджер из Университета штата Юта рекомендует версию "c" своих попыток сделать функцию вращения. Я заменил его assert поразрядным AND и обнаружил, что он все еще компилируется в один rotate insn.

typedef uint32_t rotwidth_t;  // parameterize for comparing compiler output with various sizes

rotwidth_t rotl (rotwidth_t x, unsigned int n)
{
  const unsigned int mask = (CHAR_BIT*sizeof(x)-1);  // e.g. 31

  assert ( (n<=mask)  &&"rotate by type width or more");
  n &= mask;  // avoid undef behaviour with NDEBUG.  0 overhead for most types / compilers
  return (x<<n) | (x>>( (-n)&mask ));
}

rotwidth_t rot_const(rotwidth_t x)
{
  return rotl(x, 7);
}

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

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

Пабигот самостоятельно придумал ту же идею, что и я, и разместил ее в gibhub. Его версия имеет проверку С++ static_assert, чтобы сделать ее ошибкой времени компиляции, чтобы использовать счетчик вращения вне диапазона для типа.

I проверил мою версию с gcc.godbolt.org, с определенным NDEBUG, для переменных и компиляции time-const вращает count:

  • gcc: оптимальный код с gcc >= 4.9.0, неветвящийся neg + сдвиг + или с более ранним.
    (время подсчета compile-time: gcc 4.4.7 в порядке)
  • clang: оптимальный код с clang >= 3.5.0, не ветвящийся neg + сдвиг + или с более ранним.
    (compile-time const rotate count: clang 3.0 отлично)
  • icc 13: оптимальный код.
    (время подсчета compile-time с -march = native: генерирует более медленный shld $7, %edi, %edi. Тонкий без -march=native)

Даже новые версии компилятора могут обрабатывать обычно заданный код из википедии (включенной в образец godbolt) без создания ветки или cmov. Преимущество Джона Регера состоит в том, чтобы избежать поведения undefined, когда число оборотов равно 0.

Есть некоторые предостережения с 8 и 16 битами, но компиляторы кажутся точными с 32 или 64, когда n - uint32_t. См. Комментарии в коде ссылка godbolt для некоторых заметок из моего тестирования различной ширины uint*_t. Надеемся, что эта идиома будет лучше распознана всеми компиляторами для большего количества комбинаций ширины типов в будущем. Иногда gcc бесполезно выделяет AND insn на счетчик вращения, хотя x86 ISA определяет вращение insns с таким точным AND как первый шаг.

"оптимальный" означает такую ​​же эффективную, как:

# gcc 4.9.2 rotl(unsigned int, unsigned int):
    movl    %edi, %eax
    movl    %esi, %ecx
    roll    %cl, %eax
    ret
# rot_const(unsigned int):
    movl    %edi, %eax
    roll    $7, %eax
    ret

Когда встраивается, компилятор должен иметь возможность упорядочивать значения в правильных регистрах в первую очередь, что приводит к простому вращению.

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

Кстати, я модифицировал оригинал Джона Реджера, чтобы использовать CHAR_BIT * sizeof (x), а gcc/clang/icc испускает оптимальный код для uint64_t. Тем не менее, я заметил, что изменение x на uint64_t, в то время как возвращаемый тип функции все еще uint32_t делает gcc скомпилировать его для сдвигов/или. Поэтому будьте осторожны, чтобы привести результат к 32 бит в отдельной точке последовательности, если вы хотите, чтобы низкий 32b 64b вращался. т.е. присвойте результат 64-битной переменной, затем отпустите ее/верните. icc все еще генерирует rotate insn, но gcc и clang этого не делают, для

// generates slow code: cast separately.
uint32_t r = (uint32_t)( (x<<n) | (x>>( -n&(CHAR_BIT*sizeof(x)-1) )) );

Если кто-то может проверить это с помощью MSVC, было бы полезно узнать, что там происходит.

Ответ 2

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

template <class T> inline T rotlMod(T x, unsigned int y)
{
    y %= sizeof(T)*8;
    return T((x<<y) | (x>>((sizeof(T)*8-y) % (sizeof(T)*8))));
}

Ответ 3

Написание выражения как T((x<<y) | ((x>>(sizeof(T)*CHAR_BITS-y-1)>>1)) должно давать определенное поведение для всех значений y ниже размера бита, предполагая, что T является неподписанным типом без заполнения. Если у компилятора нет хорошего оптимизатора, полученный код может быть не таким хорошим, как то, что было бы создано вашим оригинальным выражением. Однако, чтобы смириться с неуклюжим жестким для чтения кодом, который приведет к более медленному выполнению многих компиляторов, является частью цены прогресса, однако, поскольку гиперсовременный компилятор, которому присваивается

if (y) do_something();
return T((x<<y) | (x>>(sizeof(T)*8-y)));

может улучшить "эффективность" кода, сделав вызов do_something безусловным.

PS: Интересно, существуют ли какие-либо платформы реального мира, где изменение определения shift-right так, что x >> y, когда y точно равно размеру бит x, потребовалось бы либо 0 или x, но может сделать выбор произвольным (неуказанным) способом, потребует, чтобы платформа генерировала дополнительный код или исключала бы действительно полезные оптимизации в ненастроенных сценариях?

Ответ 4

Альтернативой дополнительному модулю является умножение на 0 или 1 (благодаря !!):

template <class T> T rotlMod(T x, unsigned int y)
{
    y %= sizeof(T) * 8;
    return T((x << y) | (x >> ((!!y) * (sizeof(T) * 8 - y)));
}