Войти
ПрограммированиеСтатьиОбщее

Практическое применение SSE расширения. (2 стр)

Автор:

Ассемблер, для чего он нам?

Ассемблер - единственный язык низкого уровня, и это не потому, что он такой нехороший. Просто он наиболее близок (читай идентичен) машинным кодам (не микрооперациям!) процессора. Одна инструкция ассемблера (мнемоника) соответствует одной команде процессора.
[tr=code]На заметку:

Внутренняя архитектура ядра современных Intel процессоров очень похожа на архитектуру RISC процессоров (процессоры с уменьшенным набором команд-примитивов) - внутри процессора одна инструкция раскладывается на более простые и примитивные микрооперации. В тексте статьи под инструкцией будет подразумеваться именно та инструкция, которую мы написали, а не ее микрокод (собственно нам он и недоступен).

[trtd]

Ячейки памяти на процессоре называются регистрами. Соответственно, это наиболее быстрая память доступная процессору, однако число регистров и их разрядность ограниченны. В языке Си можно напрямую писать ассемблерные вставки в программе, используя так называемый инлайн ассемблер (в различных компиляторах ключевые слова: asm, _asm, __asm или через директиву #pragma).
[tr=code]На заметку:

Кроме того, в некоторых компиляторах доступны библиотеки интринсик функций. Это наборы функций, где одна функция обычно мапируется на одну инструкцию того или иного расширения будь то MMX, 3DNOW!, SSE. Интринсики не являются полноценной заменой всех команд ассемблера а инкапсулируют лишь определенную часть его команд - только возможности расширений.

[trtd]

Основным предназначением ассемблера (Не следует забывать что ассемблер является первоисточником - вначале был ассемблер :) было системное программирование, а так же оптимизация критичных ко времени исполнения участков кода. Сейчас, как мне кажется, процент потерь в производительности кода сгенерированного компиляторами очень мал, а процессоры быстры, что эта задача уходит на второй план.
[tr=code]На заметку:

Уже достаточно давно на www.flipcode.com доступна статья об оптимизации кода с использованием шаблонов (templates). Автор статьи, используя шаблоны, добивается высокой чистоты и оптимизации при генерации кода компилятором. Хорошо ли подобное решение? Моя субъективная точка зрения - нет. Подобная оптимизация весьма зависима от типа компилятора - если это сработало на Microsoft VC, это вероятнее всего не сработает при использовании Borland C++ или скажем Intel C++. Кроме того, нет никаких гарантий, что подобный фокус будет работать с той же обновлённой версией сервис пака или новой версией этого же компилятора. Второе, что бросается в глаза, это большое количество "грязного текста": для того чтобы добиться оптимизации выходного кода, автор порождает нагромождения тяжело воспринимаемых конструкций. В конечном счете, автор, создавая пример, был в состоянии анализировать ассемблерный код, а значит с таким же успехом мог создать весь текст на ассемблере вручную. Хотя, конечно, это самый прозрачный из моих доводов - статья то ориентирована на оптимизацию с использованием шаблонов. Стоила ли овчинка выделки? Решать вам. 

Интересный факт - достаточно большое количество ошибок в реализации компиляторов связанно именно с оптимизацией ими выходного кода. Каждый желающий, ради интереса, набрав в MSDN в строке поиска фразу “global optimization” (global optimization - один из ключей оптимизации для компиляторов MS VC) будет удивлен количеству топиков с отметкой BUG. Большая часть заметок в MSDN конечно относится к предыдущим версиям компиляторов VC, однако конечный пользователь должен иметь ввиду, что компиляторы такие же программы и так же могут допускать ошибки при генерации кода. В случае если сгенерированный код для разных оптимизаций работает по разному, конечному программисту следует выполнить следующие действия:

·  Прекратить обвинять во всём всех проблемах и грехах Билла Гейтса и его родственников :)

·  Тщательно проверить код на наличие собственных ошибок или недочетов (неинициализированные данные и т.п.). Зачастую именно в мелочах кроется решение проблемы.

·  Изучить баг-трак листы (отчеты об ошибках), если таковые имеются на предмет подтверждения ошибки, либо попытаться выяснить это на официальном форуме поддержки разработчика компилятора.

·  Сообщить разработчику компилятора об обнаружении проблемы с примером возникновения ошибки и по возможности полной конфигурацией аппаратной (тип процессора, его характеристики) а так же программной части (компилятор, его конфигурация, операционная система в которой использован компилятор).

[trtd]

Что же остается первоочередной целью? А остаются следующие ситуации:

·  Компилятор не умеет генерировать код под новое расширение (например, даже при наличии ключа или опции "оптимизация кода для Pentium 3" у компилятора, это вовсе не значит, что код будет генерироваться с использованием SSE расширения для вычислений с плавающей точкой, а вероятнее всего означает базовую оптимизацию генерируемого выходного кода). В данном случае следует внимательно изучать документацию, поставляемую производителем компилятора.

·  Компилятор вообще "не знает" о существовании расширения.

Кроме того, ситуация, когда компилятор поддерживает расширение обычно не решает проблемы, так как компилятору нельзя указать явным образом какой кусок кода компилировать с использованием той или иной оптимизации.
[tr=code]На заметку:

Определенная функциональность в MS VC все же существует - прагма-скобка optimize, окружив код которой можно задавать некоторые виды оптимизации. Например:

#pragma optimize("t",on)

some_func()
{

}
#pragma optimize("t",off)

несмотря на включенную оптимизацию по размеру выходного файла, оптимизирует some_func() по скорости исполнения. За более полным описанием параметров для optimize и её применением прямая дорога в MSDN или документацию по компилятору.

[trtd]

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

Как же мне его использовать?

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

- MASM 6.14 (Microsoft Macro Assembler, кроме того доступен в комплекте процессор пака)
- MASM 7 (Кроме того, доступен в комплекте MS VC .NET)
- MS VC 6 (Инлайн ассемблер и интринсики при условии установленного на него процессор пака, который в свою очередь требует установки SP на сам VC)
- MS VC 7 (Инлайн ассемблер и интринсики)
- Intel C++ (Инлайн ассемблер и интринсики)

Я уверен, что это не полный список. Наверняка последние версии NASM так же поддерживают SSE инструкции, тем не менее, в своём примере я использовал инлайн ассемблер именно MS VC.
[tr=code]На заметку:

Если Ваш компилятор не поддерживает SSE инструкции, это не повод расстраиваться и выбрасывать его в мусорный бак. Есть несколько возможных вариантов выхода из сложившейся ситуации.

Например, вполне реальным является комбинирование объектных модулей разных компиляторов (при условии, конечно же, совпадения формата). Например, в свое время мною была использованная связка MS VC + MASM 6.14. Для компиляции исходного текста Math.asm использовался макроассемблер, для чего в настройках Developer Studio в меню Project\Settings закладка Pre Link Step была добавлена следующая строка:

ml /Zf /c /Cx /coff math.asm

А в закладку Link, поле Object Library modules был добавлен math.obj.

Исходный текст math.asm:

.686p

.XMM
.model c,flat

.code

public MATH_SSE_CrossProduct
public MATH_SSE_MatMulVec

; Далее следует текст исходный процедур
; …

end

Фрагмент e_math.h в котором определены функции из math.asm:

extern "C" void MATH_SSE_CrossProduct(float *,float *,float *);

extern "C" void MATH_SSE_MatMulVec(float *,float *);

Вторым вариантом является использование директивы _emit (поддерживается опять таки компиляторами MS VC) или подобной ей (если есть поддержка таковой в вашем компиляторе). Кроме того, подобное не сложно реализовать через стандартную директиву db. Директива _emit позволяет добавлять напрямую в сгенерированный байт код свои данные. Например, строка

_emit 0xСС;
будет заменена в исходном коде на инструкцию int 3 именно в том месте, в котором она была использована.

(0xСС код ассемблерной комманды int 3 - стандартный отладочное прерывание (Debug Break) для IA-32 архитектуры, при его выполнении будет вызвано исключение и выполнена соответствующая реакция ОС - например Windows заявит о том, что программа вызвала исключение и предложит его завершить или провести отладку)

Таким образом, можно определить макроопределениями отсутствующие инструкции любого расширения. Именно директивой _emit воспользовался Rob Wyatt (http://www.gamasutra.com) в своем KNI.H. Все комманды SSE раширения были описанным им в виде макросов. Сам заголовочный файл я приложил в архив примера данной статьи.

[trtd]

Структурирование кода.

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

Inline or not inline? forceinline !!!

Конечно же, не стоит забывать о директиве inline (не имеет никакого отношения к инлайн ассемблеру) как об одном из способов оптимизации исходного кода программы. Как ты уже наверняка знаешь, код для функции помеченный такой директивой не вызывается, а напрямую подставляется в место, где она была вызвана. Тем самым не затрачивается время на подготовку и передачу параметров, вызов и возврат из самой функции.
[tr=code]На заметку:

Интересной особенностью компилятора Microsoft является то, что директива inline (_inline, __inline) носит для него рекомендательный характер. Для принудительной подстановки кода функции необходимо воспользоваться директивой __forceinline. Кроме того, не стоит забывать о ключе /Ob.

[trtd]

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

Страницы: 1 2 3 4 5 Следующая »

#ASM, #SSE, #оптимизация

26 августа 2003

Комментарии [9]