Взлом битов: расширение битов

Я пытаюсь преобразовать uint16_t вход в uint32_t немного маски Один бит на входе переключает два бита в выходной битовой маске. Вот пример преобразования 4-битного ввода в 8-битную маску:

Input    Output
ABCDb -> AABB CCDDb

A,B,C,D are individual bits

Example outputs:

0000b -> 0000 0000b
0001b -> 0000 0011b
0010b -> 0000 1100b
0011b -> 0000 1111b
....
1100b -> 1111 0000b
1101b -> 1111 0011b
1110b -> 1111 1100b
1111b -> 1111 1111b

Есть ли хитрый способ добиться такого поведения?

9 ответов

Решение

Чередование битов с помощью двоичных магических чисел содержало ключ:

uint32_t expand_bits(uint16_t bits)
{
    uint32_t x = bits;

    x = (x | (x << 8)) & 0x00FF00FF;
    x = (x | (x << 4)) & 0x0F0F0F0F;
    x = (x | (x << 2)) & 0x33333333;
    x = (x | (x << 1)) & 0x55555555;

    return x | (x << 1);
}

Первые четыре шага последовательно чередуют исходные биты в группах по 8, 4, 2, 1 бит с нулевыми битами, что приводит к 00AB00CD после первого шага, 0A0B0C0D после второго шага и тд. Последний шаг затем дублирует каждый четный бит (содержащий исходный бит источника) в соседний нечетный бит, тем самым достигая желаемого расположения битов.

Возможны несколько вариантов. Последний шаг также может быть закодирован как x + (x << 1) или же 3 * x, | операторы в первых четырех шагах могут быть заменены ^ операторы. Маски также могут быть изменены, так как некоторые биты, естественно, равны нулю и не требуют очистки. На некоторых процессорах короткие маски могут быть включены в машинные инструкции как непосредственные, уменьшая трудозатраты на создание и / или загрузку констант маски. Также может быть выгодно увеличить параллелизм на уровне команд для процессоров, вышедших из строя, и оптимизировать их с помощью команд shift-add или integer-multiply-add. Один вариант кода, включающий различные из этих идей:

uint32_t expand_bits (uint16_t bits)
{
    uint32_t x = bits;

    x = (x ^ (x << 8)) & ~0x0000FF00;
    x = (x ^ (x << 4)) & ~0x00F000F0;
    x = x ^ (x << 2);
    x = ((x & 0x22222222) << 1) + (x & 0x11111111);
    x = (x << 1) + x;

    return x;
}

Самый простой способ сопоставить 4-битный вход с 8-битным выходом - это 16-элементная таблица. Так что это просто вопрос извлечения 4 бит за раз из uint16_tвыполнение поиска в таблице и вставка 8-битного значения в вывод.

uint32_t expandBits( uint16_t input )
{
    uint32_t table[16] = {
        0x00, 0x03, 0x0c, 0x0f,
        0x30, 0x33, 0x3c, 0x3f,
        0xc0, 0xc3, 0xcc, 0xcf,
        0xf0, 0xf3, 0xfc, 0xff
    };

    uint32_t output;
    output  = table[(input >> 12) & 0xf] << 24;
    output |= table[(input >>  8) & 0xf] << 16;
    output |= table[(input >>  4) & 0xf] <<  8;
    output |= table[ input        & 0xf];
    return output;
}

Это обеспечивает достойный компромисс между производительностью и удобочитаемостью. Он не обладает достаточной производительностью решения для поиска через cmaster, но, безусловно, более понятен, чем решение волшебной загадки thndrwrks. Как таковой, он предоставляет метод, который может быть применен к гораздо большему разнообразию проблем, то есть использовать небольшую справочную таблицу для решения более крупной проблемы.

Если вы хотите получить некоторую оценку относительной скорости, протестируйте код в вики-сообществе. Отрегулируйте по мере необходимости.

