Масса Хэмминга (число 1 в ряду) смешивание C со сборкой

Я пытаюсь посчитать, сколько число 1, в числах массива.

Сначала у меня есть код на C lenguaje(работает нормально):

int popcount2(int* array, int len){
    int i;
    unsigned x;
    int result=0;
    for (i=0; i<len; i++){
        x = array[i];
        do{
           result+= x & 0x1;
           x>>= 1;
       } while(x);
    }
return result;
}

Теперь мне нужно перевести цикл do-while в Assembly, используя 3-6 строк кода. Я написал некоторый код, но результат не правильный (я новичок в мире сборки)

int popcount3(int* array, int len){
int  i;
unsigned x;
int result=0;   
for (i=0; i<len; i++){
    x = array[i];
    asm(
    "ini3:               \n"
        "adc $0,%[r]     \n"
        "shr %[x]        \n"
        "jnz ini3        \n"

        : [r]"+r" (result)
        : [x] "r" (x)       );
  }
}

Я использую GCC (на Linux) с процессором Intel.

4 ответа

Решение

Самое приятное, что вы можете сделать, это использовать встроенный popcount функция, как предложено Полом Р., но так как вам нужно написать это в сборке, это сработало для меня:

asm (
"start:                  \n"
        "and %0, %1      \n"
        "jz end          \n"
        "shr $0, %1      \n"
        "jnc start       \n"
        "inc %1          \n"
        "jmp start       \n"
"end:                    \n"
        : "+g" (result),
          "+r" (x)
        :
        : "cc"
);

В первых двух строках вы просто проверяете содержимое x (и идти до конца, если это ноль Jump Zero). Чем ты сдвигаешься x один бит вправо и:

В конце смены CF флаг содержит последний бит, сдвинутый из целевого оператора. *

Если нет CF установить, просто пойти, чтобы начать (Jump Not Carry) иначе увеличьте результат, а затем перейдите к началу.

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

asm (
"start:                  \n"
        "shr $1, %1      \n"
        "jnc loop_cond   \n"
        "inc %0          \n"
        "and %1, %1      \n"
"loop_cond:              \n"
        "jnz start       \n"

        : "+g" (result),
          "+r" (x)
        :
        : "cc"
);

Здесь вы снова используете SHift Right инструкция, если нет CF присутствует просто перейти в состояние цикла.

В противном случае снова увеличиваем результат и вызываем двоичный AND (INC действительно изменить ZF).


С помощью LOOP а также ECX

Мне было любопытно, как это сделать в 3-х инструкциях (я подумал, что ваш учитель не даст вам нижний предел в 3, если это невозможно), и я понял, что x86 также имеет LOOP инструкция:

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

И вы можете добавить входной аргумент, используя ограничение ввода GCC:

c - c регистр.

asm (
"start:              \n"
    "shr $1, %1      \n"
    "adc $0, %0      \n"
    "loop start      \n"

    : "+g" (result)
    : "r" (x),
      "c" (8)             // Assuming 8b type (char)
);

Просто чтобы убедиться, что он компилируется для правильной сборки:

0x000000000040051f <+25>:   mov    $0x8,%ecx
0x0000000000400524 <+30>:   mov    -0x8(%rbp),%eax
0x0000000000400527 <+33>:   shr    %edx
0x0000000000400529 <+35>:   adc    $0x0,%eax
0x000000000040052c <+38>:   loop   0x400527 <main+33>

Я думаю, что первый должен иметь чуть лучшую производительность, особенно если установлен только 1 бит, такой подход всегда k*8 итерации.


SSE4 и одиночная инструкция

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

Эта инструкция вычисляет количество битов, установленных на 1 во втором операнде (источник) и возвращает счет в первом операнде (регистр назначения). *

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

asm (   
    "POPCNT %1, %0   \n"
    : "=r" (result)
    : "mr" (x)
    : "cc"                                                                                                                                       
);

(Если вы попробуете это и у вас есть расширение SSE4, пожалуйста, дайте мне знать, если оно работает)


Спектакль

Я измерил время, необходимое для выполнения 100000000 поп-аккаунтов, сравнивая мои первый и второй методы с методами Дэвида Уолферда. [Необработанные данные]

+--------------+------------+------------+------------+
|              | 0x00000000 | 0x80000001 | 0xffffffff |
+--------------+------------+------------+------------+
| 1st solution |  0.543     |  5.040     |  3.833     |
| LOOP         | 11.530     | 11.523     | 11.523     |
| Davids       |  0.750     |  4.893     |  4.890     |
+--------------+------------+------------+------------+

Если кто-нибудь может сравнить эти 3 с SSE4 POPCNT инструкция я был бы рад.

