Как вставить нули между битами в растровое изображение?

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

Для 13-битного растрового изображения создайте 26-битную растровую карту, содержащую исходные биты, расположенные на ровных позициях.

Чтобы проиллюстрировать:

0000000000000000000abcdefghijklm (input, 32 bits)
0000000a0b0c0d0e0f0g0h0i0j0k0l0m (output, 32 bits)

В настоящее время я реализовал его в C:

if (input & (1 << 12))
    output |= 1 << 24;
if (input & (1 << 11))
    output |= 1 << 22;
if (input & (1 << 10))
    output |= 1 << 20;
...

Мой компилятор (MS Visual Studio) превратил это в следующее:

test        eax,1000h
jne         0064F5EC
or          edx,1000000h
... (repeated 13 times with minor differences in constants)

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

  • Могу ли я использовать некоторые инструкции MMX/SSE для обработки всех бит одновременно?
  • Может быть, я могу использовать умножение? (умножить на 0x11111111 или какую-либо другую магическую константу)
  • Было бы лучше использовать инструкцию условного набора (SETcc) вместо инструкции условного перехода? Если да, как я могу заставить компилятор создать такой код для меня?
  • Любая другая идея, как сделать это быстрее?
  • Любая идея, как сделать обратное преобразование растрового изображения (я должен реализовать его тоже, бит его менее критическим)?

Ответ 1

Сделайте это с помощью таблицы поиска. 2 ^ 13 звучат как много записей, но они легко впишутся в кеш процессора.

О, и если в других 19 битх есть мусор, сначала нужно замаскировать их.

Ответ 2

Существует разумный способ сделать это, что может быть полезно здесь. Это на самом деле решает немного более общую проблему с перетасовкой бит. У вашей проблемы есть ввод:

+---------------+---------------+---------------+---------------+
|0 0 0 0 0 0 0 0|0 0 0 0 0 0 0 0|0 0 0 a b c d e|f g h i j k l m|
+---------------+---------------+---------------+---------------+

.... но рассмотрим все биты:

+---------------+---------------+---------------+---------------+
|A B C D E F G H|I J K L M N O P|Q R S a b c d e|f g h i j k l m|
+---------------+---------------+---------------+---------------+

и попытайтесь их чередовать следующим образом:

+---------------+---------------+---------------+---------------+
|A Q B R C S D a|E b F c G d H e|I f J g K h L i|M j N k O l P m|
+---------------+---------------+---------------+---------------+

Для первого шага рассмотрим среднюю половину ввода:

bit 31        24              16               8               0
 v             v               v               v               v
+---------------+---------------+---------------+---------------+
|               |I J K L M N O P|Q R S a b c d e|               |
+---------------+---------------+---------------+---------------+

Создайте 8-битное значение: { I^Q, J^R, K^S, L^a, M^b, N^c, O^d, P^e}.

Если мы исключительны - это 8-битное значение с битами [15: 8], а также исключающее ИЛИ то же 8-битное значение с битами [23:16], мы поменяем средние два байта: для Например, бит 23 (изначально I) станет I ^ (I^Q) = Q и бит 15 (изначально Q) станет Q ^ (I^Q) = I.

Для этого: tmp = (input ^ (input >> 8)) & 0x0000ff00;:

+---------------+---------------+---------------+---------------+
|A B C D E F G H|I J K L M N O P|Q R S a b c d e|f g h i j k l m| input
+---------------+---------------+---------------+---------------+
                            exclusive-OR with:
+---------------+---------------+---------------+---------------+
|0 0 0 0 0 0 0 0|A B C D E F G H|I J K L M N O P|Q R S a b c d e| input >> 8
+---------------+---------------+---------------+---------------+

                             -->|want these bits|<--

 mask (bitwise AND) with 0x0000ff00:
+---------------+---------------+---------------+---------------+
|0 0 0 0 0 0 0 0|0 0 0 0 0 0 0 0|1 1 1 1 1 1 1 1|0 0 0 0 0 0 0 0| 0x0000ff00
+---------------+---------------+---------------+---------------+

