Программирование сборки ARM / инструкция по моноциклу - PullRequest
0 голосов
/ 01 июля 2018

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

I) Все строковые значения должны храниться в памяти с базовой позиции X * (X 100 умножить на 100), со смещением, необходимым для получения всех 32-битных значений.

II) Окончательное значение должно быть сохранено в дополнение к последней позиции памяти данных в регистре RX.

III) Программа должна проверить, являются ли значения менее 2 147 483 648 прогрессий (1000000000000000000000000000000) 2

IV) Необходимо выполнить инструкции TST, LSL и CMP по одному, в дополнение к инструкциям, уже реализованным в одноколесном велосипеде ARM, которые можно использовать в соответствии с потребностями.

START
        AND     R5, R5, #0      ;Reset Registers
        AND     R7, R7, #0
        AND     R0, R0, #0
        AND     R4, R4, #0

        ADD     R5, R5, #1      ;R5 receives de base number of GP
        ADD     R7, R7, R5      ;R7 = register of reference number
        ADD     R0, R0, #200
        ADD     R0, R0, #200
        ADD     R0, R0, #200
        ADD     R0, R0, #100    ;sets the memory base position in 700

        ADD     R4, R4, #1      ;starts the count
        STR     R7, [R0]        ;saves the first GP value in the memory

LOOP
        TST     R7, #2147483648 ;check if GP values is higher than 2^31
        BNE     FIM             ;if so, ends the code
        LSL     R7, R7, #1      ;else, multiply by 2
        ADD     R4, R4, #1      ;increments the count
        ADD     R0, R0, #4      ;increments memory adress
        STR     R7, [R0]        ;saves value in the memory
        CMP     R4, #10         ;check if 10 interations have been executed
        BEQ     FIM             ;if so, ends the code
        B       LOOP            ;else, restarts loop loop
END

Поскольку я «новичок» в программировании ASM, мне было интересно, что можно улучшить в этом коде, соблюдая четыре требования, изложенных ранее? ЛЮБЫЕ перемены были бы хорошими, лучший способ чему-то научиться, если смотреть на это с разных сторон, верно? Заранее спасибо

Ответы [ 2 ]

0 голосов
/ 02 июля 2018

Ваша ветвь цикла должна быть cmp / bne loop, поэтому вы проваливаетесь, чтобы закончить цикл. Это означает, что в цикле меньше инструкций перехода См. Почему циклы всегда компилируются в стиле "do ... while" (прыжок с хвоста)? .

Кроме того, используйте флаги с инструкциями, которые вам уже нужны, вместо использования отдельных инструкций TST или CMP.

Если вы собираетесь использовать счетчик отдельно от выходного указателя, отсчитайте его до нуля, чтобы вы могли subs r4, r4, #1 / bne.


В вашем коде есть много пропущенных оптимизаций, особенно ваш безумный способ создания констант в регистрах. ARM имеет настоящую mov инструкцию; используйте его вместо ANDing или ADDing с нулем.

Посмотрите, что будет делать хороший компилятор C: вывод компилятора часто является хорошей отправной точкой для оптимизации или способом освоить приемы для машины, на которую вы ориентируетесь. (См. Также Как убрать «шум» из выходных данных сборки GCC / clang? и доклад Мэтта Годболта о CppCon2017 «Что мой компилятор сделал для меня в последнее время? Снятие крышки с компилятора» * .)


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

// uint32_t would be more portable, but ARM has 32-bit unsigned int
void store_sequence(unsigned val)
{
    unsigned *dst = (unsigned *)700;
    unsigned *endp = dst + 10;

    // val = 1;   // use the function arg so it's not a compile-time constant
    for (; dst < endp; dst++) {
        *dst = val;    // first store without checking the value
        val <<= 1;
        if (val >= (1UL << 31))
            break;
    }
}

Проверка val сразу после сдвига дает хороший ассемблер: в противном случае компилятор не всегда использует преимущество shift для установки флагов. Обратите внимание, что, хотя это цикл for(), компилятор может доказать, что условие выполняется в первый раз, и не добавляет дополнительную проверку / ветвь вверху, чтобы увидеть, должен ли цикл выполняться ноль раз.

Я поместил этот код в проводник компилятора Godbolt , чтобы получить выходные данные gcc и clang для ARM.

gcc7.2.1 -O3 полностью разворачивает цикл. При большем числе он в конечном итоге решает сделать цикл, но развернутый цикл интересен: при полном развертывании указатель не требуется увеличивать. Использование другого числа сдвига для повторного смещения оригинала также создает параллелизм на уровне команд (ЦП может выполнять несколько команд сдвига параллельно, поскольку нет зависимости от результата предыдущего.)

