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

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

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

Автор: 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, ассемблер



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

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

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