Ваша ветвь цикла должна быть 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=
то, что имеет его.