Самый быстрый способ подсчета числа в регистре, сборка ARM

Итак, у меня был вопрос с интервью, прежде чем обсуждать манипуляции с битами. Компания - известная компания 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)

Ответ 1

Вы можете использовать предварительно вычисленную таблицу поиска и уменьшить количество итераций до 2 или 4.

Вы также можете использовать логарифмический подход.

Подробнее см. эту статью в Википедии.

Ответ 2

Если этот код работает быстро или не зависит от процессора. Конечно, это будет не очень быстро на 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 байта вместе, что и делает остальная часть кода.

Ответ 3

Лучшие ссылки для бит-хаков -

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 ..>

Ответ 4

Так как это помеченный 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.

Ответ 5

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

Ответ 6

    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 должным образом.