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

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

Loop:   
    mrmovq (%rdi), %r10     # read val[0] from src
    andq %r10, %r10         # val[0] <= 0?
    jle Npos1               # if so, goto Npos:
    iaddq $1, %rax          # count++

Npos1:      
    mrmovq 8(%rdi), %r11    # read val[1] from src+8
    andq %r11,%r11          # val <= 0?
    jle Npos2               # if so, goto next npos:
    iaddq $1, %rax

Npos2:      
    mrmovq 16(%rdi), %r11   # read val[2] from src+8
    andq %r11,%r11          # val <= 0?
    jle Npos3               # if so, goto next npos:
    iaddq $1, %rax

Мой вопрос заключается в том, как я могу получить такую ​​же эффективную структуру, если я хочу проверить также, что она нулевая или отрицательная. В этом случае у меня будет три счетчика (один для pos, один для neg и один для нуля). Один неэффективный способ был бы таким. Я пытаюсь создать ту же структуру, что и в предыдущем примере (мы храним положительные значения в %rax, негативы в %rbx и нули в %rcx):

Loop:   mrmovq (%rdi), %r10 # read val from src...
        andq %r10, %r10     # val <= 0?
        jle Npos            # if so, goto Npos:
        irmovq $1, %r11
        addq %r11, %rax     # Count positives in rax - count_pos++ 
        jmp Rest 
Npos:   andq %r10, %r10     # Not positive 
        je Zero
        irmovq $1, %r11
        addq %r11, %rbx     # Count negatives in rbx - count_neg++
        jmp Rest
Zero:   irmovq $1, %r11
        addq %r11, %rcx     # Count zeroes in rcx - count_zero++
Rest:   irmovq $1, %r10
        subq %r10, %rdx     # len--
        irmovq $8, %r10
        addq %r10, %rdi     # src++
        addq %r10, %rsi     # dst++
        andq %rdx,%rdx      # len > 0?
        jg Loop             # if so, goto Loop:

1 ответ

Решение

обновление: смотрите самый конец для версии без ветвления, которая должна быть намного лучше, и тривиальной, чтобы развернуть. Но остальная часть ответа все еще стоит прочитать, ИМО.

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


y86 слишком урезан, чтобы позволить эффективный код по сравнению с реальными архитектурами во многих случаях. Во-первых, кажется, что нет способа условно увеличивать число без разбивания флагов. (x86 имеет lea rax, [rax+1]).

Я не вижу способа сэкономить много инструкций, считая только положительные и нулевые значения и вычисляя отрицательные значения после цикла. Вы все еще должны перейти, чтобы проверить каждое значение. обновление: нет, не вы, потому что вы можете эмулировать x86 setcc используя Y86 cmov !


Тем не менее, я нашел несколько больших улучшений в вашем коде:

  • использовать флаги, установленные первым тестом, вместо повторного тестирования

  • Еще одна важная вещь, чтобы поднять %r11 = 1 вне цикла, так что вы можете просто увеличить с одним insn. Установка констант в регистрах - это обычное дело даже в реальном коде. Большинство ISA (включая машины RISC для хранения нагрузки) имеют инструкции для немедленного добавления, например x86 add $1, %rax, но y86 не так нужен этот метод даже для приращений (x86 inc %rax)!

  • sub устанавливает флаги, поэтому используйте их вместо проведения отдельного теста.

Проблемы стиля:

С описательными названиями ярлыков вам не нужно столько комментариев.

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

        irmovq  $1, %r11     # hoisted out of the loop
        irmovq  $8, %r8

Loop:   mrmovq  (%rdi), %r10 # read val from src...
        andq    %r10, %r10   # set flags from val
        jle    not_positive
        addq    %r11, %rax   # Count positives in rax - count_pos++ 
        jmp    Rest 
not_positive:
        je     Zero          # flags still from val
        addq    %r11, %rbx   # Count negatives in rbx - count_neg++
        jmp    Rest
Zero:
        addq    %r11, %rcx   # Count zeroes in rcx - count_zero++
Rest:
        addq    %r8, %rdi    # src+=8
        //addq %r8, %rsi     # dst++  // why?  Not used...  Also note that si stands for source index, so prefer keeping src pointers in rsi, and dest pointers in rdi for human readability.
        subq    %r11, %rdx   # len--, setting flags
        jg     Loop          # } while( len-- > 1).  fall through when rdx=0

дублирование хвоста петли:

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

Я также реструктурировал цикл, так что в теле цикла есть только одна взятая ветвь на итерацию.

        irmovq $1, %r11       # hoisted out of the loop
        irmovq $8, %r8