Обратите внимание, что lsls устанавливает флаги из сдвига, а флаги ARM включают флаг N, который устанавливается, если установлен старший бит результата. Условие MInus истинно, если N==1. Название происходит от отрицательных чисел, дополняющих 2, но все это всего лишь биты, и вы можете использовать его для разветвления старшего бита. (Условие PLus имеет странное название: оно верно для неотрицательных результатов, включая ноль, т. Е. Проверяется только N==0. https://community.arm.com/processors/b/blog/posts/condition-codes-1-condition-flags-and-codes)

Вместо фактического bmi (ветвь, если минус), компилятор решил использовать предикат bx lr. то есть вернуть, если MInus, в противном случае он работает как NOP. (Использование -mcpu=cortex-a57 приводит к bmi в нижней части цикла, с bx lr. Очевидно, что параметры настройки для этой микроархитектуры позволяют gcc избегать предикатных bx инструкций.)

@ On function entry, val is in r0.  Use  mov r0, #1  if you want
@ from gcc7.2.1 -O3
store_sequence:
    mov     r3, #0             @ this is the most efficient way to zero a reg

    lsls    r2, r0, #1         @ instruction scheduling: create r2 early
    str     r0, [r3, #700]     @ gcc just uses offsets from a zeroed reg

    bxmi    lr                 @ if(val<<1 has its high bit set) return;

    lsls    r1, r0, #2
    str     r2, [r3, #704]     @ 2nd store, using val<<1 after checking it
    bxmi    lr

    lsls    r2, r0, #3         @ alternating r1 and r2 for software pipelining
    str     r1, [r3, #708]     @ 3rd store, using val<<2 after checking it
    bxmi    lr
...

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

С endp = dst+100 и gcc -O3 mcpu=cortex-a57 (чтобы избежать bxmi lr), мы получаем интересный цикл, который вводится, перепрыгивая в середину, чтобы он мог провалиться внизу. (В этом случае, вероятно, было бы эффективнее просто позволить cmp / beq запустить первую итерацию или поместить cmp / bne внизу. -Os выполняет последнюю.)

@ gcc -O3 -mcpu=cortex-a57     with loop count = 100 so it doesn't unroll.
store_sequence:
    mov     r3, #700
    movw    r2, #1100         @ Cortex-A57 has movw.  add would work, too.
    b       .L3

.L6:                          @ do {
    cmp     r3, r2
    beq     .L1                  @ if(p==endp) break;
.L3:                                 @ first iteration loop entry point
    str     r0, [r3]
    lsls    r0, r0, #1           @ val <<= 1
    add     r3, r3, #4           @ add without clobbering flags
    bpl     .L6               @ } while(val's high bit is clear)
.L1:
    bx      lr

С -Os мы получаем более привлекательный цикл. Единственным недостатком является то, что bmi (или bxmi lr) читает флаги сразу в следующей инструкции после того, как lsls установит флаги. Вы можете запланировать add между ними, хотя. (Или в режиме большого пальца вы захотите сделать это так, потому что adds имеет более короткую кодировку, чем add.)

@ gcc7.2.1 -Os  -mcpu=cortex-a57
store_sequence:
    mov     r3, #700               @ dst = 700
.L3:                            @ do{
    str     r0, [r3]
    lsls    r0, r0, #1            @ set flags from val <<= 1
    bxmi    lr                    @  bmi  to the end of the loop would work

    add     r3, r3, #4            @ dst++
    cmp     r3, #740
    bne     .L3                 @ } while(p != endp)

 @ FIM:
    bx      lr

С большим endp, который не помещается в непосредственный операнд для cmp, gcc вычисляет его в регистре вне цикла.

Он всегда использует mov или загружает его из литерального пула в памяти вместо использования add r2, r3, #8192 или чего-то еще. Я не уверен, что сконструировал случай, когда немедленное значение для add сработало бы, но немедленное значение для movw не сработало бы.

Так или иначе, обычный mov работает для небольших немедленных операций, но movw - более новая кодировка, которая не является базовой, поэтому gcc использует movw только когда вы компилируете с -mcpu= то, что имеет его.

0 голосов
/ 01 июля 2018

Это похоже на домашнее задание, поэтому я даю вам только несколько советов.

Ну, вам нужен ярлык где-то для FIM. Я предполагаю, что это в конце вашего кода.

Принудительное задание регистра в ноль и последующее добавление к нему небольших непосредственных значений выглядит как код MIPS. Узнайте, какие значения можно использовать как непосредственные, и изучите инструкции MOV и MOVW.

Наличие условной ветви вокруг безусловной ветви - плохое программирование, замените ее одной условной ветвью.

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

...