Во многих случаях оптимальный способ выполнения какой-либо задачи может зависеть от контекста, в котором выполняется задача. Если подпрограмма написана на ассемблере, последовательность команд, как правило, не может быть изменена в зависимости от контекста. В качестве простого примера рассмотрим следующий простой метод:
inline void set_port_high(void)
{
(*((volatile unsigned char*)0x40001204) = 0xFF);
}
Компилятор для 32-битного кода ARM, учитывая вышеизложенное, скорее всего будет отображать что-то вроде:
ldr r0,=0x40001204
mov r1,#0
strb r1,[r0]
[a fourth word somewhere holding the constant 0x40001204]
или, возможно,
ldr r0,=0x40001000 ; Some assemblers like to round pointer loads to multiples of 4096
mov r1,#0
strb r1,[r0+0x204]
[a fourth word somewhere holding the constant 0x40001000]
Это может быть немного оптимизировано в собранном вручную коде, например:
ldr r0,=0x400011FF
strb r0,[r0+5]
[a third word somewhere holding the constant 0x400011FF]
или
mvn r0,#0xC0 ; Load with 0x3FFFFFFF
add r0,r0,#0x1200 ; Add 0x1200, yielding 0x400011FF
strb r0,[r0+5]
Для обоих подходов, собранных вручную, потребуется 12 байт пространства кода, а не 16; последний заменит «нагрузку» на «добавление», что на ARM7-TDMI выполнит два цикла быстрее. Если бы код собирался выполняться в контексте, где r0 не знал / не заботился, версии на ассемблере были бы несколько лучше, чем скомпилированная версия. С другой стороны, предположим, что компилятор знал, что какой-то регистр [например, r5] должен был содержать значение, которое было в пределах 2047 байтов от желаемого адреса 0x40001204 [например, 0x40001000] и далее знал, что какой-то другой регистр [например, r7] собирался содержать значение, младшие биты которого были 0xFF. В этом случае компилятор может оптимизировать C-версию кода до простого:
strb r7,[r5+0x204]
Гораздо короче и быстрее, чем даже оптимизированный вручную ассемблерный код. Далее, предположим, что set_port_high произошел в контексте:
int temp = function1();
set_port_high();
function2(temp); // Assume temp is not used after this
Совсем неправдоподобно при кодировании для встроенной системы. Если в коде ассемблера записано set_port_high
, компилятору придется переместить r0 (который содержит возвращаемое значение из function1
) куда-то еще до вызова кода ассемблера, а затем переместить это значение обратно в r0 (поскольку function2
будет ожидать свой первый параметр в r0), поэтому для «оптимизированного» кода сборки потребуется пять инструкций. Даже если компилятор не знает ни одного регистра, содержащего адрес или значение для хранения, его версия из четырех команд (которую он может адаптировать для использования любых доступных регистров - не обязательно r0 и r1) превзойдет «оптимизированную» сборку языковая версия. Если бы компилятор имел необходимые адреса и данные в r5 и r7, как описано ранее, function1
не изменил бы эти регистры, и, таким образом, он мог бы заменить set_port_high
одной инструкцией strb
- на четыре команды меньше и быстрее , чем «оптимизированный вручную» код сборки.
Обратите внимание, что оптимизированный вручную ассемблерный код может часто опережать компилятор в тех случаях, когда программист знает точный ход программы, но компиляторы работают лучше в тех случаях, когда фрагмент кода написан до того, как известен его контекст, или когда один фрагмент исходного кода код может быть вызван из нескольких контекстов [если set_port_high
используется в пятидесяти различных местах кода, компилятор может самостоятельно решить для каждого из них, как лучше его расширить].
В целом, я хотел бы предположить, что ассемблер имеет тенденцию давать наибольшие улучшения производительности в тех случаях, когда к каждому куску кода можно подходить из очень ограниченного числа контекстов, и это может нанести ущерб производительности в местах, где К фрагменту кода можно подходить из разных контекстов. Интересно (и удобно), что случаи, когда сборка наиболее выгодна для производительности, часто бывают такими, где код наиболее прост и удобен для чтения. Места, в которых код на ассемблере превращается в неприятный беспорядок, - это те места, где написание на ассемблере дает наименьшее преимущество в производительности.
[Незначительное примечание: в некоторых местах ассемблерный код может использоваться для создания гипероптимизированного тупого беспорядка; например, один фрагмент кода, который я сделал для ARM, должен был извлечь слово из ОЗУ и выполнить одну из примерно двенадцати подпрограмм, основанных на верхних шести битах значения (многие значения сопоставлены одной и той же подпрограмме). Я думаю, что я оптимизировал этот код до чего-то вроде:
ldrh r0,[r1],#2! ; Fetch with post-increment
ldrb r1,[r8,r0 asr #10]
sub pc,r8,r1,asl #2
Регистр r8 всегда содержал адрес основной таблицы диспетчеризации (в цикле, где код тратит 98% своего времени, ничто никогда не использовало его для каких-либо других целей);все 64 записи относятся к адресам в 256 байтах, предшествующих ему.Поскольку основной цикл имел в большинстве случаев жесткий предел времени выполнения около 60 циклов, выборка и отправка из девяти циклов были очень полезны для достижения этой цели.Использование таблицы из 256 32-битных адресов было бы на один цикл быстрее, но поглотило бы 1 КБ очень ценной оперативной памяти [флэш-память добавила бы более одного состояния ожидания].Использование 64 32-битных адресов потребовало бы добавления инструкции для маскировки некоторых битов из извлеченного слова, и все равно поглотило бы на 192 байт больше, чем таблица, которую я фактически использовал.Использование таблицы 8-битных смещений позволило получить очень компактный и быстрый код, но я не ожидал, что компилятор когда-нибудь придумает;Я также не ожидал бы, что компилятор выделит регистр «полный рабочий день» для хранения адреса таблицы.
Приведенный выше код был разработан для работы в качестве автономной системы;он может периодически вызывать код C, но только в определенные моменты, когда аппаратное обеспечение, с которым оно обменивалось данными, может безопасно переводиться в состояние «ожидания» на два интервала примерно в одну миллисекунду каждые 16 мс.