Как эффективно де-перемежать биты (обратный Мортон) - PullRequest
18 голосов
/ 05 февраля 2011

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

Для моего использования мне нужно взять 32-битное целое число и извлечь два 16-битных целых, где один - четные биты, а другой - нечетные биты, сдвинутые вправо на 1 бит, например,

input,  z: 11101101 01010111 11011011 01101110

output, x: 11100001 10110111 // odd bits shifted right by 1
        y: 10111111 11011010 // even bits

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

UPDATE

Перечитав раздел «Восхищение Хакера», посвященный идеальным перемешкам / растасовкам, я нашел несколько полезных примеров, которые я адаптировал следующим образом:

// morton1 - extract even bits

uint32_t morton1(uint32_t x)
{
    x = x & 0x55555555;
    x = (x | (x >> 1)) & 0x33333333;
    x = (x | (x >> 2)) & 0x0F0F0F0F;
    x = (x | (x >> 4)) & 0x00FF00FF;
    x = (x | (x >> 8)) & 0x0000FFFF;
    return x;
}

// morton2 - extract odd and even bits

void morton2(uint32_t *x, uint32_t *y, uint32_t z)
{
    *x = morton1(z);
    *y = morton1(z >> 1);
}

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

Ответы [ 5 ]

11 голосов
/ 07 февраля 2011

Если ваш процессор эффективно обрабатывает 64-битные числа, вы можете объединить операции ...

int64 w = (z &0xAAAAAAAA)<<31 | (z &0x55555555 )
w = (w | (w >> 1)) & 0x3333333333333333;
w = (w | (w >> 2)) & 0x0F0F0F0F0F0F0F0F; 
...
6 голосов
/ 31 мая 2015

Код для процессоров Intel Haswell и более поздних. Вы можете использовать набор инструкций BMI2, который содержит инструкции pext и pdep. Они могут (помимо прочего) использоваться для создания ваших функций.

#include <immintrin.h>
#include <stdint.h>

// on GCC, compile with option -mbmi2, requires Haswell or better.

uint64_t xy_to_morton (uint32_t x, uint32_t y)
{
    return _pdep_u32(x, 0x55555555) | _pdep_u32(y,0xaaaaaaaa);
}

uint64_t morton_to_xy (uint64_t m, uint32_t *x, uint32_t *y)
{
    *x = _pext_u64(m, 0x5555555555555555);
    *y = _pext_u64(m, 0xaaaaaaaaaaaaaaaa);
}
4 голосов
/ 06 февраля 2015

В случае, если кто-то использует коды Morton в 3d, ему нужно читать один бит каждые 3, и вот 64-битная функция, которую я использовал:

1 голос
/ 10 декабря 2016

Я не хотел ограничиваться целым числом фиксированного размера и составлением списков похожих команд с жестко закодированными константами, поэтому я разработал решение C ++ 11, которое использует шаблонное метапрограммирование для генерации функций и констант.Код ассемблера, сгенерированный с помощью -O3, выглядит настолько узким, насколько это возможно без использования BMI:

andl    $0x55555555, %eax
movl    %eax, %ecx
shrl    %ecx
orl     %eax, %ecx
andl    $0x33333333, %ecx
movl    %ecx, %eax
shrl    $2, %eax
orl     %ecx, %eax
andl    $0xF0F0F0F, %eax
movl    %eax, %ecx
shrl    $4, %ecx
orl     %eax, %ecx
movzbl  %cl, %esi
shrl    $8, %ecx
andl    $0xFF00, %ecx
orl     %ecx, %esi

TL; DR репозиторий и livedemo .


Реализация

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

  1. 0b0101010101010101 (поочередно 1 и 0)
  2. 0b0011001100110011 (поочередно 2x 1 и 0)
  3. 0b0000111100001111 (поочередно 4x 1 и 0)
  4. 0b0000000011111111 (поочередно 8x 1 и 0)

Если бы мы использовали D размеры, у нас был бы шаблон с D-1 нулями и 1 единицей.Таким образом, чтобы сгенерировать их, достаточно сгенерировать последовательные и применить побитовое или:

/// @brief Generates 0b1...1 with @tparam n ones
template <class T, unsigned n>
using n_ones = std::integral_constant<T, (~static_cast<T>(0) >> (sizeof(T) * 8 - n))>;

