Какая ваша любимая битовая техника? [закрыто]


14

Несколько дней назад член StackExchange Анто спросил о допустимых применениях для побитовых операторов. Я заявил, что сдвиг быстрее, чем умножение и деление целых чисел на степени двух. Члену StackExchange Daemin ответили, заявив, что смещение вправо представляет проблемы с отрицательными числами.

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

Несколько минут размышлений привели к решению первого порядка. Решение условно преобразует отрицательные значения в положительные значения перед сдвигом. Значение условно преобразуется обратно в его отрицательную форму после выполнения операции сдвига.

int a = -5;
int n = 1;

int negative = q < 0; 

a = negative ? -a : a; 
a >>= n; 
a = negative ? -a : a; 

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

С учетом вышесказанного я проснулся в субботу с ответом на проблему условного назначения. Проблема округления, с которой мы сталкиваемся при выполнении операции арифметического сдвига, возникает только при работе с представлением дополнения до двух. Это не происходит с представлением своего дополнения. Решение проблемы включает в себя преобразование значения дополнения до двух в значение дополнения до выполнения операции сдвига. Затем мы должны преобразовать значение дополнения до значения дополнения до двух. Удивительно, но мы можем выполнить этот набор операций без условного преобразования отрицательных значений перед выполнением операции сдвига.

int a = -5;
int n = 1;

register int sign = (a >> INT_SIZE_MINUS_1) & 1

a = (a - sign) >> n + sign;   

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

С учетом вышесказанного, можете ли вы вспомнить другие хитроумные хаки, подобные приведенному выше, которые превратили его в ваш пакет фокусов? Какой твой любимый битый хак? Я всегда ищу новые ориентированные на производительность хиты.


3
Этот вопрос и имя вашей учетной записи - мир снова обретает смысл ...
JK

+1 Интересный вопрос в качестве продолжения моего и в остальном;)
Anto

Я также сделал несколько быстрых вычислений четности один раз. Четность - это боль, потому что традиционно она включает циклы и подсчет, если бит установлен, и все это требует большого количества прыжков. Четность может быть рассчитана с использованием Shift и XOR, тогда куча сделанных один за другим избегает циклов и скачков.
quick_now

2
Вы знаете, что есть целая книга об этих методах? - Хакеры Delight amazon.com/Hackers-Delight-Henry-S-Warren/dp/0201914654
Ники

Да, есть сайт, посвященный битовым операциям. Я забыл URL, но Google скоро его включит.
quick_now

Ответы:


23

Мне нравится взлом Госпера (HAKMEM # 175), очень хитрый способ получения числа и получения следующего числа с таким же количеством битов. Это полезно, например, для генерации комбинаций kпредметов из n:

int set = (1 << k) - 1;
int limit = (1 << n);
while (set < limit) {
    doStuff(set);

    // Gosper's hack:
    int c = set & -set;
    int r = set + c;
    set = (((r^set) >>> 2) / c) | r;
}

7
+1. Но теперь у меня будут кошмары о том, чтобы найти этот во время сеанса отладки без комментариев.
nikie

@nikie, muahahahaha! (Я склонен использовать это для таких вещей, как проблемы Project Euler - моя повседневная работа не требует много комбинаторики).
Питер Тейлор

7

Быстрый обратный квадратный метод корня использует большинство методов причудливого на битовом уровне для вычисления обратного квадратного корня , который я когда - либо видел:

float Q_rsqrt( float number )
{
    long i;
    float x2, y;
    const float threehalfs = 1.5F;

    x2 = number * 0.5F;
    y  = number;
    i  = * ( long * ) &y;                       // evil floating point bit level hacking [sic]
    i  = 0x5f3759df - ( i >> 1 );               // what the fuck? [sic]
    y  = * ( float * ) &i;
    y  = y * ( threehalfs - ( x2 * y * y ) );   // 1st iteration
    //    y  = y * ( threehalfs - ( x2 * y * y ) );   // 2nd iteration, this can be removed

    return y;
}

Быстрый sqrt также удивителен. Кармак выглядит одним из величайших программистов.
BenjaminB

В Википедии есть даже более старые источники, например, beyond3d.com/content/articles/15
MSalters,

0

Деление на 3 - без обращения к вызову библиотеки во время выполнения.

Оказывается, что деление на 3 (благодаря подсказке о переполнении стека) может быть аппроксимировано как:

Х / 3 = [(х / 4) + (х / 12)]

И X / 12 есть (x / 4) / 3. Здесь внезапно появляется элемент рекурсии.

Также оказывается, что если вы ограничите диапазон чисел, в которых вы играете, вы можете ограничить количество необходимых итераций.

И, таким образом, для целых чисел без знака <2000 следующее является быстрым и простым алгоритмом / 3. (Для больших чисел просто добавьте больше шагов). Компиляторы оптимизируют эту черту, чтобы она была быстрой и маленькой:

статический беззнаковый короткий FastDivide3 (const беззнаковый короткий аргумент)
{
  неподписанная короткая RunningSum;
  беззнаковый короткий FractionalTwelth;

  RunningSum = arg >> 2;

  FractionalTwelth = RunningSum >> 2;
  RunningSum + = FractionalTwelth;

  FractionalTwelth >> = 2;
  RunningSum + = FractionalTwelth;

  FractionalTwelth >> = 2;
  RunningSum + = FractionalTwelth;

  FractionalTwelth >> = 2;
  RunningSum + = FractionalTwelth;

  // Больше повторов вышеупомянутых 2 строк для большей точности

  вернуть RunningSum;
}

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

1
Да, конечно, но маленькие микро без аппаратного множителя на самом деле очень распространены. А если вы работаете на встраиваемой земле и хотите сэкономить 0,10 доллара на каждом из миллиона проданных продуктов, то вам лучше знать некоторые грязные приемы. Эти сэкономленные деньги = дополнительная прибыль, которая делает вашего босса очень счастливым.
quick_now

Ну, грязно ... это просто умножение на .0101010101(примерно 1/3). Совет для профессионалов: вы также можете умножить на .000100010001и 101(который занимает всего 3 битных сдвига, но имеет лучшее приближение.010101010101
MSalters

Как я могу сделать это только с целыми числами и без плавающей запятой?
quick_now

1
Поразрядно, x * 101 = x + x << 2. Аналогично, x * 0.000100010001 равен x >> 4 + x >> 8 + x >> 12.
MSalters

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.