LDM. Моя любимая инструкция ARM

Автор оригинала: Vladimir Keleshev
  • Перевод
LDM — или load multiple — моя любимая инструкция в ассемблере для ARM. Вот почему.

Во-первых, что она делает. Вот пример:

ldm r4, {r0, r1, r2, r3}

Здесь она принимает базовый регистр (в данном случае r4) и набор регистров (в данном случае {r0, r1, r2, r3}). Загружает последовательные слова из адреса в базовом регистре в регистры из набора. Действие инструкции можно продемонстрировать с помощью такого C-подобного псевдокода:

r0 = r4[0];
r1 = r4[1];
r2 = r4[2];
r3 = r4[3];

Немало заданий для одной инструкции! Именно поэтому она называется load multiple.

Нотация набора также допускает диапазоны. Мы можем переписать предыдущий пример следующим образом:

ldm r4, {r0-r3}

В наборе разрешены все 16 регистров ARM. Итак, законно следующее:

ldm r0, {r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15}

В 32-битной инструкции набор регистров кодируется как 16-битная маска. Вот упрощённая кодировка исходного примера:

Упрощённое кодирование инструкции LDM


Такая инструкция идеально подходит для архитектуры load-store, такой как ARM, где основной рабочий процесс выглядит так:

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

Противоположностью LDM находится STM — store multiple.

Копирование блоков


С помощью этих двух инструкций можно быстро копировать большие блоки памяти. Вы можете скопировать восемь слов (или 32 байта!) памяти всего двумя инструкциями:

ldm r0, {r4-r11}
stm r1, {r4-r11}

У LDM и STM также есть варианты автоинкремента (обозначаемые знаком “!”), где базовый регистр увеличивается на количество загруженных/сохранённых слов, так что можно копировать в быстром цикле:

ldm r0!, {r4-r11}
stm r1!, {r4-r11}

Реализация стеков


В ARM инструкция POP — это просто псевдоним для LDM с указателем стека (и автоинкрементом). Она выглядит и работает точно так же:

ldm sp!, {r0-r3}
pop {r0-r3}

А инструкция PUSH — это псевдоним для варианта STM (STMDB).

Вы можете делать push и pop, копируя регистры в большом объёме в стек и из него за один раз. А если заменить SP другим регистром, то сможете реализовать эффективные стеки в других областях памяти. Например, вы можете реализовать теневой стек в куче.

Сохранение регистров


Вы боитесь использовать регистры, сохранённые вызовом (call-preserved), потому что их требуется сохранить, а в любом случае можно использовать слот стека? Это больше не проблема, потому что можно за один раз сохранить все call-preserved регистры, которые вы хотите использовать:

push {r4-r11}

Пролог и эпилог


В ARM первые четыре аргумента, обратный адрес (LR) и указатель кадра (FP) передаются в регистрах. Вот почему особенно важно иметь эффективные пролог и эпилог. К счастью, вы можете сохранить FP и LR за один раз, используя довольно стандартный пролог ARM:

push {fp, lr}

А потом восстановить их и вернуться (для эпилога):

pop {fp, lr}
bx lr

Ещё лучше, вы можете всё восстановить и вернуться за один раз!

pop {fp, pc}

Тут значение обратного адреса (LR) вставляется в регистр счётчика программ (PC), так что не нужен явный возврат!

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

push {r0-r3, fp, lr}

Или можно сохранить FP и LR и одновременно выделить некоторое пространство в стеке:

push {r0-r3, fp, lr}

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

ARM64


Подозреваю, когда пришло время разработать 64-битную версию набора команд ARM, пришлось пойти на компромисс и удвоить количество регистров до 32-х. Помню, в какой-то статье говорилось, что это изменение улучшает производительность примерно на 6% по всем направлениям. С 32-мя регистрами больше невозможно закодировать битовую маску всех регистров в 32-битную длинную инструкцию. Таким образом, вместо ARM64 появились LDP и STP: load pair и store pair, духовные преемники LDM и STM.

Этот пост изначально начинался как тред в твиттере.

Похожие публикации

Средняя зарплата в IT

110 000 ₽/мес.
Средняя зарплата по всем IT-специализациям на основании 8 580 анкет, за 2-ое пол. 2020 года Узнать свою зарплату
Реклама
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее

Комментарии 15

    0
    Спасибо, интересная вещь.

    Интересно, arm_dsp_library пользует ли данную инструкцию? Функции копирования там есть, но вот оптимизирует ли они свой код этой инструкцией или отдают на откуп компилятору, не ясно до конца.
      0

      Замечание: одна и та же команда не может использоваться и для копирования, и для работы со стеком, потому что в случае стека команда push должна делать не инкремент, а декремент, и писать регистры в обратном порядке.

        0

        del

          0
          Команда LDM имеет модификатор для управления порядком действий:
          ldmia (increment after)
          ldmib (increment before)
          ldmda (decrement after)
          ldmdb (decrement before)

          Регистры пишутся в одинаковом порядке (IIRC).

          В ARM стек можно сделать в любом направлении и даже в обоих одновременно:
          в режиме jazelle есть два стека — Java и нативный стек, которые растут в противоположные стороны.

          0
          ещё лучше, вы можете всё восстановить и вернуться за один раз!
          pop {fp, pc}

          Надо понимать, что за один раз — это как минимум 3 цикла тут + сброс конвейера.

            0

            А что по времени выполнения таких инструкций?

              0

              на cortex M3 число тактов равно количеству регистров + 1

                0
                или проще говоря, количеству всех задействованных регистров
              +3
              AArch64 ISA спроектирована так, чтобы быть эффективной на больших OoO процессорах.

              пришлось пойти на компромисс и удвоить количество регистров до 32-х.

              Компромисс — странное слово применительно к удвоению регистров не меняя при этом размера инструкции.
              Отказ от условного выполнения инструкций (не только не полезного, но и вредного для OoO) позволил сильно увеличить пространство опкодов и удвоить количество регистров.

              Таким образом, вместо ARM64 появились LDP и STP

              Множественная загрузка так же плохо ложится на OoO движок (микрокод, неравномерное использование rob/rename, возможно даже блокировка ld queue/st queue).
              На PowerPC аналогичный lmw также реализован в микрокоде и не рекомендуется к использованию вообще никогда (даже в in-order процессорах).

              Даже старые Cortex-ы могут выполнять чтение и запись одновременно благодаря наличию двух блоков LD/ST.
              Копирование рекомендуется осуществлять интерливом:
                  LDRD r3,r4,[r1,#0] 
                  STRD r3,r4,[r0,#0] 
                  LDRD r3,r4,[r1,#8] 
                  STRD r3,r4,[r0,#8]
              

              LDRD это почти что аналог LDP для 32-битного режима.

              Таким образом LDM хорош только на микроконтроллерах или на старых ARM-ах.
                0
                Команда зависает в конвейере на 1 такт больше чем всё остальное
                LDRD R0, R1, [R2, #const.]!
                Сразу за ней запрещено использовать команду перехода (в любом варианте), а так-же любой другой команды с любым из используемых регистров. Нужна команда прослойка. Всё дело в том что R2 должен сначала измениться.
                LDR R0, [R1], #const
                Тут требуется две команды до использования регистров или перехода.
                Словом, программировать на асме для арм — можно только в самых исключительных случаях — когда уже нет иного выхода.
                  +1
                  Сразу за ней запрещено использовать команду перехода (в любом варианте), а так-же любой другой команды с любым из используемых регистров.

                  Это где же такое написано? И что по-вашему будет? Big badaboom?

                  По крайней мере в доке по A-профайлу ничего такого нет.
                  developer.arm.com/documentation/ddi0406/b/Application-Level-Architecture/Instruction-Details/Alphabetical-list-of-instructions/LDRD--register-?lang=en

                  В псевдокоде показано когда результат невалидный — к примеру когда регистром приёмником является регистр адреса, при включении режима апдейта адреса.

                  Всё дело в том что R2 должен сначала измениться.

                  developer.arm.com/documentation/ddi0290/g/cycle-timings-and-interlock-behavior/load-and-store-doubleword-instructions

                  The updated base register has a result latency of one. For back-to-back load/store instructions with base write back, the updated base is available to the following load/store instruction with a result latency of 0.

                  Тут требуется две команды до использования регистров или перехода.

                  Это называется load-to-use latency. Никто вас не заставляет впихивать команды в пузыри если не хотите. К переходу это вообще не имеет никакого отношения, если конечно код не прыгает через R0. Выполнение инструкций после перехода начинается ещё до того как завершится выполнение вашего load-а.
                  На OoO процессоре вообще не нужно затыкать пузыри. Как показано на моём примере выше — группировка load-load | store-store менее эффективна чем back-to-back. OoO движок их выполнит когда нужно.
                  В реальности конечно мы генерим код по разные таргеты, например (A57-A78)+(A53-A55).

                  Словом, программировать на асме для арм — можно только в самых исключительных случаях — когда уже нет иного выхода.

                  Программировать на ARM легко и приятно. Если программируете in-order процессор, то неплохо бы понимать что такое темп и что такое латентность. И ARM тут ничем не отличается от остальных.
                    0
                    И что по-вашему будет?

                    Регистр R0 не успеет обновиться из внешней памяти.
                    Это не сложно проверить.
                      0
                      Процессор работает согласно спецификации.
                      В каком документе описаны ваши «находки»?

                      Регистр R0 не успеет обновиться из внешней памяти.

                      С чего бы ему не успеть? При доступе к регистру, содержимое которого ещё не готово, на in-order процессоре будет столл.
                      Процессоры без отслеживания зависимостей бывают, например первые MIPS (Microprocessor without. Interlocked. Pipeline. Stages), но ARM не из их числа.

                      Чтение данных из внешней памяти, не находящехся в кэше — это не 1 и не 2, а сотни тактов.

                      Это не сложно проверить.

                      Давайте проверим :)

                      badaboom test
                              .text
                              .global _start
                              .arch armv7a
                      _start:
                              
                              ldr r3,=result
                              ldr r2,=testdata
                      
                              mov r0,#0x30
                              mov r1,#0x30
                              ldrd r0,r1,[r2,#8]! //34 
                              strb r0, [r3,#0]
                              strb r1, [r3,#1]
                      
                              mov r0,#0x30
                              ldr r0,[r2],#4 //3
                              b 1f
                      1:      strb r0,[r3,#2]
                      
                      // show results
                              mov r0,#0
                              ldr r1,=text
                              ldr r2,=textlen  
                              mov r7,#4
                              swi #0
                      
                              mov r0,#0
                              mov r7,#1
                              swi #0
                       
                              .data
                      text:   .ascii "result is "
                      result: .ascii "XXX\n"
                      textlen = . - text
                      
                      testdata: .word 0x31,0x32,0x33,0x34
                                 
                      
                      



                      Осилите собрать?
                      as test.S
                      ld a.out -o test
                      ./test

                      Какой по вашему мнению будет результат?

                      Обратите внимание на «опасный» бранч после ldr, чего по вашим словам делать запрещено!
                        0
                        Регистр R0 не успеет обновиться из внешней памяти.
                        Это не сложно проверить.

                        Не верю…
                    0
                    Была бы хорошей командой, но вызывает исключение при обращении к памяти с адресами не кратными 4-м. Это, конечно, портит жизнь разработчикам компиляторов.

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                    Самое читаемое