Вы начинаете с действительно неэффективного алгоритма - если вы используете лучший алгоритм, вам не нужно тратить время на ассемблер. Посмотрите Восхищение Хакера и / или Бит Тиддлинг Хаки для намного более эффективных методов.

Также обратите внимание, что более новые процессоры x86 имеют инструкцию POPCNT, которая выполняет все вышеперечисленное в одной инструкции (и вы можете вызывать ее через встроенную функцию, так что нет необходимости в asm).

И, наконец, gcc имеет встроенную функцию: __builtin_popcount, который снова делает все, что вам нужно - он будет использовать POPCNT на более новых процессорах и эквивалентный asm на более старых процессорах.

Когда мне нужно было создать поп-счет, я в конечном итоге использовал метод 5 и 3 из упомянутых Bit Twiddling Hacks @PaulR. Но если бы я хотел сделать это с помощью цикла, может быть, что-то вроде этого:

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

int popcount2(int v) {
   int result = 0;
   int junk;

   asm (
        "shr $1, %[v]      \n\t"   // shift low bit into CF
        "jz done           \n"     // and skip the loop if that was the only set bit
     "start:               \n\t"
        "adc $0, %[result] \n\t"   // add CF (0 or 1) to result
        "shr $1, %[v]      \n\t"
        "jnz start         \n"     // leave the loop after shifting out the last bit
     "done:                \n\t"
        "adc $0, %[result] \n\t"   // and add that last bit

        : [result] "+r" (result), "=r" (junk)
        : [v] "1" (v)
        : "cc"
   );

   return result;
}

int main(int argc, char *argv[])
{
   for (int x=0; x < argc-1; x++)
   {
      int v = atoi(argv[x+1]);

      printf("%d %d\n", v, popcount2(v));
   }
}

adc почти всегда более эффективен, чем ветвление на CF.

"=r" (junk) является фиктивным выходным операндом, который находится в том же регистре, что и v ("1" ограничение). Мы используем это, чтобы сообщить компилятору, что оператор asm уничтожает v вход. Мы могли бы использовать [v] "+r"(v) чтобы получить операнд чтения-записи, но мы не хотим переменную C v быть обновленным.

Обратите внимание, что счетчик отключения цикла для этой реализации является позицией самого высокого установленного бита. (bsr, или же 32 - clz(v)). Реализация @rcgldr, которая очищает младший установленный бит на каждой итерации, обычно будет быстрее, когда число установленных битов невелико, но не все они находятся в нижней части целого числа.

сборка с использованием 3-6 строк кода.

В этом примере используется цикл из 4 инструкций:

popcntx proc    near
        mov     ecx,[esp+4]             ;ecx = value to popcnt
        xor     eax,eax                 ;will be popcnt
        test    ecx,ecx                 ;br if ecx == 0
        jz      popc1
popc0:  lea     edx,[ecx-1]             ;edx = ecx-1
        inc     eax                     ;eax += 1
        and     ecx,edx                 ;ecx &= (ecx-1)
        jnz     short popc0
popc1:  ret
popcntx endp

В этом примере используется цикл из 3 инструкций, но он будет медленнее, чем версия цикла из 4 инструкций на большинстве процессоров.

popcntx proc    near
        mov     eax,[esp+4]             ;eax = value to popcnt
        mov     ecx,32                  ;ecx = max # 1 bits
        test    eax,eax                 ;br if eax == 0
        jz      popc1
popc0:  lea     edx,[eax-1]             ;eax &= (eax-1)
        and     eax,edx
        loopnz  popc0
popc1:  neg     ecx
        lea     eax,[ecx+32]
        ret
popcntx endp

Это альтернативный пример без зацикливания:

popcntx proc    near
        mov     ecx,[esp+4]             ;ecx = value to popcnt
        mov     edx,ecx                 ;edx = ecx
        shr     edx,1                   ;mov upr 2 bit field bits to lwr
        and     edx,055555555h          ; and mask them
        sub     ecx,edx                 ;ecx = 2 bit field counts
                                        ; 0->0, 1->1, 2->1, 3->1
        mov     eax,ecx
        shr     ecx,02h                 ;mov upr 2 bit field counts to lwr
        and     eax,033333333h          ;eax = lwr 2 bit field counts
        and     ecx,033333333h          ;edx = upr 2 bit field counts
        add     ecx,eax                 ;ecx = 4 bit field counts
        mov     eax,ecx
        shr     eax,04h                 ;mov upr 4 bit field counts to lwr
        add     eax,ecx                 ;eax = 8 bit field counts
        and     eax,00f0f0f0fh          ; after the and
        imul    eax,eax,01010101h       ;eax bit 24->28 = bit count
        shr     eax,018h                ;eax bit 0->4 = bit count
        ret
popcntx endp
Другие вопросы по тегам