Теперь 8-битное значение, которое нам нужно, находится в битах [15: 8], со всеми остальными битами 0. Теперь мы можем выполнить обмен с

input ^= (tmp ^ (tmp << 8));

в результате:

+---------------+---------------+---------------+---------------+
|A B C D E F G H|Q R S a b c d e|I J K L M N O P|f g h i j k l m| input
+---------------+---------------+---------------+---------------+

Для следующего шага разделите и покорите... выполните аналогичный обмен среднего бит левой половины половины:

+---------------+---------------+---------------+---------------+
|A B C D E F G H|Q R S a b c d e|               |               |
+---------------+---------------+---------------+---------------+
             becomes
+---------------+---------------+---------------+---------------+
|A B C D Q R S a|E F G H b c d e|               |               |
+---------------+---------------+---------------+---------------+

... и правая половина:

+---------------+---------------+---------------+---------------+
|               |               |I J K L M N O P|f g h i j k l m|
+---------------+---------------+---------------+---------------+
                                             becomes
+---------------+---------------+---------------+---------------+
|               |               |I J K L f g h i|M N O P j k l m|
+---------------+---------------+---------------+---------------+

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

tmp = (input ^ (input >> 4)) & 0x00f000f0;

строит две пары из 4 бит, которые мы будем использовать для swap, а затем

input ^= (tmp ^ (tmp << 4));

фактически выполняет обмен.

Мы можем продолжать применять тот же принцип до тех пор, пока своп не будет завершен. Биты, участвующие в обмене в каждой точке, помечены знаком #:

+---------------+---------------+---------------+---------------+
|A B C D E F G H|I J K L M N O P|Q R S a b c d e|f g h i j k l m|
+---------------+---------------+---------------+---------------+
                 ###############/###############
+---------------+---------------+---------------+---------------+
|A B C D E F G H|Q R S a b c d e|I J K L M N O P|f g h i j k l m|
+---------------+---------------+---------------+---------------+
         #######/#######                 #######/#######
+---------------+---------------+---------------+---------------+
|A B C D Q R S a|E F G H b c d e|I J K L f g h i|M N O P j k l m|
+---------------+---------------+---------------+---------------+
     ###/###         ###/###         ###/###         ###/###
+---------------+---------------+---------------+---------------+
|A B Q R C D S a|E F b c G H d e|I J f g K L h i|M N j k O P l m|
+---------------+---------------+---------------+---------------+
   #/#     #/#     #/#     #/#       #/#   #/#     #/#     #/#
+---------------+---------------+---------------+---------------+
|A Q B R C S D a|E b F c G d G e|I f J g K h L i|M j N k O l P m|
+---------------+---------------+---------------+---------------+

код:

tmp = (input ^ (input >> 8)) & 0x0000ff00;
input ^= (tmp ^ (tmp << 8));
tmp = (input ^ (input >> 4)) & 0x00f000f0;
input ^= (tmp ^ (tmp << 4));
tmp = (input ^ (input >> 2)) & 0x0c0c0c0c;
input ^= (tmp ^ (tmp << 2));
tmp = (input ^ (input >> 1)) & 0x22222222;
input ^= (tmp ^ (tmp << 1));                    /* = output */

Обратная операция может быть выполнена путем выполнения 4 шагов назад:

tmp = (input ^ (input >> 1)) & 0x22222222;
input ^= (tmp ^ (tmp << 1));                    /* = output */
tmp = (input ^ (input >> 2)) & 0x0c0c0c0c;
input ^= (tmp ^ (tmp << 2));
tmp = (input ^ (input >> 4)) & 0x00f000f0;
input ^= (tmp ^ (tmp << 4));
tmp = (input ^ (input >> 8)) & 0x0000ff00;
input ^= (tmp ^ (tmp << 8));

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


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

Ответ 3

Не используйте ветвление:

output =
   (input & 1)
   | ((input & 2) << 1)
   | ((input & 4) << 2)
   | ((input & 8) << 3)
   | ((input & 16) << 4)
   /* etc. */

Здесь возможно легче прочитать/понять версию того же самого:

output =
     ((input & (1 <<  0)) <<  0)
   | ((input & (1 <<  1)) <<  1)
   | ((input & (1 <<  2)) <<  2)
   | ((input & (1 <<  3)) <<  3)
   | ((input & (1 <<  4)) <<  4)
   | ((input & (1 <<  5)) <<  5)
   | ((input & (1 <<  6)) <<  6)
   | ((input & (1 <<  7)) <<  7)
   | ((input & (1 <<  8)) <<  8)
   | ((input & (1 <<  9)) <<  9)
   | ((input & (1 << 10)) << 10)
   | ((input & (1 << 11)) << 11)
   | ((input & (1 << 12)) << 12);

Ответ 4

Вы можете сделать:

; eax = input bits
shrd edx,eax,2
shr eax,1
shrd edx,eax,2
shr eax,1
shrd edx,eax,2
shr eax,1
shrd edx,eax,2
shr eax,1
shrd edx,eax,2
shr eax,1
shrd edx,eax,2
shr eax,1
shrd edx,eax,2
shr eax,1
shrd edx,eax,2
shr eax,1
shrd edx,eax,2
shr eax,1
shrd edx,eax,2
shr eax,1
shrd edx,eax,2
shr eax,1
shrd edx,eax,2
shr eax,1
shrd edx,eax,8
and edx,0x01555555
; edx = output

Ответ 5

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

Здесь код C для 13 бит. Ниже приведен пример того, как метод работает для 3 бит, и обобщение будет ясным. Надеюсь.

(Примечание: код не развернут. Хороший компилятор сделает это за вас, поэтому вы можете просто сконденсировать его в цикле.)

unsigned mask, output;
unsigned x = input;

mask = ((1<<13)-1) << 13;
x = (x + mask) & ~mask;

mask = ((1<<12)-1) << 12;
x = (x + mask) & ~mask;

...

mask = ((1<<3)-1) << 3;
x = (x + mask) & ~mask;

mask = ((1<<2)-1) << 2;
x = (x + mask) & ~mask;

mask = ((1<<1)-1) << 1;
x = (x + mask) & ~mask;

output = x;

код >

Теперь, здесь объяснение метода для 3 бит. Начальное состояние - "00abc". Начните с перемещения "a" на два места слева, добавив 01100, а затем ANDing с 10011 (что побитовое NOT предыдущего номера). Вот как это работает при a = 0,1 (первая стрелка - это добавление, вторая стрелка - AND):

a = 0: 00abc = 000bc → 011bc → 000bc = a00bc
a = 1: 00abc = 001bc → 100bc → 100bc = a00bc

Затем переместите 'b' на одно место слева, добавив 00010, а затем ANDing с 10101:

b = 0: a00bc = a000c → a001c → a000c = a0b0c
b = 1: a00bc = a001c → a010c → a010c = a0b0c

Что это.

Ответ 6

Во-первых, для ваших "26-битных" значений старший бит всегда должен быть ясным, поэтому на самом деле это 25-битное значение.

1) MMX (и/или SSE) не поможет, так как основная проблема заключается в том, что нет простой серии арифметических или логических операций, которая дает желаемые результаты, и все поддерживает одни и те же арифметические и логические операции.

2) Я не мог придумать или найти магическую константу для умножения.

3) Я не вижу метода использования любой команды установки условий (например, SETcc), которая имеет какие-либо преимущества перед инструкциями shift/add.

4) jdv и paul (см. выше) являются правильными. Если вам необходимо сделать это преобразование достаточно часто, чтобы производительность имела значение, тогда таблица поиска была бы лучшим/быстрым вариантом для современных процессоров. Таблица поиска для "13-бит-26-бит" будет 2 ** 13 слов или 32 KiB. На старых процессорах (с небольшими кешами L1) относительная разница между скоростью ЦП и скоростью ОЗУ не так плоха, как сейчас.