void f_cmp(uint32_t (*f1)(uint16_t x), uint32_t (*f2)(uint16_t x)) {
  uint16_t x = 0;
  do {
    uint32_t y1 = (*f1)(x);
    uint32_t y2 = (*f2)(x);
    if (y1 != y2) {
      printf("%4x %8lX %8lX\n", x, (unsigned long) y1, (unsigned long) y2);
    }
  } while (x++ != 0xFFFF);
}

void f_time(uint32_t (*f1)(uint16_t x)) {
  f_cmp(expand_bits, f1);
  clock_t t1 = clock();
  volatile uint32_t y1 = 0;
  unsigned n = 1000;
  for (unsigned i = 0; i < n; i++) {
    uint16_t x = 0;
    do {
      y1 += (*f1)(x);
    } while (x++ != 0xFFFF);
  }
  clock_t t2 = clock();
  printf("%6llu %6llu: %.6f %lX\n", (unsigned long long) t1,
          (unsigned long long) t2, 1.0 * (t2 - t1) / CLOCKS_PER_SEC / n,
          (unsigned long) y1);
  fflush(stdout);
}

int main(void) {
  f_time(expand_bits);
  f_time(expandBits);
  f_time(remask);
  f_time(javey);
  f_time(thndrwrks_expand);
  // now in the other order
  f_time(thndrwrks_expand);
  f_time(javey);
  f_time(remask);
  f_time(expandBits);
  f_time(expand_bits);
  return 0;
}

Результаты

     0    280: 0.000280 FE0C0000 // fast
   280    702: 0.000422 FE0C0000
   702   1872: 0.001170 FE0C0000
  1872   3026: 0.001154 FE0C0000
  3026   4399: 0.001373 FE0C0000 // slow

  4399   5740: 0.001341 FE0C0000
  5740   6879: 0.001139 FE0C0000
  6879   8034: 0.001155 FE0C0000
  8034   8470: 0.000436 FE0C0000
  8486   8751: 0.000265 FE0C0000

Вот рабочая реализация:

uint32_t remask(uint16_t x)
{
    uint32_t i;
    uint32_t result = 0;
    for (i=0;i<16;i++) {
        uint32_t mask = (uint32_t)x & (1U << i);
        result |= mask << (i);
        result |= mask << (i+1);
    }
    return result;
}

На каждой итерации цикла этот бит из uint16_t маскируется и сохраняется.

Затем этот бит сдвигается на свою битовую позицию и OR в результат, затем снова сдвигается на свою битовую позицию плюс 1 и ORed в результат.

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

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

//Implementation defined behavior ahead:
//Works correctly for both little and big endian machines,
//however, results will be wrong on a PDP11...
uint32_t getMask(uint16_t input) {
    assert(sizeof(uint16_t) == 2);
    assert(sizeof(uint32_t) == 4);
    static const uint16_t lookupTable[256] = { 0x0000, 0x0003, 0x000c, 0x000f, ... };

    unsigned char* inputBytes = (unsigned char*)&input;    //legal because we type-pun to char, but the order of the bytes is implementation defined
    char outputBytes[4];
    uint16_t* outputShorts = (uint16_t*)outputBytes;    //legal because we type-pun from char, but the order of the shorts is implementation defined
    outputShorts[0] = lookupTable[inputBytes[0]];
    outputShorts[1] = lookupTable[inputBytes[1]];
    uint32_t output;
    memcpy(&output, outputBytes, 4);    //can't type-pun directly from uint16 to uint32_t due to strict aliasing rules
    return output;
}

Приведенный выше код работает вокруг строгих правил наложения имен, приводя только к / из char, что является явным исключением из строгих правил наложения имен. Он также работает вокруг эффектов порядка байтов little/big-endian, создавая результат в том же порядке, в котором входные данные были разделены. Тем не менее, он по-прежнему предоставляет поведение, определяемое реализацией: машина с порядком байтов 1, 0, 3, 2или другие порядки среднего порядка молча будут давать неправильные результаты (на самом деле были такие процессоры, как PDP11...).

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

Простая петля. Может быть, недостаточно хакерский?

