В основном мне трудно получить время выполнения ниже, чем оно есть, а также уменьшить количество тактовых циклов и размер памяти. Кто-нибудь знает, как я могу это сделать? Код работает нормально, я просто хочу его немного изменить.
Написал рабочий код, но не хочу портить код, но и не знаю какие изменения внести.
; Calculation of a factorial value using a simple loop
; set up the exception addresses
THUMB
AREA RESET, CODE, READONLY
EXPORT __Vectors
EXPORT Reset_Handler
__Vectors
DCD 0x00180000 ; top of the stack
DCD Reset_Handler ; reset vector - where the program starts
AREA 2a_Code, CODE, READONLY
Reset_Handler
ENTRY
start
MOV r1,#0 ; count the number of multiplications performed
MOV r2,#3 ; the final value in the factorial calculation
MOV r3,#1 ; the factorial result will be stored here
; loop r2 times forming the product
fact
ADD r1,r1,#1 ; find the next multiplicand
MUL r3,r1,r3 ; form the next product - note that MUL r3,r3,r1 gives unpredictable output
CMP r1,r2 ; check if the final value has been reached
BMI fact ; continue if all products have not been formed
exit ; stay in an endless loop
B exit
END
Текущие результаты: Размер памяти: 0x00000024 Тактовые циклы: 22 Общее время выполнения: 1,1 микросекунды
Работаем с Cortex M3
Мне просто нужно, чтобы любой из них был уменьшен, изменения в коде могут быть незначительными, если они дают разные результаты.
Не могли бы вы уточнить, что вы подразумеваете под представленным кодом, который можно заменить на MOV r3, # 6, пожалуйста? Ядром процессора является Cortex M3, надеюсь, это поможет.
@ Hysteria103: ваша текущая программа вычисляет факториал постоянной времени сборки. Точно так же, как компилятор C, выполняющий распространение констант и заменяющий цикл результатом, это будет эквивалентно mov r3, #n!
, за исключением того, что ассемблер не сделает это за вас, поэтому вам придется вручную записать это как 6
. В любой реальной программе, где вам иногда нужно вычислить факториал, если количество возможных входных данных невелико, то стоит подумать о том, чтобы просто выбрать из некоторых предварительно рассчитанных версий. Или, если это факториал постоянной времени сборки, вычисления во время выполнения были бы ужасной идеей.
Очевидно, вы делаете mov r2, #3
только для создания ввода для факторного кода, чтобы вы могли легко его протестировать, без функции ввода с клавиатуры и так далее. Но эта настройка ввода должна быть отделена от инструкций, которые фактически являются частью вычислений.
elaborate on what you mean
- код между метками start
и exit
в вашем вопросе можно заменить на MOV r3, # 6, если задача, которую вы решаете, состоит в том, чтобы вычислить 3! и поместите результат в r3. Конечно, совершенно очевидно, что такая задача не имеет большого смысла, но проблема в том, что вы так и не объяснили, что такое точно ваша задача (до сих пор не объяснили). Это немного плохой стиль — просто кинуть код в людей и оставить их гадать, так как они могут потратить свое время, гадая в неправильном направлении. К счастью, @PeterCordes нашел достаточно времени, чтобы подготовить часовую лекцию в качестве ответа.
.. охватывает практически любую архитектуру и ядро ARM :) К сожалению, некоторые люди просто не могут тратить столько времени, даже если они готовы помочь. Видите ли, теперь, когда вы упомянули Cortex-M3, ~ 80% его ответа оказались бесполезными (для вас). Впрочем, ничего, я просто ворчу, ведь на этом ресурсе были и будут гораздо худшие вопросы, чем ваши.
если вы используете .align, .balign или nops для изменения выравнивания цикла, это может повысить производительность. Часто, но не всегда flash медленнее, чем sram, поэтому, если вы бежите из sram (копируете и прыгаете), то питание ядра становится лучше. вы также можете подтвердить, что он использует расширения thumb2, когда это возможно. вы также можете проверить, есть ли в ядре предсказатель ветвления или нет, я не помню, есть ли он у cortex-m3 или нет.
всего несколько циклов, хотя вы можете не увидеть большого прироста производительности. какая конкретная часть этого cortex-m3 - это только часть представления о производительности, чип, типичные конструкции поставщиков чипов и т. д. являются фактором.
Можно было бы использовать что-то вроде этого: (при условии, что 32-битные регистры, где 12! - максимальное возможное значение), но Питер Кордес больше знаком с ARM (прошло 10 лет с тех пор, как я работал с ARM), и его ответ на основе кода хорош . Поиск в таблице, показанный ниже, должен быть самым быстрым, и он требует больше места, но не так много, поскольку диапазон равен 0! до 12! для 32-битных целых чисел без знака.
mov r2,#3 ;r2 = n
; ...
mov r3,#1
sub r2,#2
blo factx
mov r1,#(fact11-fact12)
mul r1,r2,r1 ; or better, use a left-shift by 2 or 3 and an assemble time static assert that fact11-fact12 == 4 or 8
adr r2,fact2
sub r2,r2,r1
mov r1,#2
b r2
fact12 mul r3,r1,r3
add r1,r1,#1
fact11 mul r3,r1,r3
add r1,r1,#1
mul r3,r1,r3
add r1,r1,#1
mul r3,r1,r3
add r1,r1,#1
mul r3,r1,r3
add r1,r1,#1
mul r3,r1,r3
add r1,r1,#1
mul r3,r1,r3
add r1,r1,#1
mul r3,r1,r3
add r1,r1,#1
mul r3,r1,r3
add r1,r1,#1
mul r3,r1,r3
add r1,r1,#1
fact2 mul r3,r1,r3
factx ... ;r3 = n!
или еще проще, поиск по таблице:
tblfac dcd 1,1,2,6,24,120,720,5040
dcd 40320,362880,3628800,39916800
dcd 479001600
; ...
mov r2,#3 ;r2 = n
adr r3,tblfac
ldr r3,[r3, r2, lsl #2] ;r3 = n!
mul
предположительно имеет задержку в несколько циклов, поэтому полное развертывание только с одним аккумулятором не устраняет это узкое место. Кроме того, ARM позволяет индексировать режимы адресации со смещенным индексом, поэтому что-то вроде ldr r3, [r3 + r2 lsl #2]
избегает add
.
@PeterCordes - Спасибо, я обновил свой ответ. В последний раз я работал с ARM около 10 лет назад, ARM v4. Я не знаю, сколько циклов mul потребуется на современных ARM. Поиск в таблице должен быть быстрым, но я не знаю, разрешено ли это для этой проблемы.
Что ж, в OP упоминается размер кода, поэтому похоже, что они заинтересованы в оптимизации компромисса между размером и производительностью. Поиск по таблице находится на одном конце спектра. Если они заботятся о больших входных данных, которые могут переполниться, возможно, LUT для каждого 4-го n
и цикл на младших битах. Но в любом случае, я догадался, что большинство ARM будут иметь конвейерные множители, но они могут быть на Cortex-M, который имеет либо 1-тактный (быстрее, больший кристалл), либо 32-тактный (меньший кристалл, итеративный) множители. Смотрите внизу моего ответа. Но я нашел числа для Cortex-A9, вокруг которых написал свой ответ...
Позволить ассемблеру вычислить множитель — это интересно, но плохо для производительности и размера кода по сравнению с sub r1, r2 lsl #2
. Однако я не думаю, что существует версия mul
или muls
, которая поддерживает немедленное. keil.com/support/man/docs/armasm/armasm_dom1361289882394.htm документирует только источник регистра. (И muls
составляет 2 байта в режиме Thumb, по сравнению с 4 для mul
).
@PeterCordes - я обновил свой ответ, чтобы устранить mul с константами. Поиск в таблице должен быть самым быстрым. Доступны ли ldr r3,[r3+r2 lsl #2] в режиме большого пальца?
Это неправильный синтаксис (в своем первом комментарии я исходил из памяти; я исправил код в вашем ответе с помощью редактирования), но да, в Thumb2 он доступен в виде 32-битной инструкции. Я проверял газом. 2-регистр без сдвига влево доступен как 16-битная инструкция. keil.com/support/man/docs/armasm/armasm_dom1361289874275.htm. О да, ARM позволяет вычитать регистр как часть режима адресации, так что это круто.
Вы можете сократить размер кода вдвое для версии с вычисляемым переходом, используя adds
и muls
вместо add и mul. (И у вас все еще есть глючный mul
-непосредственно в прологе, который должен быть сдвигом влево на 3. Мое изменение сделало бы это сдвигом влево на 2 для режима большого пальца, но все еще 3 для режима ARM, конечно, так что вы, вероятно, просто хочу изменить это на комментарий.)
@PeterCordes - спасибо. Единственный код ARM, который я смог найти в своей системе, — это небольшая многозадачная ОС, которую я написал 10 лет назад для повышения скорости, с ограничением, заключающимся в том, что каждая задача имеет свой приоритет (используется одно целое число с битами, установленными в 1, чтобы указать, что задача готова к выполнению). запустить, и не было времени нарезки). Я не был уверен в умножении со знаком.
Объединение r1
и r2
является очевидным решением, которое вы тоже получаете, когда читерите с компилятором c...
unsigned int foo(unsigned int a)
{
unsigned int res = 1;
while (a > 0) {
res *= a;
--a;
}
return res;
}
переводится как
subs r3, r0, #0
mov r0, #1
bxeq lr
1: mul r0, r3, r0
subs r3, r3, #1
bne 1b
bx lr
Часто размер кода и производительность являются компромиссом. Развертывание цикла часто повышает производительность (по крайней мере, для больших входных данных), но требует дополнительной логики вне цикла для обработки очистки и т.д.
(В исходном вопросе не указывалось ядро, и я ожидал, что даже недорогие процессоры будут иметь многотактную задержку mul
. Я нашел числа Cortex-M3 только после его написания.)
Ваш код, вероятно, будет узким местом в задержке целочисленного умножения. В отличие от add
, где результат будет готов в следующем цикле, mul
является сложным и требует нескольких циклов для получения результата.
(За исключением некоторых чипов с очень низкой тактовой частотой, например, Cortex-M3 имеет 1-тактовую инструкцию mul
. Но Cortex-M0/M0+/M23 доступны на выбор для этой инструкции составляет 1 или 32 такта! Медленная итерация = меньший размер кремния.)
Сам модуль выполнения умножения часто является конвейерным, поэтому несколько умножений независимый могут выполняться одновременно, но вашему факторному циклу требуется каждый результат умножения в качестве входных данных для следующей итерации. (Только для ядер с более высокой производительностью, а не для серии Cortex-M. 32-тактное умножение на медленных чипах Cortex-M является итеративным и, предположительно, неконвейерным, поэтому другое умножение не может начаться во время его работы, и в этом не будет никакой пользы. для раскрытия любого параллелизма на уровне инструкций, помимо уменьшения накладных расходов на цикл.)
Обратите внимание, что умножение является ассоциативным: 1 * 2 * 3
= 3 * 2 * 1
, поэтому мы можем отсчитывать от n
, как указывает ответ @ensc. Или (1*2) * (3*4)
= 1*2*3*4
.
Вместо этого мы могли бы выполнять 1 * 2 * ... * (n/2)
параллельно с n/2+1 * n/2+2 * n/2+3 * ... * n
, чередуя работу над этими двумя цепочками зависимостей. Или мы могли бы чередовать 1 * 3 * 5 * ... * n
с 2 * 4 * 6 * ... n-1
в цикле, который делал n -= 2
и вычислял n+1
из этого. (Затем, в конце, вы умножаете эти 2 произведения).
Это, очевидно, потребует большего размера кода, но может сильно повысить производительность.
Конечно, таблица поиска — еще один обходной путь. Если вас интересуют только входные данные, которые не переполняют 32-битный результат, это довольно маленькая таблица. Но это имеет значительную стоимость размера.
Даже на упорядоченном ЦП (где выполнение инструкций должно быть Начало в порядке программы) длительные инструкции, такие как загрузка с промахом кеша или умножение, могут быть разрешены полный не по порядку, поэтому, например. некоторые инструкции add
могли выполняться после запуска mul
, но до того, как результат mul
был записан обратно. Или даже запустить другую независимую mul
инструкцию в тени более ранней mul
задержки.
Я погуглил некоторые показатели производительности ARM, чтобы понять, что является типичным.
Например, Кортекс-А9 — это более старый довольно распространенный высокопроизводительный процессор ARMv7, который является суперскалярным (несколько инструкций за цикл) с участием внеочередным выполнением.
mul
«занимает» 2 цикла и имеет задержку результата 4 цикла. Они не объясняют, что они подразумевают под стоимостью без задержки. Возможно, это обратная пропускная способность исполнительного устройства, например, как часто вы можете запускать новую независимую операцию. Это неисправный ЦП, поэтому нет смысла останавливать другие инструкции на 2 цикла. В Раздел инструкций NEON SIMD они объясняют, как выглядит одно и то же число «циклов»:
This is the number of issue cycles the particular instruction consumes, and is the absolute minimum number of cycles per instruction if no operand interlocks are present.
(операнд блокируется = ожидание готовности входного операнда, если более ранняя инструкция еще не дала результат).
(Cortex-A9 поддерживает умножение упакованных целых чисел, поэтому для больших факториалов вы можете посмотреть на параллельное выполнение 4 умножений, начиная с одного вектора за 4 цикла, используя vmul.32 q1, q1, q2
. Или 2 за 2 цикла с 64-битными d
регистрами, но тогда вы бы нужно больше vadd
инструкций, и, в отличие от умножения, vadd.32
так же быстро работает со 128-битными q
регистрами, как и с 64-битными векторами. Таким образом, SIMD может дать вам вдвое большую пропускную способность, чем скаляр на Cortex-A9, если вы используете достаточное количество регистров, чтобы скрыть большая задержка.Но SIMD, вероятно, будет полезен только с n
настолько большим, что n!
переполняет 32-битное целое число, поэтому вы получаете результат по модулю 2^32.)
mul
— это 32x32 => 32-битное умножение. На Cortex-A9 он имеет пропускную способность 2c и задержку 4c.
(muls
— это 16-битная инструкция в режиме большого пальца, и ее следует отдавать предпочтение, если только вам не нужно стирать флаги. mul
в режиме большого пальца доступно только в ARMv6T2 и более поздних версиях.)
smulbb
- это 16x16 => 32-битное умножение со знаком, который считывает только младшую половину своих входных данных, но имеет Пропускная способность 1c и задержка 3c на A9. (BB = дно, дно. Доступны и другие комбинации, а также умножение-накопление и различные причудливые штуки.)
Не существует 2-байтовой версии Thumb для smulxy
, так что это хуже по размеру кода, чем muls
.
К сожалению, smulxy
недоступен в версии без знака, поэтому диапазон входных данных, с которыми мы можем его использовать, ограничивается положительным int16_t
, а не uint16_t
.
Но если нас интересует только случай, когда окончательный 32-битный результат не переполняется, мы можем расположить наш порядок операций так, чтобы последнее умножение имело 2 входа одинаковой величины (оба большие 16-битные числа). то есть как можно ближе к sqrt(n!)
. Так, например. произведение шансов и четных было бы разумным, но (n-1)! * n
было бы наихудшим случаем, потому что это потребовало бы (n-1)!
, чтобы уместиться в 16 бит. На самом деле в худшем случае будет обратный отсчет от n
, поэтому последнее умножение на 3, а затем на 2. Мы могли бы в частном случае умножить на 2 на сдвиг влево...
Собирая эти части вместе, обратите внимание, что умножение на 1
не является операцией (за исключением smulbb
, где оно усекает ввод до 16 бит). Таким образом, мы можем развернуть таким образом, чтобы он останавливался после умножения на 1 или 2 в зависимости от того, является ли ввод нечетным или четным.
Таким образом, вместо того, чтобы знать, что нечетно, а что четно, мы просто имеем lo (начиная с n-1
) и hi (начиная с n
).
;; UNTESTED, but it does assemble with the GNU assembler, after sed -i 's/;/@/' arm-fact.S
;; and replacing THUMB with
; .thumb
; .syntax unified
THUMB
;; Input: n in r0. (n is signed positive, otherwise we return n.)
;; Output: n! in r0.
;; clobbers: r1, r2, r3
;; pre-conditions: n! < 2^31. Or maybe slightly lower.
fact:
subs r3, r0, #3 ; r3 = lo = n-3 (first multiplier for loprod)
bls .Ltiny_input
subs r2, r0, #2 ; r2 = hi = n-2 (first multiplier for hiprod)
subs r1, r0, #1 ; r1 = loprod = n-1
; r0 = hiprod = n
.Lloop: ; do {
smulbb r0,r0, r2 ; hiprod *= hi
subs r2, #2 ; hi -= 2 for next iter
smulbb r1,r1, r3
subs r3, #2 ; lo -= 2 for next iter
bgt .Lloop ; while((lo-=2) > 0); signed condition
; r3 = 0 or -1, r2 = 1 or 0. The last multiplies were:
; hiprod *= 2 and loprod *= 1 for even n
; or hiprod *= 3 and loprod *= 2 for odd n
; muls r0, r1
smulbb r0,r0, r1 ; return hiprod *= loprod
bx lr ; or inline this
.Ltiny_input: ; alternate return path for tiny inputs
; r0 = n. flags still set from n - 3
IT eq ; GAS insists on explicit IT for thumb mode
moveq r0, #6 ; 3! = 6, else n! = n for smaller n=1 or 2.
; 0! = 1 case is not handled, nor are negative inputs
bx lr
(.L в имени метки делает ее локальной меткой, которая не отображается в объектном файле, по крайней мере, в синтаксисе GAS. Возможно, не в ARMASM, если вы используете этот ассемблер.)
Сборка ARM позволяет вам не указывать место назначения, если оно совпадает с первым источником, для некоторых инструкций, таких как subs
, но не smulbb
. Вы можете написать это как subs r2, r2, #2
каждый раз, если хотите.
Вы можете использовать muls r0, r1
для конечного продукта, потому что финальный hiprod
немного выше, чем loprod
. Продукт может не переполниться, даже если hiprod
> max int16_t. Это также сэкономит 2 байта кода, но добавит 1 цикл задержки на Cortex-A9. (Кстати, ARMv6 исправил «непредсказуемый результат» со странностью mul d,d, src
, а ваш код использовал 32-битные инструкции Thumb2, поэтому он в любом случае работает только на ARMv6T2 и выше.)
С 2 аккумуляторами для продуктов это может работать с 2 умножениями на 3 цикла на Cortex-A9., в значительной степени зависит от микроархитектуры процессора и от того, сможет ли его внешний интерфейс справиться с этой задачей. На ARM в порядке я бы беспокоился о том, что он сможет запустить другие инструкции до завершения умножения.
Возможно, было бы лучше потратить 2 дополнительных байта на sub
вместо subs
, чтобы мы могли вычислить флаги на пару инструкций раньше ветки, возможно, уменьшить штраф за неправильное предсказание ветвления и избежать остановок процессоров в порядке. smulbb
не касается флагов, поэтому мы можем сначала сделать loprod
и hi
не касаться флагов.
.loop: ; do {
smulbb r1, r3 ; loprod *= lo
subs r3, #2 ; lo -= 2 for next iter, and set flags
smulbb r0, r2 ; hiprod *= hi
sub r2, #2 ; hi -= 2 for next iter (no flags)
bgt .loop ; while((lo-=2) >= 0);
Обратите внимание, что мы модифицируем r3
и r2
правое послеsmulbb
считывает их, избегая создания задержки для зависимости данных от упорядоченных микросхем.
Вы используете режим Thumb и оптимизируете размер кода, поэтому важно знать, какие формы каких инструкций могут использовать 2-байтовую/16-битную кодировку, а какие доступны только как 32-битные кодировки Thumb2.
subs Rd, Rn, #imm
может быть закодировано как 16-битная инструкция Thumb для imm=0..7 (3-битное немедленное). Или с тем же регистром, что и источник и пункт назначения, для imm=0..255. Так что мои инструкции по копированию и подписке компактны.
Без установки флага sub
не может быть 16-битной инструкцией, кроме как внутри IT-блока или с SP
в качестве операнда.
Предсказанные инструкции в режиме Thumb, как и moveq r0, #6
, требуют, чтобы ассемблер использовал IT
инструкция для введения предикации для следующих до 4 инструкций. В режиме ARM верхние 4 бита каждой инструкции сигнализируют о предикации. (Если вы не используете суффикс, ассемблер кодирует его как ВСЕГДА, т.е. без предикатов.)
Мы могли бы обработать случай n==0
с помощью еще 4 или 6 байтов., с cmp r0,#0
/ moveq r0, #1
. Возможно, уменьшим его до 4 байт, если поместим tst/mov в тот же IT-блок. IT не фиксирует фактическое состояние флага, а моментальные снимки предиката, поэтому инструкции по установке флага внутри IT-блока могут влиять на последующие инструкции в том же блоке. (Я думаю, что это правильно, но я не уверен на 100%).
tiny_input: ; r0 = n, flags set according to n-3
ITET EQ
moveq r0, #6
cmpne r0, #0
moveq r0, #1
Или есть 16-битный cbnz
, чтобы условно перепрыгнуть через mov r0, #1
. Но цель ветвления должна быть от 4 до 130 байт после cbnz
, поэтому мы, по-видимому, не можем перепрыгнуть через одну 16-битную инструкцию!
$ arm-none-eabi-gcc -g -c -mcpu=cortex-a9 arm-fact.S
$ arm-none-eabi-objdump -drwC arm-fact.o
arm-fact.o: file format elf32-littlearm
Disassembly of section .text:
00000000 <fact>:
0: 1ec3 subs r3, r0, #3
2: d90b bls.n 1c <.tiny_input>
4: 1e82 subs r2, r0, #2
6: 1e41 subs r1, r0, #1
00000008 <.loop>:
8: fb10 f002 smulbb r0, r0, r2
c: 3a02 subs r2, #2
e: fb11 f103 smulbb r1, r1, r3
12: 3b02 subs r3, #2
14: dcf8 bgt.n 8 <.loop>
16: fb10 f001 smulbb r0, r0, r1
1a: 4770 bx lr
0000001c <.tiny_input>:
1c: bf08 it eq
1e: 2006 moveq r0, #6
20: 4770 bx lr
Так что это 0x22 байта для этой функции. (Или 0x26, если мы хотим обрабатывать 0! = 1
.)
Это больше, чем ваша версия (ваше количество байтов включает в себя некоторые константы в памяти и инструкции mov
для создания ввода), но теоретически может быть лучше, чем в два раза быстрее для больших входных данных, на процессорах с конвейерными множителями). И, возможно, намного быстрее для входов от 1 до 3, где он просто разветвляется один раз и выдает результат.
У вас, вероятно, нет ничего похожего на Cortex-A9, потому что ваши 1,1 микросекунды = 22 тактовых цикла означают тактовую частоту 20 МГц., тогда как Cortex-A9 был доступен в диапазоне частот от 0,8 до 2 ГГц.
Так что, может быть, у вас есть гораздо более простое ядро, такое как Кортекс М3? M3 поддерживает инструкцию mul
и режим Thumb2. И википедия говорит, что его умножение равно 1 циклу! Так что это странно, я удивлен, что у него такой эффективный множитель. Или просто он работает так медленно, что есть время для большого количества задержек ворот на 1 этапе, а это всего лишь 3-этапный конвейер.
сабы и мулы однотактные на Cortex-M3. Я не нашел показателей производительности на ветвях, но они распространены, поэтому я предполагаю, что это, вероятно, 1 цикл и не вызывает большого пузыря выборки (если правильно предсказано...). В руководстве Cortex-M3 HTML есть раздел Целевая переадресация филиала, который, по-видимому, посвящен уменьшению всплывающего окна выборки.
Его таблица синхронизации команд показывает, что b<cond>
стоит 1 цикл для невыполненного или 2 цикла для взятого. (1 для ответвления, 1 для перезагрузки трубопровода после немедленного перемещения.). Таким образом, взятые ветки имеют медленный по сравнению с sub/mul, и развертывание было бы полезно, поэтому мой код выше должен работать хорошо. (Но несколько аккумуляторов продуктов не нужны, поэтому его можно упростить).
;; UNTESTED
THUMB
;; Input: n in r0. (n is signed positive, otherwise we return n.)
;; Output: n! in r0.
;; clobbers: r1
fact:
subs r1, r0, #1 ; i = n-1
bls .Ltiny_input ; jump if n<=1
.Lloop: ; do {
muls r0, r1 ; prod *= i
subs r1, #1 ; --i
bgt .Lloop ; while(--i > 0); signed condition
; r1 = 0, r0 = n!
; last multiply was a redundant prod *= 1 but avoiding that would take a cmp
.Ltiny_input: ; alternate return path for tiny inputs
; 0! = 1 case is not handled, nor are negative inputs
bx lr ; or inline this
Я думаю, это самое малое, что мы можем себе позволить. Цикл состоит из 3 инструкций и, вероятно, стоит 4 такта на итерацию (1 + 1 + 2, взятая ветвь стоит 2 такта).
00000000 <fact>:
0: 1e41 subs r1, r0, #1
2: d902 bls.n a <fact+0xa>
4: 4348 muls r0, r1
6: 3901 subs r1, #1
8: dcfc bgt.n 4 <fact+0x4>
a: 4770 bx lr # don't count this if inlining
Итак, это 0xa = 10 байт, не считая инструкции возврата bx lr
.
Мы могли бы обработать случай 0! = 1
с блоком IT
после первого subs
, до ветвью, так что мы все еще можем перейти сразу после цикла (вместо отдельного блока, как в моей версии Cortex-A9). Впрочем, вы можете использовать и этот трюк.
subs r1, r0, #1 ; i = n-1
it lt
movlt r0, #1 ; n = 1 for n<1
bls .Ltiny_input ; return n if n was <=1
Если бы нам нужно было больше диапазона для ветвления, мы могли бы использовать itt ls
/ movls r0, #1
, чтобы ветвь находилась внутри блока IT (где инструкции ветвления могут использовать кодировку, которая тратит больше битов на смещение, а не на предикат). Но в данном случае это небольшой диапазон, поэтому я решил оставить r0
без изменений в случае r0 == 1
. Я не знаю, есть ли какие-либо процессоры, где более эффективно или с меньшей задержкой предикатная инструкция должна быть NOP, а не выполняться, но они могут быть.
Без развертывания добавление cmp
в цикл, чтобы избежать последней *=1
итерации, стоило бы нам дополнительного цикла на итерацию (4 цикла вместо 3), поэтому окупайтесь только с помощью n=2
или, может быть, n=3
.
Развертывание может значительно ускорить работу с более крупными входными данными: от 1 мул за 3 цикла до асимптотического приближения к 1 мул за 2 цикла. (sub + mul + амортизируемые служебные данные цикла). Я не вижу никакого способа избежать такой инструкции, как sub
или mov
, для создания отдельного ввода для каждого mul
, за исключением жесткого кодирования последовательности особого случая для каждого n
(например, *2 * 4
= *8
= сдвиг влево на 3), когда вы могли бы вместо этого просто жестко закодируйте ответ.
Спасибо за ответ, и вы правы, я использую Cortex M3, извините, что не указал ранее, можете ли вы как-нибудь объяснить свой ответ в контексте использования Cortex M3, поскольку я все еще не совсем понимаю, что вы написали .
@ Hysteria103: большая часть этого вообще не применима к Cortex M3. Его множитель не является конвейерным (и muls
не медленнее, чем smulbb
), а ЦП не является суперскалярным, так что в создании ILP нет смысла. Все, что вы можете сделать, это уменьшить количество инструкций без умножения, например. путем развертывания, чтобы уменьшить накладные расходы на цикл, и, как показывает ensc, с помощью subs
для установки флагов во время обратного отсчета.
@Hysteria103: добавил к моему ответу раздел Cortex-M3, код уменьшился до 0xa = 10 байт: PI не удосужился найти хороший компромисс между производительностью и размером для M3, где взятые ветки стоят столько же, сколько тело цикла . Вероятно, мы можем использовать ту же i-=2
развертку, что и мой цикл с двумя аккумуляторами, возможно, с более простой настройкой. Или, может быть, что-то вроде обработки нечетных/четных n
с условным mul
для начала, чтобы мы знали, чем закончится цикл... Много забавных вещей, которые вы могли бы сделать, в зависимости от того, какой компромисс между размером и производительностью вы ищете для.
Если вы хотите краткое изложение, перейдите к концу.
Я запустил это на синей таблетке STM32, STM32F103C8T6.
Определенно ожидайте, что результаты изменятся с разными чипами, даже если они имеют одинаковую версию cortex-m3, поскольку процессор - это одно, а то, что его питает, и как - другое, и это зависит от поставщика. Также иногда поставщик чипа может компилировать ядро по-разному, иногда они могут иметь многоцикловые умножения, чтобы сэкономить на площади чипа, некоторые ядра они могут выбирать между выборкой 16 бит за раз или 32. Тестовые тесты часто легко испортить, поэтому возьмите их. недоверчиво.
Я видел, что выполнение в sram обычно происходит быстрее, чем из flash. ST хотя, иногда нет, я не думаю, что на этих древних cortex-m3s у них есть свой (инструкционный) кеш с каким-то причудливым именем. Более новые делают, и вы не можете отключить это.
Другие поставщики чипов не имеют этого и будут для ядер, которые его поддерживают, внедрять кеши рук, а не свои собственные (или не иметь ни того, ни другого). Возможно, поэтому первые два эксперимента ниже выполняются в разное время (две цифры впереди шестнадцатеричные, таймер systick считает, адрес systick cvr передается в r0. Вы можете видеть, что я использовал nop для изменения выравнивания цикла. Документация по руке не указывала в обычном месте, что cortex-m3 извлекает полуслова или слова, но в документации ST, когда речь идет о чем-то другом, говорится о выборке слов Ваш цикл из четырех инструкций состоит из двух слов, но выровнен не по границе слова, что означает, что это должен получить три слова за цикл. Если эти четыре слова выровнены, то ему нужно получить два слова за цикл, что позволит Питеру или кому-то еще подсчитать инструкции для этого/вашего кода. Я уверен, что это фактор, но, возможно, есть другие, наверное, нет.
Для этого чипа запуск с флешки происходит намного быстрее. Вы можете увидеть влияние отключения предварительной выборки ST и добавления состояний ожидания.
000 Zero wait state, if 0 < SYSCLK≤ 24 MHz
001 One wait state, if 24 MHz < SYSCLK ≤ 48 MHz
010 Two wait states, if 48 MHz < SYSCLK ≤ 72 MHz
Итак, пока я работаю от внутренних часов 8 МГц, здесь есть два измерения: одно — это количество часов, необходимое для выполнения чего-либо, если мы утроим sysclk до 24 МГц, количество часов не должно измениться. Длительность настенных часов каждого цикла sysclk составляет треть времени, поэтому время настенных часов быстрее. Производительность в реальном времени лучше. Следуя этим правилам, поднимитесь на один шаг выше 24 МГц, и теперь вы добавляете состояние ожидания, и ваш код теперь снова замедляется. Поскольку количество системных часов для запуска кода теперь замедлилось. Теперь, если вы удвоите это до 48 МГц, преодолеет ли это состояние ожидания? Вероятно, но для каждой программы/цикла есть точка между 24 МГц + чуть-чуть и 48 МГц догоняет производительность до 24 МГц. И 48 МГц плюс немного, теперь вы снова замедляетесь, и где-то между 48 МГц плюс немного и 72 МГц мы надеемся догнать и превзойти производительность 48 МГц.
Точно так же, как флэш-память не справляется, у других периферийных устройств есть правила, особенно с этими старыми чипами, такими как многие из чипов на базе cortex-m3, есть другие обрывы производительности, с которых вы падаете, некоторые периферийные устройства не могут работать так же быстро, как любой sysclk, поэтому вы можете есть какая-то другая скорость X, где вы находитесь на максимальной скорости для одного / некоторых из ваших периферийных устройств или периферийных шин, и X + чуть-чуть вам нужно вдвое сократить тактовую частоту, так как это ваш наименьший делитель, теперь ваши периферийные устройства и / или их шины теперь наполовину скорость, поэтому производительность вашего кода падает с обрыва, возможно, хуже, чем наполовину. Этот твой код не касается периферийного устройства. Он использует умножение, что опасно для производительности, но для cortex-m3 я не видел, чтобы была опция времени компиляции для одного цикла по сравнению с другим, он просто сказал один цикл.
Питер рассмотрел очевидную оптимизацию, когда вы считаете до некоторого числа, если набор инструкций позволяет, и ваш код, что он делает в этом случае, потому что a * b * c = c * b * a, поэтому вы хотите считать в обратном порядке. и используйте флаги для сравнения с нулем или плюсом минус, если это плавает в вашей лодке, а не увеличивайте, а затем выполняйте сравнение перед условным выражением. Когда вы пропустите до конца, вы увидите, что это было быстрее (меньше часов).
У M3 нет кешей, у m4 и m7 есть. Таким образом, запуск этого кода с его небольшим циклом потребует многократного выполнения цикла и времени, чтобы увидеть влияние кэширования, выравнивания строк кэша и тому подобного. Но для m3 достаточно один раз (если у чипа нет скрытого кеша, которым вы не можете управлять).
Меня действительно интересует только петля здесь, поскольку она имеет наибольший потенциал для похитителей циклов. Проверка/ограничение ввода, проверка ярлыков, поиск переполнения при умножении и т. д., а не то, о чем беспокоится этот ответ.
Я рекомендую вам поискать в Google книги Майкла Абраша. Например, Zen of Assembly, копию которого вы можете собрать на GitHub. Я читал ее, когда она вышла, и с тех пор я в значительной степени использовал то, чему я научился, отлаживая микросхемы, инструменты, ломая вещи, улучшая производительность и т. д. 8088/86 устарел, когда он вышел, и если вы думаете, что это книга по x86 вы совершенно упускаете суть. Например, мое предположение о sram будет быстрее, здесь этого не произошло. Я также пробовал такие вещи, как добавление nops (дополнительных инструкций) внутри цикла, хотите верьте, хотите нет, бывают случаи, когда это может ускорить выполнение цикла. Эти короткие конвейеры, небольшие процессоры предварительной выборки, хотя обычно это не так.
Иногда вы можете получить бесплатные инструкции в цикле, количество часов такое же, даже при большем количестве инструкций. Например, если бы у этого было многотактовое умножение, в зависимости от того, сколько часов и в зависимости от того, какие регистры/ресурсы вы касаетесь, вы можете получить некоторые бесплатные инструкции в этом цикле. Похоже, что это умножение одного цикла, поэтому здесь нельзя на это надеяться.
Кроме того, есть материалы о трубопроводах, которые вы читали в учебниках Паттерсона и Хеннесси. Выбор регистров может повлиять на производительность. Порядок инструкций, если вы можете функционально перестроить инструкции и т. д.
Заметки, сделанные при выполнении простых экспериментов
15
20000018 <fact>:
20000018: b430 push {r4, r5}
2000001a: 2100 movs r1, #0
2000001c: 2203 movs r2, #3
2000001e: 2301 movs r3, #1
20000020: 6804 ldr r4, [r0, #0]
20000022 <fact_loop>:
20000022: 3101 adds r1, #1
20000024: 434b muls r3, r1
20000026: 4291 cmp r1, r2
20000028: d4fb bmi.n 20000022 <fact_loop>
2000002a: 6805 ldr r5, [r0, #0]
2000002c: 1b60 subs r0, r4, r5
2000002e: bc30 pop {r4, r5}
20000030: 4770 bx lr
12
20000018 <fact>:
20000018: b430 push {r4, r5}
2000001a: 2100 movs r1, #0
2000001c: 2203 movs r2, #3
2000001e: 2301 movs r3, #1
20000020: 46c0 nop ; (mov r8, r8)
20000022: 6804 ldr r4, [r0, #0]
20000024 <fact_loop>:
20000024: 3101 adds r1, #1
20000026: 434b muls r3, r1
20000028: 4291 cmp r1, r2
2000002a: d4fb bmi.n 20000024 <fact_loop>
2000002c: 6805 ldr r5, [r0, #0]
2000002e: 1b60 subs r0, r4, r5
20000030: bc30 pop {r4, r5}
20000032: 4770 bx lr
15
20000018 <fact>:
20000018: b430 push {r4, r5}
2000001a: 2100 movs r1, #0
2000001c: 2203 movs r2, #3
2000001e: 2301 movs r3, #1
20000020: 46c0 nop ; (mov r8, r8)
20000022: 46c0 nop ; (mov r8, r8)
20000024: 6804 ldr r4, [r0, #0]
20000026 <fact_loop>:
20000026: 3101 adds r1, #1
20000028: 434b muls r3, r1
2000002a: 4291 cmp r1, r2
2000002c: d4fb bmi.n 20000026 <fact_loop>
2000002e: 6805 ldr r5, [r0, #0]
20000030: 1b60 subs r0, r4, r5
20000032: bc30 pop {r4, r5}
20000034: 4770 bx lr
20000036: 46c0 nop ; (mov r8, r8)
12
20000018 <fact>:
20000018: b430 push {r4, r5}
2000001a: 2100 movs r1, #0
2000001c: 2203 movs r2, #3
2000001e: 2301 movs r3, #1
20000020: 46c0 nop ; (mov r8, r8)
20000022: 46c0 nop ; (mov r8, r8)
20000024: 46c0 nop ; (mov r8, r8)
20000026: 6804 ldr r4, [r0, #0]
20000028 <fact_loop>:
20000028: 3101 adds r1, #1
2000002a: 434b muls r3, r1
2000002c: 4291 cmp r1, r2
2000002e: d4fb bmi.n 20000028 <fact_loop>
20000030: 6805 ldr r5, [r0, #0]
20000032: 1b60 subs r0, r4, r5
20000034: bc30 pop {r4, r5}
20000036: 4770 bx lr
55
20000018 <fact>:
20000018: b430 push {r4, r5}
2000001a: 2100 movs r1, #0
2000001c: 220b movs r2, #11
2000001e: 2301 movs r3, #1
20000020: 6804 ldr r4, [r0, #0]
20000022 <fact_loop>:
20000022: 3101 adds r1, #1
20000024: 434b muls r3, r1
20000026: 4291 cmp r1, r2
20000028: d4fb bmi.n 20000022 <fact_loop>
2000002a: 6805 ldr r5, [r0, #0]
2000002c: 1b60 subs r0, r4, r5
2000002e: bc30 pop {r4, r5}
20000030: 4770 bx lr
20000032: bf00 nop
42
20000018 <fact>:
20000018: b430 push {r4, r5}
2000001a: 2100 movs r1, #0
2000001c: 220b movs r2, #11
2000001e: 2301 movs r3, #1
20000020: 46c0 nop ; (mov r8, r8)
20000022: 6804 ldr r4, [r0, #0]
20000024 <fact_loop>:
20000024: 3101 adds r1, #1
20000026: 434b muls r3, r1
20000028: 4291 cmp r1, r2
2000002a: d4fb bmi.n 20000024 <fact_loop>
2000002c: 6805 ldr r5, [r0, #0]
2000002e: 1b60 subs r0, r4, r5
20000030: bc30 pop {r4, r5}
20000032: 4770 bx lr
41
20000018 <fact>:
20000018: b430 push {r4, r5}
2000001a: 210b movs r1, #11
2000001c: 2301 movs r3, #1
2000001e: 6804 ldr r4, [r0, #0]
20000020 <fact_loop>:
20000020: 434b muls r3, r1
20000022: 3901 subs r1, #1
20000024: d1fc bne.n 20000020 <fact_loop>
20000026: 6805 ldr r5, [r0, #0]
20000028: 1b60 subs r0, r4, r5
2000002a: bc30 pop {r4, r5}
2000002c: 4770 bx lr
2000002e: bf00 nop
42
20000018 <fact>:
20000018: b430 push {r4, r5}
2000001a: 210b movs r1, #11
2000001c: 2301 movs r3, #1
2000001e: 46c0 nop ; (mov r8, r8)
20000020: 6804 ldr r4, [r0, #0]
20000022 <fact_loop>:
20000022: 434b muls r3, r1
20000024: 3901 subs r1, #1
20000026: d1fc bne.n 20000022 <fact_loop>
20000028: 6805 ldr r5, [r0, #0]
2000002a: 1b60 subs r0, r4, r5
2000002c: bc30 pop {r4, r5}
2000002e: 4770 bx lr
41
20000018 <fact>:
20000018: b430 push {r4, r5}
2000001a: 210b movs r1, #11
2000001c: 2301 movs r3, #1
2000001e: 46c0 nop ; (mov r8, r8)
20000020: 46c0 nop ; (mov r8, r8)
20000022: 6804 ldr r4, [r0, #0]
20000024 <fact_loop>:
20000024: 434b muls r3, r1
20000026: 3901 subs r1, #1
20000028: d1fc bne.n 20000024 <fact_loop>
2000002a: 6805 ldr r5, [r0, #0]
2000002c: 1b60 subs r0, r4, r5
2000002e: bc30 pop {r4, r5}
20000030: 4770 bx lr
20000032: bf00 nop
FLASH ACR 0x30
2d
08000020 <fact>:
8000020: b430 push {r4, r5}
8000022: 210b movs r1, #11
8000024: 2301 movs r3, #1
8000026: 6804 ldr r4, [r0, #0]
08000028 <fact_loop>:
8000028: 434b muls r3, r1
800002a: 3901 subs r1, #1
800002c: d1fc bne.n 8000028 <fact_loop>
800002e: 6805 ldr r5, [r0, #0]
8000030: 1b60 subs r0, r4, r5
8000032: bc30 pop {r4, r5}
8000034: 4770 bx lr
2d
08000020 <fact>:
8000020: b430 push {r4, r5}
8000022: 210b movs r1, #11
8000024: 2301 movs r3, #1
8000026: 46c0 nop ; (mov r8, r8)
8000028: 6804 ldr r4, [r0, #0]
0800002a <fact_loop>:
800002a: 434b muls r3, r1
800002c: 3901 subs r1, #1
800002e: d1fc bne.n 800002a <fact_loop>
8000030: 6805 ldr r5, [r0, #0]
8000032: 1b60 subs r0, r4, r5
8000034: bc30 pop {r4, r5}
8000036: 4770 bx lr
FLASH_ACR 0x00
2d
08000020 <fact>:
8000020: b430 push {r4, r5}
8000022: 210b movs r1, #11
8000024: 2301 movs r3, #1
8000026: 46c0 nop ; (mov r8, r8)
8000028: 6804 ldr r4, [r0, #0]
0800002a <fact_loop>:
800002a: 434b muls r3, r1
800002c: 3901 subs r1, #1
800002e: d1fc bne.n 800002a <fact_loop>
8000030: 6805 ldr r5, [r0, #0]
8000032: 1b60 subs r0, r4, r5
8000034: bc30 pop {r4, r5}
8000036: 4770 bx lr
FLASH_ACR 0x02
5e
08000020 <fact>:
8000020: b430 push {r4, r5}
8000022: 210b movs r1, #11
8000024: 2301 movs r3, #1
8000026: 6804 ldr r4, [r0, #0]
08000028 <fact_loop>:
8000028: 434b muls r3, r1
800002a: 3901 subs r1, #1
800002c: d1fc bne.n 8000028 <fact_loop>
800002e: 6805 ldr r5, [r0, #0]
8000030: 1b60 subs r0, r4, r5
8000032: bc30 pop {r4, r5}
8000034: 4770 bx lr
5f
08000020 <fact>:
8000020: b430 push {r4, r5}
8000022: 210b movs r1, #11
8000024: 2301 movs r3, #1
8000026: 46c0 nop ; (mov r8, r8)
8000028: 6804 ldr r4, [r0, #0]
0800002a <fact_loop>:
800002a: 434b muls r3, r1
800002c: 3901 subs r1, #1
800002e: d1fc bne.n 800002a <fact_loop>
8000030: 6805 ldr r5, [r0, #0]
8000032: 1b60 subs r0, r4, r5
8000034: bc30 pop {r4, r5}
8000036: 4770 bx lr
FLASH_ACR 0x32
41
08000020 <fact>:
8000020: b430 push {r4, r5}
8000022: 210b movs r1, #11
8000024: 2301 movs r3, #1
8000026: 6804 ldr r4, [r0, #0]
08000028 <fact_loop>:
8000028: 434b muls r3, r1
800002a: 3901 subs r1, #1
800002c: d1fc bne.n 8000028 <fact_loop>
800002e: 6805 ldr r5, [r0, #0]
8000030: 1b60 subs r0, r4, r5
8000032: bc30 pop {r4, r5}
8000034: 4770 bx lr
41
08000020 <fact>:
8000020: b430 push {r4, r5}
8000022: 210b movs r1, #11
8000024: 2301 movs r3, #1
8000026: 46c0 nop ; (mov r8, r8)
8000028: 6804 ldr r4, [r0, #0]
0800002a <fact_loop>:
800002a: 434b muls r3, r1
800002c: 3901 subs r1, #1
800002e: d1fc bne.n 800002a <fact_loop>
8000030: 6805 ldr r5, [r0, #0]
8000032: 1b60 subs r0, r4, r5
8000034: bc30 pop {r4, r5}
8000036: 4770 bx lr
PUT32(FLASH_ACR,0x3A);
41
08000020 <fact>:
8000020: b430 push {r4, r5}
8000022: 210b movs r1, #11
8000024: 2301 movs r3, #1
8000026: 6804 ldr r4, [r0, #0]
08000028 <fact_loop>:
8000028: 434b muls r3, r1
800002a: 3901 subs r1, #1
800002c: d1fc bne.n 8000028 <fact_loop>
800002e: 6805 ldr r5, [r0, #0]
8000030: 1b60 subs r0, r4, r5
8000032: bc30 pop {r4, r5}
8000034: 4770 bx lr
...
41
08000020 <fact>:
8000020: b430 push {r4, r5}
8000022: 210b movs r1, #11
8000024: 2301 movs r3, #1
8000026: 46c0 nop ; (mov r8, r8)
8000028: 6804 ldr r4, [r0, #0]
0800002a <fact_loop>:
800002a: 434b muls r3, r1
800002c: 3901 subs r1, #1
800002e: d1fc bne.n 800002a <fact_loop>
8000030: 6805 ldr r5, [r0, #0]
8000032: 1b60 subs r0, r4, r5
8000034: bc30 pop {r4, r5}
8000036: 4770 bx lr
flash acr 0x32
4c
08000020 <fact>:
8000020: b430 push {r4, r5}
8000022: 210b movs r1, #11
8000024: 2301 movs r3, #1
8000026: 6804 ldr r4, [r0, #0]
08000028 <fact_loop>:
8000028: 46c0 nop ; (mov r8, r8)
800002a: 434b muls r3, r1
800002c: 3901 subs r1, #1
800002e: d1fb bne.n 8000028 <fact_loop>
8000030: 6805 ldr r5, [r0, #0]
8000032: 1b60 subs r0, r4, r5
8000034: bc30 pop {r4, r5}
8000036: 4770 bx lr
4c
08000020 <fact>:
8000020: b430 push {r4, r5}
8000022: 210b movs r1, #11
8000024: 2301 movs r3, #1
8000026: 46c0 nop ; (mov r8, r8)
8000028: 6804 ldr r4, [r0, #0]
0800002a <fact_loop>:
800002a: 46c0 nop ; (mov r8, r8)
800002c: 434b muls r3, r1
800002e: 3901 subs r1, #1
8000030: d1fb bne.n 800002a <fact_loop>
8000032: 6805 ldr r5, [r0, #0]
8000034: 1b60 subs r0, r4, r5
8000036: bc30 pop {r4, r5}
8000038: 4770 bx lr
flash acr 0x30
38
08000020 <fact>:
8000020: b430 push {r4, r5}
8000022: 210b movs r1, #11
8000024: 2301 movs r3, #1
8000026: 6804 ldr r4, [r0, #0]
08000028 <fact_loop>:
8000028: 46c0 nop ; (mov r8, r8)
800002a: 434b muls r3, r1
800002c: 3901 subs r1, #1
800002e: d1fb bne.n 8000028 <fact_loop>
8000030: 6805 ldr r5, [r0, #0]
8000032: 1b60 subs r0, r4, r5
8000034: bc30 pop {r4, r5}
8000036: 4770 bx lr
3b
0800002c <fact_loop>:
800002c: d002 beq.n 8000034 <fact_done>
800002e: 434b muls r3, r1
8000030: 3901 subs r1, #1
8000032: e7fb b.n 800002c <fact_loop>
08000034 <fact_done>:
8000034: 6805 ldr r5, [r0, #0]
8000036: 1b60 subs r0, r4, r5
8000038: bc30 pop {r4, r5}
800003a: 4770 bx lr
38
08000020 <fact>:
8000020: b430 push {r4, r5}
8000022: 2100 movs r1, #0
8000024: 220b movs r2, #11
8000026: 2301 movs r3, #1
8000028: 6804 ldr r4, [r0, #0]
0800002a <fact_loop>:
800002a: 3101 adds r1, #1
800002c: 434b muls r3, r1
800002e: 4291 cmp r1, r2
8000030: d4fb bmi.n 800002a <fact_loop>
8000032: 6805 ldr r5, [r0, #0]
8000034: 1b60 subs r0, r4, r5
8000036: bc30 pop {r4, r5}
8000038: 4770 bx lr
38
08000020 <fact>:
8000020: b430 push {r4, r5}
8000022: 2100 movs r1, #0
8000024: 220b movs r2, #11
8000026: 2301 movs r3, #1
8000028: 46c0 nop ; (mov r8, r8)
800002a: 6804 ldr r4, [r0, #0]
0800002c <fact_loop>:
800002c: 3101 adds r1, #1
800002e: 434b muls r3, r1
8000030: 4291 cmp r1, r2
8000032: d4fb bmi.n 800002c <fact_loop>
8000034: 6805 ldr r5, [r0, #0]
8000036: 1b60 subs r0, r4, r5
8000038: bc30 pop {r4, r5}
800003a: 4770 bx lr
2d
08000020 <fact>:
8000020: b430 push {r4, r5}
8000022: 210b movs r1, #11
8000024: 2301 movs r3, #1
8000026: 6804 ldr r4, [r0, #0]
08000028 <fact_loop>:
8000028: 434b muls r3, r1
800002a: 3901 subs r1, #1
800002c: d1fc bne.n 8000028 <fact_loop>
800002e: 6805 ldr r5, [r0, #0]
8000030: 1b60 subs r0, r4, r5
8000032: bc30 pop {r4, r5}
8000034: 4770 bx lr
Обратите внимание, что я изменил количество петель, входное значение с 3 до 11.
При нулевом состоянии ожидания во флэш-памяти и включенной предварительной выборке ваш цикл:
38
08000020 <fact>:
8000020: b430 push {r4, r5}
8000022: 2100 movs r1, #0
8000024: 220b movs r2, #11
8000026: 2301 movs r3, #1
8000028: 6804 ldr r4, [r0, #0]
0800002a <fact_loop>:
800002a: 3101 adds r1, #1
800002c: 434b muls r3, r1
800002e: 4291 cmp r1, r2
8000030: d4fb bmi.n 800002a <fact_loop>
8000032: 6805 ldr r5, [r0, #0]
8000034: 1b60 subs r0, r4, r5
8000036: bc30 pop {r4, r5}
8000038: 4770 bx lr
Это означает, что системные часы 0x38 между двумя инструкциями ldr. Выравнивание не повлияло на это во flash.
Если вы используете Peter's или его вариант (bne имеет для меня больше смысла, чем плюс-минус, YMMV):
2d
08000020 <fact>:
8000020: b430 push {r4, r5}
8000022: 210b movs r1, #11
8000024: 2301 movs r3, #1
8000026: 6804 ldr r4, [r0, #0]
08000028 <fact_loop>:
8000028: 434b muls r3, r1
800002a: 3901 subs r1, #1
800002c: d1fc bne.n 8000028 <fact_loop>
800002e: 6805 ldr r5, [r0, #0]
8000030: 1b60 subs r0, r4, r5
8000032: bc30 pop {r4, r5}
8000034: 4770 bx lr
Выравнивание также не повлияло на этот цикл. Это меньше инструкций, а также быстрее.
Таким образом, из другого ответа и документации mul и sub по одному тактовому сигналу каждая ветвь, когда она взята, составляет 2 такта в соответствии с этим ответом, поэтому 4 такта на цикл, умноженный на 11, составляет 44 такта или 0x2C. Без сомнения, два ldr имеют свою стоимость, возможно, отсюда и дополнительные два часа. Или это может быть связано с тем, как работает блок предварительной выборки или что-то еще.
Ваш цикл составляет 5 часов, или 55, или 0x37, тот же ответ для дополнительных двух измеряемых часов.
Поэтому я усложнил некоторые из этих экспериментов, модуль предварительной выборки из ST и работа с нулевыми состояниями ожидания позволили нам увидеть производительность, показанную в документации ARM. Обратный отсчет вместо увеличения сохранил инструкцию в цикле, которая меньше по размеру и быстрее, о чем вы просили.
Ваши 5 тактов на цикл, умноженные на 3 факториала, означают 14 тактов (5 + 5 + 4), ваши 22 такта (проверьте, как вы их измерили, очень часто линейка — это проблема с эталонным тестом, а не кодом) имеют 8 тактов где-то еще минус 3 для инструкций по настройке, если вы их считали. Какую бы линейку вы ни использовали, если вы используете решение обратного отсчета, посмотрите, как это сравнивается в вашей системе. Сохраняет пару инструкций, одну внутри и одну вне цикла.
Я несколько удивлен, что gcc не оптимизировал это в цикл обратного отсчета. Я пробовал только одну версию, возможно, более старую версию 3.x или 4.x. Кроме того, если вы строите для cortex-m3, он использует инструкцию thumb2, а не инструкцию thumb.
unsigned int fact ( unsigned int x )
{
unsigned int a;
unsigned int rb;
a=1;
for(rb=1;rb<=x;rb++)
{
a*=rb;
}
return(a);
}
unsigned int fact2 ( unsigned int x )
{
unsigned int a;
a=1;
while(x)
{
a*=x--;
}
return(a);
}
Да, я мог бы оптимизировать код C дальше....
Disassembly of section .text:
00000000 <fact>:
0: b140 cbz r0, 14 <fact+0x14>
2: 2301 movs r3, #1
4: 461a mov r2, r3
6: fb03 f202 mul.w r2, r3, r2
a: 3301 adds r3, #1
c: 4298 cmp r0, r3
e: d2fa bcs.n 6 <fact+0x6>
10: 4610 mov r0, r2
12: 4770 bx lr
14: 2201 movs r2, #1
16: 4610 mov r0, r2
18: 4770 bx lr
1a: bf00 nop
0000001c <fact2>:
1c: 4603 mov r3, r0
1e: 2001 movs r0, #1
20: b123 cbz r3, 2c <fact2+0x10>
22: fb03 f000 mul.w r0, r3, r0
26: 3b01 subs r3, #1
28: d1fb bne.n 22 <fact2+0x6>
2a: 4770 bx lr
2c: 4770 bx lr
2e: bf00 nop
Я забыл о cbz, я не использую thumb2, если в этом нет необходимости, не так универсально переносим, как классические инструкции для большого пальца...
Более портативная версия:
Disassembly of section .text:
00000000 <fact>:
0: 2800 cmp r0, #0
2: d007 beq.n 14 <fact+0x14>
4: 2301 movs r3, #1
6: 2201 movs r2, #1
8: 435a muls r2, r3
a: 3301 adds r3, #1
c: 4298 cmp r0, r3
e: d2fb bcs.n 8 <fact+0x8>
10: 0010 movs r0, r2
12: 4770 bx lr
14: 2201 movs r2, #1
16: e7fb b.n 10 <fact+0x10>
00000018 <fact2>:
18: 0003 movs r3, r0
1a: 2001 movs r0, #1
1c: 2b00 cmp r3, #0
1e: d003 beq.n 28 <fact2+0x10>
20: 4358 muls r0, r3
22: 3b01 subs r3, #1
24: 2b00 cmp r3, #0
26: d1fb bne.n 20 <fact2+0x8>
28: 4770 bx lr
2a: 46c0 nop ; (mov r8, r8)
Хммм:
20: 4358 muls r0, r3
22: 3b01 subs r3, #1
24: 2b00 cmp r3, #0
26: d1fb bne.n 20 <fact2+0x8>
Вот это да.
arm-none-eabi-gcc --version
arm-none-eabi-gcc (GCC) 8.3.0
Copyright (C) 2018 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Спасибо за публикацию этого; Я задавался вопросом о получении инструкций после публикации моего ответа. Думаю, мне следует отредактировать раздел Cortex-M3 в своем ответе, чтобы сказать, что мой статический анализ предполагает 0 состояний ожидания и предварительную выборку. Я предполагаю, что с циклом из 3 инструкций, состоящим только из 16-битных инструкций, выравнивание не будет иметь значения, потому что в любом случае у вас есть половина и одно целое слово? Или полезно, чтобы первая пара была целым словом?
@PeterCordes, я думаю, первая пара не имеет значения. Тем не менее, нужно было бы найти одну из других cortex-ms, которая была настроена для выборки полуслов, а затем попробовать что-нибудь, чтобы увидеть, есть ли там проблема с выравниванием. Но на самом деле, зачем вам строить для выборки полуслов, если ваша шина не имеет 16-битной ширины, и зачем вам строить с 16-битной шиной.
Я сопротивлялся желанию подписаться на пробный период с их логикой, что я не хочу на самом деле видеть это, а затем мне постоянно запрещают говорить или делать что-то. Поэтому я могу только предположить, что дополнительные часы на цикл — это выравнивание. У ST есть своя логика, чтобы попытаться сгладить флэш-память, и я тоже не знаю точно, как это выглядит, но ветке все еще приходится возиться с предварительной выборкой...
arm и st говорят о том, что ядро m3 имеет своего рода предсказание ветвления, но не уверены, как это стимулировать, с более крупными ядрами, которые извлекают несколько инструкций за транзакцию выборки, вы можете возиться с этим, есть сладкое место, где вы можете переместить свой конец ветви цикла и включать и отключать предсказание ветвления и видеть часы или два сбережения. cortex-ms, насколько я могу судить, - это одно слово за раз или одно полуслово за раз, с крошечными каналами, поэтому я не вижу, чтобы реальное предсказание ветвления делало много, ну, тип кеша все равно будет работать.
Основываясь на других ответах и комментариях, самой быстрой на самом деле является таблица, поскольку единственные результаты, которые соответствуют 32-битному результату, - это от 0 до 12, что в целом занимает больше байтов, но одновременно быстро и последовательно, как 1! и 12! должно занимать одинаковое количество тактов.
Это домашнее задание или что? Какова точная формулировка задачи, которую вам поставили? Представленный вами код можно заменить на MOV r3, # 6, отсюда и вопросы выше. Также с каким ядром процессора вы работаете и как вы рассчитываете тактовые циклы?