Если вы не можете сэкономить 32 KiB для таблицы поиска от 13 до 25 бит, вы можете разбить 13-битное значение на пару значений (одно 6-битное значение и одно 7-битное значение), а затем используйте таблицу поиска по каждому из этих значений перед объединением результатов, например:

mov ebx,eax                    ;ebx = 13-bit value
shr eax,6                      ;eax = highest 7 bits of value
and ebx,0x003F                 ;ebx = lowest 6 bits of value
mov eax,[lookup_table + eax*2] ;eax = highest 14-bits of result
mov ebx,[lookup_table + ebx*2] ;eax = lowest 12-bits of result
shl eax,12
or eax,ebx                     ;eax = 25-bit result

В этом случае таблица поиска содержит 128 записей (с 2 байтами на запись), поэтому она составляет всего 256 байтов.

5) Для обратной операции простая таблица поиска обойдется вам в 64 MiB (2 ** 25 * 2), так что это не очень хорошая идея. Тем не менее, вы можете разделить 25-битное значение на 13-битное значение и 11-битное значение (12-битное значение, в котором старший бит всегда ясен) и использовать таблицу записей 8192 с одним байтом на запись (всего стоимость составляет 8 Киб). Нет причин, по которым вы не могли бы разделить 25-битные значения на более/меньшие части (и использовать гораздо меньшую таблицу).

Ответ 7

В процессорах Intel x86, начиная с Haswell, вы можете использовать одну команду pdep из набора команд BMI2, чтобы сделать это:

uint32_t interleave_zero_bits(uint32_t x) {
    return _pdep_u32(x, 0x55555555U);
}

Ответ 8

Я думаю, это может быть актуальным, но я не совсем уверен. Я знаю инструкции MMX для чередования байтов 32/64 бит, но не отдельные биты.

Ответ 9

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

Большинство платформ имеют отдельные команды сдвига и поворота. Почти всегда есть инструкция, которая включает флаги переноса/переполнения, так что вы можете "сдвинуть" бит, который вы хотите. Скажем, у нас есть следующие инструкции: * SHIFTLEFT: делает левый сдвиг и заполняет нижний бит нулем. * ROTATELEFT: делает левый сдвиг, устанавливает младший бит из прежнего значения в флагом переноса и устанавливает перенос с бита, который сдвинулся слева от него.

псевдокод:

LOAD value into register A;
LOAD 0 into register B;
SHIFT register A (registerwidth-13) times; 
ROTATELEFT A
ROTATELEFT B
SHIFTLEFT  B

... повторите 13 раз. Разверните, как вам будет угодно.

Первая смена должна занять самый верхний бит прямо перед переносом. ROTATELEFT A вытолкнет MSB в перенос, ROTATELEFT B вытолкнет бит в LSB B, а SHIFTLEFT B поместит 0 дюймов. Сделайте это для всех бит.


Редактировать/Добавлено:

Вы можете сделать обратное (обратное преобразование растрового изображения) с теми же инструкциями, как это:

Значение LOAD в регистр A; LOAD 0 в регистр B;

ROTATELEFT A; ROTATELEFT A; ROTATELEFT B; ... повторить 13 раз а потом SHIFTLEFT B; для (ширина регистров-13).

LSB для переноса; забыть об этом, следующий LSB в перенос, поместить его в целевой регистр, повторить для всех бит, а затем выровнять результат.

Ответ 10

Вы всегда можете использовать цикл for:

for (int i = 0; i < 13; i++)
{
    output |= (input & (1 << i)) << i;
}

Это короче, но я не думаю, что это значительно быстрее.

Ответ 11

Проверьте, поддерживает ли ваш процессор байтов и слово swapping (для преобразования в конце) - если это так - просто переверните swap над ним - это будет примерно на 6 (5) инструкций короче.