uint32_t thndrwrks_expand(uint16_t x) {
  uint32_t mask = 3;
  uint32_t y = 0;
  while (x) {
    if (x&1) y |= mask;
    x >>= 1;
    mask <<= 2;
  }
  return y;
}

Пробовал другой, который в два раза быстрее. Все еще 655/272 так медленно, как expand_bits(), Представляется самым быстрым итерационным решением с 16 циклами.

uint32_t thndrwrks_expand(uint16_t x) {
  uint32_t y = 0;
  for (uint16_t mask = 0x8000; mask; mask >>= 1) {
    y <<= 1;
    y |= x&mask;
  }
  y *= 3;
  return y;
}

Попробуйте это, где input16 является маской ввода uint16_t:

uint32_t input32 = (uint32_t) input16;
uint32_t result = 0;
uint32_t i;
for(i=0; i<16; i++)
{
    uint32_t bit_at_i = (input32 & (((uint32_t)1) << i)) >> i;
    result |= ((bit_at_i << (i*2)) | (bit_at_i << ((i*2)+1)));
}
// result is now the 32 bit expanded mask

Мое решение предназначено для работы на обычных компьютерах x86 и должно быть простым и универсальным. Я не писал это, чтобы конкурировать за самую быструю и / или самую короткую реализацию. Это просто еще один способ решить проблему, представленную ОП.

#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>

#define BITS_TO_EXPAND (4U)
#define SIZE_MAX (256U)

static bool expand_uint(unsigned int *toexpand,unsigned int *expanded);

int main(void)
{
    unsigned int in = 12;
    unsigned int out = 0;
    bool success;
    char buff[SIZE_MAX];

    success = expand_uint(&in,&out);
    if(false == success)
    {
        (void) puts("Error: expand_uint failed");
        return EXIT_FAILURE;
    }
    (void) snprintf(buff, (size_t) SIZE_MAX,"%u expanded is %u\n",in,out);
    (void) fputs(buff,stdout);
    return EXIT_SUCCESS;
}
/*
** It expands an unsigned int so that every bit in a nibble is copied twice
** in the resultant number. It returns true on success, false otherwise.
*/
static bool expand_uint(unsigned int *toexpand,unsigned int *expanded)
{
    unsigned int i;
    unsigned int shifts = 0;
    unsigned int mask;

    if(NULL == toexpand || NULL == expanded)
    {
        return false;
    }
    *expanded = 0;
    for(i = 0; i < BIT_TO_EXPAND; i++)
    {
        mask = (*toexpand >> i) & 1;
        *expanded |= (mask << shifts);
        ++shifts;
        *expanded |= (mask << shifts);
        ++shifts;
    }
    return true;
}

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

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

      unsigned long expand_bitmask(unsigned long mask, unsigned nbits_in, unsigned expand_by)
{
    unsigned long result, mask_out;
    int i, shift;

    assert(nbits_in * expand_by <= 8 * sizeof(unsigned));

    // mask input
    mask &= (unsigned long)(-1) >> ((8 * sizeof(unsigned long)) - nbits_in);

    result = 0;     // holds results
    mask_out = 0;   

    for (i = 0, shift = 0; i < nbits_in; ++i, shift += expand_by)
    {
        result   |= (mask << (shift - i));  // the shift differential places the bits
                                            // in the right place.
                                            // equivalent to mask << (i * (shift - 1))
        mask_out |= (1 << shift);           // this will mask the shited bits we want to keep
                                            // equivalent to 1 << (i * shift)
    }

    result &= mask_out;   // wipe out the garbage bits

    // multiply by a mask representing the number of wanted bits.
    result *= (unsigned long)(-1) >> ((8* sizeof(unsigned long)) - expand_by);
    return result;
}

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

Для исходного вопроса это дает:

      input_mask &= 0b11;
result = input_mask;
result |= (input_mask << 1);         // 1 * (2 bits out - 1)
result = (result & 0b0101) * 0b0011; // mask has bits set every 2 bits.
                                     // mutiplied by 2 set bits for expansion 
Другие вопросы по тегам