/// @brief Performs `@tparam input | (@tparam input << @tparam width` @tparam repeat times.
template <class T, T input, unsigned width, unsigned repeat>
struct lshift_add :
    public lshift_add<T, lshift_add<T, input, width, 1>::value, width, repeat - 1> {
};
/// @brief Specialization for 1 repetition, just does the shift-and-add operation.
template <class T, T input, unsigned width>
struct lshift_add<T, input, width, 1> : public std::integral_constant<T,
    (input & n_ones<T, width>::value) | (input << (width < sizeof(T) * 8 ? width : 0))> {
};

Теперь, когда мы можем генерировать константы во время компиляции для произвольных измерений со следующими параметрами:

template <class T, unsigned step, unsigned dimensions = 2u>
using mask = lshift_add<T, n_ones<T, 1 << step>::value, dimensions * (1 << step), sizeof(T) * 8 / (2 << step)>;

Используя один и тот же тип рекурсии, мы можем генерировать функции для каждого из этапов алгоритма x = (x | (x >> K)) & M:

template <class T, unsigned step, unsigned dimensions>
struct deinterleave {
    static T work(T input) {
        input = deinterleave<T, step - 1, dimensions>::work(input);
        return (input | (input >> ((dimensions - 1) * (1 << (step - 1))))) & mask<T, step, dimensions>::value;
    }
};
// Omitted specialization for step 0, where there is just a bitwise and

Осталось ответить на вопрос «сколько шагов нам нужно?».Это зависит также от количества измерений.В общем, k шагов вычисляют 2^k - 1 выходных битов;максимальное число значащих битов для каждого измерения задается z = sizeof(T) * 8 / dimensions, поэтому достаточно сделать 1 + log_2 z шагов.Проблема в том, что нам нужно это как constexpr, чтобы использовать его в качестве параметра шаблона.Лучший способ обойти это - определить log2 с помощью метапрограммирования:

template <unsigned arg>
struct log2 : public std::integral_constant<unsigned, log2<(arg >> 1)>::value + 1> {};
template <>
struct log2<1u> : public std::integral_constant<unsigned, 0u> {};

/// @brief Helper constexpr which returns the number of steps needed to fully interleave a type @tparam T.
template <class T, unsigned dimensions>
using num_steps = std::integral_constant<unsigned, log2<sizeof(T) * 8 / dimensions>::value + 1>;

И, наконец, мы можем выполнить один вызов:

/// @brief Helper function which combines @see deinterleave and @see num_steps into a single call.
template <class T, unsigned dimensions>
T deinterleave_first(T n) {
    return deinterleave<T, num_steps<T, dimensions>::value - 1, dimensions>::work(n);
}
1 голос
/ 07 февраля 2011

Если вам нужна скорость, чем вы можете использовать поиск по таблице для преобразования одного байта за один раз (таблица с двумя байтами быстрее, но слишком большая). Процедура выполняется в Delphi IDE, но ассемблер / алгоритм такой же.

const
  MortonTableLookup : array[byte] of byte = ($00, $01, $10, $11, $12, ... ;

procedure DeinterleaveBits(Input: cardinal);
//In: eax
//Out: dx = EvenBits; ax = OddBits;
asm
  movzx   ecx, al                                     //Use 0th byte
  mov     dl, byte ptr[MortonTableLookup + ecx]
//
  shr     eax, 8
  movzx   ecx, ah                                     //Use 2th byte
  mov     dh, byte ptr[MortonTableLookup + ecx]
//
  shl     edx, 16
  movzx   ecx, al                                     //Use 1th byte
  mov     dl, byte ptr[MortonTableLookup + ecx]
//
  shr     eax, 8
  movzx   ecx, ah                                     //Use 3th byte
  mov     dh, byte ptr[MortonTableLookup + ecx]
//
  mov     ecx, edx  
  and     ecx, $F0F0F0F0
  mov     eax, ecx
  rol     eax, 12
  or      eax, ecx

  rol     edx, 4
  and     edx, $F0F0F0F0
  mov     ecx, edx
  rol     ecx, 12
  or      edx, ecx
end;
...