Ответ 1
Вы можете использовать предварительно вычисленную таблицу поиска и уменьшить количество итераций до 2 или 4.
Вы также можете использовать логарифмический подход.
Подробнее см. эту статью в Википедии.
Итак, у меня был вопрос с интервью, прежде чем обсуждать манипуляции с битами. Компания - известная компания GPU. У меня было очень мало фона на языке ассемблера (странно, несмотря на то, что я являюсь студентом-программистом в компьютерной архитектуре), и, как показывает это повествование, я его не понимаю. Вопрос был простой:
"Напишите быстрый код, который будет считать число 1 в 32-битном регистре".
Теперь я изучаю сборку рук. Поэтому, естественно, я снова заново рассмотрел эту проблему и придумал этот код, просто изучив ISA.
Для вас, армейские эксперты, это правильно? Есть ли более быстрый способ сделать это? Будучи новичком, я, естественно, считаю, что это неполное. Инструкция AND в "xx" кажется избыточной, но нет другого способа сдвинуть регистр в ARM isa...
R1 будет содержать количество бит в конце, а R2 - регистр с битами, которые мы хотим подсчитать. r6 - просто фиктивный регистр. Комментарии прилагаются в()
MOV R1, #0 (initialize R1 and R6 to zero)
MOV R6, #0
xx: AND R6, R6, R2, LSR #1 (Right shift by 1, right most bit is in carry flag)
ADDCS R1, #1 (Add #1 to R1 if carry flag is set)
CMP R2, #0 (update the status flags if R2 == 0 or not)
BEQ xx (branch back to xx until R2==0)
Вы можете использовать предварительно вычисленную таблицу поиска и уменьшить количество итераций до 2 или 4.
Вы также можете использовать логарифмический подход.
Подробнее см. эту статью в Википедии.
Если этот код работает быстро или не зависит от процессора. Конечно, это будет не очень быстро на Cortex-A8, но может работать очень быстро на Cortex-A9 и более новом процессоре.
Это, однако, очень короткое решение.
Ожидает ввод в r0 и возвращает вывод в r0
vmov.32 d0[0], r0
vcnt.8 d0, d0
vmov.32 r0, d0[0]
add r0, r0, r0, lsr #16
add r0, r0, r0, lsr #8
and r0, r0, #31
Основная работа выполняется в команде vcnt.8, которая подсчитывает бит каждого байта в регистре NEON и сохраняет битконт назад в байты D0.
Нет формы vcnt.32
, только .8
, поэтому вам нужно горизонтально добавить 4 байта вместе, что и делает остальная часть кода.
Лучшие ссылки для бит-хаков -
Bit Twiddling Hacks
страница говорит
The best method for counting bits in a 32-bit
integer v is the following:
v = v - ((v >> 1) & 0x55555555); // reuse input as temporary
v = (v & 0x33333333) + ((v >> 2) & 0x33333333); // temp
c = ((v + (v >> 4) & 0xF0F0F0F) * 0x1010101) >> 24; // count
Тогда я предлагаю вам использовать gcc
и objdump
(или этот отличный онлайн-инструмент gcc), чтобы увидеть, как этот высокий уровень фрагмент будет выглядеть как инструкции для рук.
00000000 <popcount>:
0: 1043 asrs r3, r0, #1
2: f003 3355 and.w r3, r3, #1431655765 ; 0x55555555
6: 1ac0 subs r0, r0, r3
8: 1083 asrs r3, r0, #2
a: f000 3033 and.w r0, r0, #858993459 ; 0x33333333
e: f003 3333 and.w r3, r3, #858993459 ; 0x33333333
12: 18c0 adds r0, r0, r3
14: eb00 1010 add.w r0, r0, r0, lsr #4
18: f000 300f and.w r0, r0, #252645135 ; 0xf0f0f0f
1c: eb00 2000 add.w r0, r0, r0, lsl #8
20: eb00 4000 add.w r0, r0, r0, lsl #16
24: 1600 asrs r0, r0, #24
26: 4770 bx lr
Итак, похоже, что это дает результат в инструкциях 12
, которые грубо могут перевести на такое же количество циклов.
Сравнивая значение целочисленного tweedling выше с подходом look up table
, используемое libgcc, таблица поиска должна быть еще медленнее, учитывая дополнительные обращения к памяти.
00000028 <__popcountSI2>:
28: b410 push {r4}
2a: 2200 movs r2, #0
2c: 4c06 ldr r4, [pc, #24] ; (48 <__popcountSI2+0x20>)
2e: 4613 mov r3, r2
30: fa40 f103 asr.w r1, r0, r3
34: 3308 adds r3, #8
36: 2b20 cmp r3, #32
38: b2c9 uxtb r1, r1
3a: 5c61 ldrb r1, [r4, r1]
3c: 440a add r2, r1
3e: d1f7 bne.n 30 <__popcountSI2+0x8>
40: 4610 mov r0, r2
42: bc10 pop {r4}
44: 4770 bx lr
46: bf00 nop
48: 00000000 andeq r0, r0, r0
<.. snipped ..>
Так как это помеченный ARM, наиболее полезной может быть инструкция clz
. Проблема также описывается как подсчет населения. gcc
имеет __ builtin_popcount() для этого. Как и инструменты ARM. Существует эта ссылка (не чувствуйте себя плохо в своем решении, кто-то сделал веб-страницу с почти одинаковой), а также есть версия Dave Seal с шестью инструкциями для ARM без clz
. clz
выгоден и может использоваться для создания более быстрого алгоритма в зависимости от ввода.
Как и auselen хорошее предложение для чтения, Hacker Delight этот бит twiddling blog может быть полезен, говоря о таких вещах в графическом контексте. По крайней мере, мне показалось, что полезно понять некоторые из Qt blitting code. Тем не менее, он имеет некоторую полезность при кодировании подпрограммы подсчета населения.
Единица carry add
полезна в смысле разделения и покорения, что делает проблему O(ln n)
. clz
более полезен, если у данных есть пробеги или нули.
Запись в Hacker Delight содержит больше информации о коде ARM Dave Seal.
long count_bits_long (long);
vmov.32 d0[0], r0 // R0 --> SIMD
vcnt.8 d0, d0 // count bits in bytes
vpaddl.u8 d0, d0 // add adjacent pairs of bytes and put into 16b words
vpaddl.u16 d0, d0 // add adjacent pairs of 16b words and put into 32b word
vmov.32 r0, d0[0] // SIMD --> R0
mov pc, lr // return
LDR r0, = 0x000000FF;
MOV r1, #0;
MOV r3, #0; this will always be zero
MOV r2,r0;
rep MOVS r2, r2, LSR #1;
ADC r1,r1, r3; this adds r1 with zero plus the carry bit
CMP r2, #0;
BNE rep
Это сделает это, r3 - это просто фиктивный регистр с 0, чтобы сделать работу ADC должным образом.