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