Как уменьшить время выполнения и количество циклов факторного цикла? И/или размер кода?

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

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

; 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, отсюда и вопросы выше. Также с каким ядром процессора вы работаете и как вы рассчитываете тактовые циклы?

tum_ 10.04.2019 00:30

Не могли бы вы уточнить, что вы подразумеваете под представленным кодом, который можно заменить на MOV r3, # 6, пожалуйста? Ядром процессора является Cortex M3, надеюсь, это поможет.

Hysteria103 10.04.2019 20:43

@ Hysteria103: ваша текущая программа вычисляет факториал постоянной времени сборки. Точно так же, как компилятор C, выполняющий распространение констант и заменяющий цикл результатом, это будет эквивалентно mov r3, #n!, за исключением того, что ассемблер не сделает это за вас, поэтому вам придется вручную записать это как 6. В любой реальной программе, где вам иногда нужно вычислить факториал, если количество возможных входных данных невелико, то стоит подумать о том, чтобы просто выбрать из некоторых предварительно рассчитанных версий. Или, если это факториал постоянной времени сборки, вычисления во время выполнения были бы ужасной идеей.

Peter Cordes 10.04.2019 22:32

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

Peter Cordes 10.04.2019 22:34
elaborate on what you mean - код между метками start и exit в вашем вопросе можно заменить на MOV r3, # 6, если задача, которую вы решаете, состоит в том, чтобы вычислить 3! и поместите результат в r3. Конечно, совершенно очевидно, что такая задача не имеет большого смысла, но проблема в том, что вы так и не объяснили, что такое точно ваша задача (до сих пор не объяснили). Это немного плохой стиль — просто кинуть код в людей и оставить их гадать, так как они могут потратить свое время, гадая в неправильном направлении. К счастью, @PeterCordes нашел достаточно времени, чтобы подготовить часовую лекцию в качестве ответа.
tum_ 10.04.2019 23:12

.. охватывает практически любую архитектуру и ядро ​​ARM :) К сожалению, некоторые люди просто не могут тратить столько времени, даже если они готовы помочь. Видите ли, теперь, когда вы упомянули Cortex-M3, ~ 80% его ответа оказались бесполезными (для вас). Впрочем, ничего, я просто ворчу, ведь на этом ресурсе были и будут гораздо худшие вопросы, чем ваши.

tum_ 10.04.2019 23:18

если вы используете .align, .balign или nops для изменения выравнивания цикла, это может повысить производительность. Часто, но не всегда flash медленнее, чем sram, поэтому, если вы бежите из sram (копируете и прыгаете), то питание ядра становится лучше. вы также можете подтвердить, что он использует расширения thumb2, когда это возможно. вы также можете проверить, есть ли в ядре предсказатель ветвления или нет, я не помню, есть ли он у cortex-m3 или нет.

old_timer 10.04.2019 23:57

всего несколько циклов, хотя вы можете не увидеть большого прироста производительности. какая конкретная часть этого cortex-m3 - это только часть представления о производительности, чип, типичные конструкции поставщиков чипов и т. д. являются фактором.

