У багатьох випадках оптимальний спосіб виконання певного завдання може залежати від контексту, в якому виконується завдання. Якщо рутина написана мовою складання, то, як правило, неможливо змінювати послідовність інструкцій залежно від контексту. Як простий приклад розглянемо наступний простий метод:
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. У такому випадку компілятор може оптимізувати версію коду С просто:
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 мс.