Loop:   mrmovq (%rdi), %r10   # read val from src...
        addq   %r8, %rdi      # src+=8 for next iteration

        andq   %r10, %r10     # set flags from val
        je    Zero
        jl    Negative
        # else Positive
        addq   %r11, %rax     # Count positives in rax - count_pos++ 

        subq   %r11, %rdx
        jg    Loop
        jmp   end_loop
Negative:
        addq   %r11, %rbx     # Count negatives in rbx - count_neg++

        subq   %r11, %rdx
        jg    Loop 
        jmp   end_loop
Zero:
        addq   %r11, %rcx     # Count zeroes in rcx - count_zero++

        subq   %r11, %rdx     # len--, setting flags
        jg Loop               # } while( len-- > 1).  fall through when rdx=0
end_loop:

Развертывание не принесет особой пользы, так как тело цикла такое большое. Если вы это сделали, вы можете сделать это так:

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

Развернутый вдвое, с дублированием хвоста

        irmovq $1, %r11       # hoisted out of the loop
        irmovq $2, %r12
        irmovq $16, %r8

        sub    %r12, %rdi
        jl     end_loop       # unrolled loop requires len >= 2

Loop:   mrmovq (%rdi), %r10   # read val from src...
        mrmovq 8(%rdi), %r9   # read next val here so we don't have to duplicate this
        addq   %r8, %rdi      # src+=16 for next iteration

        andq   %r10, %r10     # set flags from val
        je    Zero1
        jl    Negative1
        # else Positive1
        addq   %r11, %rax     # Count positives in rax - count_pos++ 

        andq   %r9, %r9       # set flags from val2
        je    Zero2
        jl    Negative2
Positive2:
        addq   %r11, %rax     # Count positives in rax - count_pos++ 

        subq   %r12, %rdx     # loop tail
        jge   Loop
        jmp   end_loop

Negative1:
        addq   %r11, %rbx     # Count negatives in rbx - count_neg++

        andq   %r9, %r9       # set flags from val2
        je    Zero2
        jg    Positive2
Negative2:
        addq   %r11, %rbx     # Count negatives in rbx - count_neg++

        subq   %r12, %rdx     # loop tail
        jge   Loop 
        jmp   end_loop

Zero1:
        addq   %r11, %rcx     # Count zeroes in rcx - count_zero++

        andq   %r9, %r9       # set flags from val2
        jg    Positive2
        jl    Negative2
Zero2:
        addq   %r11, %rcx     # Count zeroes in rcx - count_zero++

        subq   %r12, %rdx     # len-=2, setting flags
        jge   Loop            # fall through when rdx=-1 or -2
end_loop:

# loop epilogue to handle cases where there was an odd number of elements, so rdx=-1 here:
        add   %r12, %rdx
        jne  all_done
        #else there's one more to do
        #... load and test a single element

Я не удивлюсь, если в моих условиях цикла что-то не так, или что-то в этом роде.


Как отметил в комментариях Шут, x86 может считать негативы с

sar   $63, %r10     # broadcast the sign bit to all bits: -1 or 0
sub   %r10, %rbx    # subtracting -1 (or 0): i.e. add 1 (or 0)

Обновление: версия без ветвления, которая использует cmov для эмуляции setcc.

Мы можем использовать cmov установить регистр в 0 или 1, а затем добавить это к нашему счету. Это позволяет избежать всех ветвлений. (0 - аддитивная идентичность. Этот базовый метод работает для любой операции, которая имеет элемент идентичности. Например, все единицы - элемент идентичности для AND. 1 - элемент идентичности для умножения.)

Развертывание это просто, но есть 3 инструкции из-за циклических издержек по сравнению с 8 инструкциями, которые необходимо повторить. Прибыль будет довольно небольшой.

        irmovq $1, %r11       # hoisted out of the loop
        irmovq $8, %r8
        mov    %rdx, %rbx     # neg_count is calculated later

Loop:   mrmovq (%rdi), %r10   # read val from src...
        addq   %r8, %rdi      # src+=16 for next iteration

        andq   %r10, %r10     # set flags from val

        irmovq $0, %r13
        cmovg  %r11, %r13     # emulate setcc
        irmovq $0, %r14
        cmove  %r11, %r14

        add    %r13, %rax     # pos_count  += (val >  0)
        add    %r14, %rcx     # zero_count += (val == 0)

        subq   %r11, %rdx     # len-=1, setting flags
        jg    Loop            # fall through when rdx=0
end_loop:

        sub    %rax, %rbx
        sub    %rcx, %rbx     # neg_count = initial_len - pos_count - zero_count

Если ветки (особенно непредсказуемые) дороги, эта версия будет работать намного лучше. Использование предложенного Джестером расчета одного из двух отсчетов очень помогло в этом случае.

Здесь довольно хороший параллелизм на уровне инструкций. Две отдельные цепочки зависимостей setcc -> add могут работать параллельно, как только результат теста будет готов.

Другие вопросы по тегам