цифровая электроника
вычислительная техника
встраиваемые системы

 
» » На самом ли деле ассемблер так хорош?

На самом ли деле ассемблер так хорош?

Автор: Mike(admin) от 28-09-2013, 11:25

Население этой планеты свято верит в то, что ассемблерный код, написанный в тяжелых муках, несмотря на свою времязатратность и высокие требования к навыкам программиста, всегда даст лучший результат. Поэтому ниже будут приведены еретические доводы против подобных заблуждений, и будут представлены случаи, когда язык C в паре с современным хорошим компилятором могут сделать работу лучше.


ассемблерные инструкции

Получение оптимального кода


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


Это может показаться очевидным, но здесь имеется один неучтенный фактор — человеческая природа. Но мы поговорим об этом ниже.


Подход компилятора


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


Чтобы посмотреть на то, как компиляторы обрабатывают обычный код для встраиваемых систем, воспользуемся конструкцией switch языка C. Конструкция switch может принимать одну из четырех форм:


  • с небольшим количеством значений case

  • с большим количеством значений case, которые выстроены в непрерывной последовательности

  • с большим количеством значений case, которые в большинстве своем выстроены в непрерывной последовательности, но не все

  • с большим количеством значений case, которые выстроены в хаотичной последовательности

Мы рассмотрим каждый из этих случаев. Примеры обрабатывались с помощью компилятора Sourcery CodeBench компании Mentor Embedded для микроконтроллера Coldfire с минимальной оптимизацией. Ассемблер для Coldfire хорошо читаемый, поэтому был выбран именно он.


Несколько значений case


Как правило, switch используют, когда логика структуры if/else становится достаточно запутанной. В данной ситуации задействовано небольшое количество значений case:



switch (x)
{
case 1:
a();
case 22:
b();
case 83:
c();
}

Для большинства архитектур процессоров эффективным путем обработки такой логики является организация нескольких сравнений и прыжков, как в примере ниже, где %d0 выделено под x:



moveq #22,%d1
cmp.l %d0,%d1
jeq .L4
moveq #83,%d1
cmp.l %d0,%d1
jeq .L5
moveq #1,%d1
cmp.l %d0,%d1
jne .L6
.L3:
jsr a
.L4:
jsr b
.L5:
jsr c
.L6:

Непрерывная последовательность


В других ситуациях может быть задействовано большое количество значений case. Большинство компиляторов не устанавливает планку максимально возможного количества значений, но имеется аргумент, огораживающий от использования большого числа кейсов, и он заключается в том, что в результате такого беспредела очень сильно страдает читаемость кода. Иногда значения case выстраивают в четкой, правильной последовательности, как в данном случае:



switch (x)
{
case 1:
a();
case 2:
b();
case 3:
c();
case 4:
d();
case 5:
e();
}

Компилятор заметит такую последовательность и сгенерирует следующий код:



add.l %d0,%d0
move.w .L8(%pc,%d0.l),%d0
ext.l %d0
jmp %pc@(2,%d0:l)
.balignw 2,0x284c
.swbeg &6
.L8:
.word .L9-.L8
.word .L3-.L8
.word .L4-.L8
.word .L5-.L8
.word .L6-.L8
.word .L7-.L8
.L3:
jsr a
.L4:
jsr b
.L5:
jsr c
.L6:
jsr d
.L7:
jsr e
.L9:

В этом коде значение х используется в качестве индекса в таблице переходов. Это значительно быстрее, чем идти по длинной последовательности сравнений.


Почти непрерывная последовательность


Другим возможным случаем является последовательность, которая не совсем непрерывная, вроде этой:



switch (x)
{
case 1:
a();
case 2:
b();
case 3:
c();
case 4:
d();
case 6:
e();
}

И опять компилятор распознает последовательность подобного рода и создает таблицу переходов:



add.l %d0,%d0
move.w .L8(%pc,%d0.l),%d0
ext.l %d0
jmp %pc@(2,%d0:l)
.balignw 2,0x284c
.swbeg &7
.L8:
.word .L9-.L8
.word .L3-.L8
.word .L4-.L8
.word .L5-.L8
.word .L6-.L8
.word .L9-.L8
.word .L7-.L8
.L3:
jsr a
.L4:
jsr b
.L5:
jsr c
.L6:
jsr d
.L7:
jsr e
.L9:

В этот раз таблица включает в себя запись-заглушку для размещения пропущенного значения.


Хаотичная последовательность


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



switch (x)
{
case 11:
a();
case 23:
b();
case 113:
c();
case 40:
d();
case 5:
e();
}

В таком случае компилятор возвращается к последовательности сравнений, то есть:



moveq #23,%d1
cmp.l %d0,%d1
jeq .L5
moveq #23,%d1
cmp.l %d0,%d1
jlt .L8
moveq #5,%d1
cmp.l %d0,%d1
jeq .L3
moveq #11,%d1
cmp.l %d0,%d1
jeq .L4
jra .L9
.L8:
moveq #40,%d1
cmp.l %d0,%d1
jeq .L6
moveq #113,%d1
cmp.l %d0,%d1
jeq .L7
jra .L9
.L4:
jsr a
.L5:
jsr b
.L7:
jsr c
.L6:
jsr d
.L3:
jsr e
.L9:

Интересно моментом является то, что компилятор понимает, что при большом числе значений, он может сократить количество сравнений путем их разделения на группы с «большими» и «маленькими» значениями. Возможно, архитектуры ARM и x86 справляются с такой задачей даже лучше.


Человеческий подход


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


Тем не менее, это маловероятно. Хорошо обученный программист знает, что желательно писать читабельный и «удобный» код, даже если придется пожертвовать долей производительности. Поэтому использование преимущества непрерывной или почти непрерывной последовательности, заключающегося в индексировании таблицы переходов, было бы плохой практикой, так как будущие изменения программы не могут быть внесены в такую структуру. Ни один разработчик не желал бы переписывать целый кусок кода, зная, что в программу могут быть внесены изменения.


Компилятор не побрезгует каждый раз переписывать код, так что в большинстве случаев он будет выдавать оптимальный результат. Использование конструкции switch позволяет получить понятный и легко поддерживаемый код, так что разработчикам стоит пользоваться в первую очередь языками высокого уровня.




Перевод © digitrode.ru


<Источник>


Теги: язык C, ассемблер



   Благодарим Вас за интерес к информационному проекту digitrode.ru.
   Если Вы хотите, чтобы интересные и полезные материалы выходили чаще, и было меньше рекламы,
   Вы можее поддержать наш проект, пожертвовав любую сумму на его развитие.


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

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

Оставить комментарий