old_timer 10.04.2019 23:59
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
8
1 306
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Можно было бы использовать что-то вроде этого: (при условии, что 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.
Peter Cordes 10.04.2019 06:38

@PeterCordes - Спасибо, я обновил свой ответ. В последний раз я работал с ARM около 10 лет назад, ARM v4. Я не знаю, сколько циклов mul потребуется на современных ARM. Поиск в таблице должен быть быстрым, но я не знаю, разрешено ли это для этой проблемы.

rcgldr 10.04.2019 09:49

Что ж, в OP упоминается размер кода, поэтому похоже, что они заинтересованы в оптимизации компромисса между размером и производительностью. Поиск по таблице находится на одном конце спектра. Если они заботятся о больших входных данных, которые могут переполниться, возможно, LUT для каждого 4-го n и цикл на младших битах. Но в любом случае, я догадался, что большинство ARM будут иметь конвейерные множители, но они могут быть на Cortex-M, который имеет либо 1-тактный (быстрее, больший кристалл), либо 32-тактный (меньший кристалл, итеративный) множители. Смотрите внизу моего ответа. Но я нашел числа для Cortex-A9, вокруг которых написал свой ответ...

Peter Cordes 10.04.2019 10:03

Позволить ассемблеру вычислить множитель — это интересно, но плохо для производительности и размера кода по сравнению с sub r1, r2 lsl #2. Однако я не думаю, что существует версия mul или muls, которая поддерживает немедленное. keil.com/support/man/docs/armasm/armasm_dom1361289882394.htm документирует только источник регистра. (И muls составляет 2 байта в режиме Thumb, по сравнению с 4 для mul).

Peter Cordes 10.04.2019 10:08

@PeterCordes - я обновил свой ответ, чтобы устранить mul с константами. Поиск в таблице должен быть самым быстрым. Доступны ли ldr r3,[r3+r2 lsl #2] в режиме большого пальца?

rcgldr 10.04.2019 14:36

Это неправильный синтаксис (в своем первом комментарии я исходил из памяти; я исправил код в вашем ответе с помощью редактирования), но да, в Thumb2 он доступен в виде 32-битной инструкции. Я проверял газом. 2-регистр без сдвига влево доступен как 16-битная инструкция. keil.com/support/man/docs/armasm/armasm_dom1361289874275.htm‌​. О да, ARM позволяет вычитать регистр как часть режима адресации, так что это круто.

Peter Cordes 10.04.2019 14:50

Вы можете сократить размер кода вдвое для версии с вычисляемым переходом, используя adds и muls вместо add и mul. (И у вас все еще есть глючный mul-непосредственно в прологе, который должен быть сдвигом влево на 3. Мое изменение сделало бы это сдвигом влево на 2 для режима большого пальца, но все еще 3 для режима ARM, конечно, так что вы, вероятно, просто хочу изменить это на комментарий.)

Peter Cordes 10.04.2019 15:00

@PeterCordes - спасибо. Единственный код ARM, который я смог найти в своей системе, — это небольшая многозадачная ОС, которую я написал 10 лет назад для повышения скорости, с ограничением, заключающимся в том, что каждая задача имеет свой приоритет (используется одно целое число с битами, установленными в 1, чтобы указать, что задача готова к выполнению). запустить, и не было времени нарезки). Я не был уверен в умножении со знаком.

rcgldr 10.04.2019 15:00

Объединение 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
Ответ принят как подходящий

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


Большая часть этого ответа предполагала более производительный процессор, такой как Cortex-A9 или Cortex-A53, где программная конвейерная обработка для создания параллелизма на уровне инструкций была бы полезна. Cortex M3 является скалярным и имеет инструкцию умножения за один цикл, что значительно упрощает оптимизацию.

(В исходном вопросе не указывалось ядро, и я ожидал, что даже недорогие процессоры будут иметь многотактную задержку 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.)


Инструкции умножения ARM с меньшей задержкой:

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-этапный конвейер.


Версия Кортекс-М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 10.04.2019 22:09

@ Hysteria103: большая часть этого вообще не применима к Cortex M3. Его множитель не является конвейерным (и muls не медленнее, чем smulbb), а ЦП не является суперскалярным, так что в создании ILP нет смысла. Все, что вы можете сделать, это уменьшить количество инструкций без умножения, например. путем развертывания, чтобы уменьшить накладные расходы на цикл, и, как показывает ensc, с помощью subs для установки флагов во время обратного отсчета.

Peter Cordes 10.04.2019 22:38

@Hysteria103: добавил к моему ответу раздел Cortex-M3, код уменьшился до 0xa = 10 байт: PI не удосужился найти хороший компромисс между производительностью и размером для M3, где взятые ветки стоят столько же, сколько тело цикла . Вероятно, мы можем использовать ту же i-=2 развертку, что и мой цикл с двумя аккумуляторами, возможно, с более простой настройкой. Или, может быть, что-то вроде обработки нечетных/четных n с условным mul для начала, чтобы мы знали, чем закончится цикл... Много забавных вещей, которые вы могли бы сделать, в зависимости от того, какой компромисс между размером и производительностью вы ищете для.

Peter Cordes 11.04.2019 00:23

Если вы хотите краткое изложение, перейдите к концу.

Я запустил это на синей таблетке 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-битных инструкций, выравнивание не будет иметь значения, потому что в любом случае у вас есть половина и одно целое слово? Или полезно, чтобы первая пара была целым словом?

Peter Cordes 11.04.2019 09:46

@PeterCordes, я думаю, первая пара не имеет значения. Тем не менее, нужно было бы найти одну из других cortex-ms, которая была настроена для выборки полуслов, а затем попробовать что-нибудь, чтобы увидеть, есть ли там проблема с выравниванием. Но на самом деле, зачем вам строить для выборки полуслов, если ваша шина не имеет 16-битной ширины, и зачем вам строить с 16-битной шиной.

old_timer 11.04.2019 16:30

Я сопротивлялся желанию подписаться на пробный период с их логикой, что я не хочу на самом деле видеть это, а затем мне постоянно запрещают говорить или делать что-то. Поэтому я могу только предположить, что дополнительные часы на цикл — это выравнивание. У ST есть своя логика, чтобы попытаться сгладить флэш-память, и я тоже не знаю точно, как это выглядит, но ветке все еще приходится возиться с предварительной выборкой...

old_timer 11.04.2019 16:32

arm и st говорят о том, что ядро ​​​​m3 имеет своего рода предсказание ветвления, но не уверены, как это стимулировать, с более крупными ядрами, которые извлекают несколько инструкций за транзакцию выборки, вы можете возиться с этим, есть сладкое место, где вы можете переместить свой конец ветви цикла и включать и отключать предсказание ветвления и видеть часы или два сбережения. cortex-ms, насколько я могу судить, - это одно слово за раз или одно полуслово за раз, с крошечными каналами, поэтому я не вижу, чтобы реальное предсказание ветвления делало много, ну, тип кеша все равно будет работать.

old_timer 11.04.2019 16:34

Основываясь на других ответах и ​​комментариях, самой быстрой на самом деле является таблица, поскольку единственные результаты, которые соответствуют 32-битному результату, - это от 0 до 12, что в целом занимает больше байтов, но одновременно быстро и последовательно, как 1! и 12! должно занимать одинаковое количество тактов.

old_timer 11.04.2019 16:57

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