Text
                    Юрий Магда
ИСПОЛЬЗОВАНИЕ
АССЕМБЛЕРА
ДЛЯ ОПТИМИЗАЦИИ
ПРОГРАММ НА C++
Санкт-Петербург
«БХВ-Петербург»
2004


УДК 681.3.068+800.92Ассемблер/С++ ББК 32.973.26-018.1 М12 Магда Ю. С. М12 Использование ассемблера для оптимизации программ на C++. — СПб.: БХВ-Петербург, 2004. — 496 с: ил. ISBN 5-94157-414-2 Рассматривается использование языка ассемблера для оптимизации про- программ, написанных на языке C++. Подробно изложены вопросы приме- применения современных технологий обработки данных ММХ и SSE, а также использования особенностей архитектур современных процессоров для оп- оптимизации программ. Приведены практические рекомендации по оптими- оптимизации логических структур высокого уровня, использованию эффективных алгоритмов вычислений, работе со строками и массивами данных. В книгу включены примеры программного кода приложений, иллюст- иллюстрирующие различные аспекты применения ассемблера. В качестве средств разработки примеров используются макроассемблер MASM 6.14 и Microsoft Visual C++ .NET 2003. Исходные тексты программ содержатся на прила- прилагаемом к книге компакт-диске. Для программистов УДК 681.3.068+80(ШАссемблер/С++ ББК 32.973.26-018.1 Группа подготовки издания: Главный редактор Екатерина Кондукова Зам. главного редактора Игорь Шиишгин Зав. редакцией Григорий Добин Редактор Юрий Якубович Компьютерная верстка Натальи Смирновой Корректор Наталия Першакова Дизайн серии Инна Танина Оформление обложки Игоря Цырульникова Зав. производством Николай Тверских Лицензия ИД N» 02429 от 24.07.00. Подписано в печать 29.04.04. Формат 70x100VM. Печать офсетная. Усл. печ. л. 39,99. Тираж 3000 экз. Заказ Nt 243 "БХВ-Петербург", 190005, Санкт-Петербург, Измайловский пр., 29. Гигиеническое заключение на продукцию, товар № 77.99.02.953.Д.001537.03.02 от 13.03.2002 г. выдано Департаментом ГСЭН Минздрава России. Отпечатано с готовых диапозитивов в Академической типографии "Наука" РАН 199034, Санкт-Петербург, 9 линия, 12. ISBN 5-94157-414-2 с Мал» ю. с, 20м © Оформление, издательство "БХВ-Петербург , 2004
Содержание Предисловие 1 Введение 9 Часть I. Основы эффективного программирования НА АССЕМБЛЕРЕ 23 Глава 1. Оптимизация ассемблерного кода для процессоров Pentium 25 Глава 2. Оптимизация вычислительных алгоритмов с помощью ассемблера 61 Глава 3. Разработка и использование подпрограмм на ассемблере 107 Глава 4. Оптимизация логических структур C++ с помощью ассемблера 130 Часть II. Интерфейс с языками высокого уровня 153 Глава 5. Интерфейс модулей на ассемблере с программами на C++ 155 Глава б. Особенности разработки и применения подпрограмм на ассемблере ....174 Глава 7. Компоновка ассемблерных модулей с программами на C++ .NET 205 Глава 8. Разработка библиотек динамической компоновки (DLL) на ассемблере 247 Часть III. Встроенный ассемблер Visual C++ .NET 2003 И ЕГО ИСПОЛЬЗОВАНИЕ 269 Глава 9. Базовые структуры встроенного ассемблера Visual C++ .NET 2003 ....271 Глава 10. Встроенный ассемблер и оптимизация приложений. Технологии ММХ и SSE 290
IV , Содержание Глава 11. Оптимизация мультимедийных приложений с помощью ассемблера...381 Глава 12. Оптимизация многопоточных приложений с помощью ассемблера 390 Глава 13. Встроенный ассемблер C++ .NET и функции времени Windows 402 Глава 14. Ассемблер в задачах системного программирования Windows 434 Глава 15. Оптимизация процедурно-ориентированных приложений и системных служб 445 Заключение 478 Приложения 479 Приложение 1. Инструкции процессоров 80x86 481 Приложение 2. Описание CD 489 Список литературы 490 Предметный указатель 491
Предисловие Эволюция средств проектирования программ в течение последних десятиле- десятилетий способна удивить любого человека, занимающегося разработкой про- программного обеспечения. Особенно это касается написания программ для операционных систем семейства Windows. Современные инструментальные средства позволяют разработать приложение с помощью нескольких щелч- щелчков мыши\1 часто экономят недели и месяцы рутинной работы программи- программиста. Практически любая среда программирования содержит мастера разра- разработки приложений, которые могут создать приложение, имеющее те или иные отличительные особенности. Среда разработки Microsoft Visual C++ .NET, являясь одним из наиболее мощных инструментальных средств разработки, предлагает программисту широкий спектр возможностей для проектирования приложений любых ти- типов и сложности. Тем не менее, большинство серьезных приложений пи- пишется со значительной долей ручного труда, и вызвано это тем, что ни одно инструментальное средство программирования на языке высокого уровня не может дать максимального выигрыша в производительности. Это объектив- объективный фактор, и обусловлен он самой структурой и семантикой языков высо- высокого уровня. Возможным решением проблемы оптимизации приложений является при- применение языка ассемблера. Хочу заметить, что приложение можно написать и без использования этого языка. Существует целый ряд программ, не нуж- нуждающихся в сколько-нибудь серьезной оптимизации. Но как только речь заходит о приложениях, работающих в системах реального времени, драйве- драйверах устройств, мультимедиа-приложениях, программах для обработки звука и изображений и вообще о любых программах, критичных к времени вы- выполнения, применение ассемблера становится неизбежным, потому что ни- никакие другие методы оптимизации работать не будут. По существу, ассемблер является языком, на котором "говорит" процессор, и исчезнуть он может только вместе с исчезновением процессоров! По этой же причине ассемблер имеет и всегда будет иметь одно фундаментальное преимущество перед языками высокого уровня: он самый быстрый. Боль- Большинство приложений, работающих в режиме реального времени, либо на- написаны на ассемблере, либо используют в критических участках кода ас- ассемблерные модули. Многие программисты, пишущие на языках высокого уровня, с опаской относятся к применению ассемблера в своих разработках. Иногда приходит- приходится слышать, что язык ассемблера слишком сложен и труден для изучения.
2_ Предисловие Такие утверждения, вообще-то, не соответствуют действительности. Язык ассемблера не сложнее других языков программирования и довольно легко осваивается как опытными, так и начинающими программистами. Кроме того, в последние годы появились очень мощные инструментальные средства разработки программ на ассемблере, и это вынуждает по-другому взглянуть на процесс разработки программ на этом языке. Среди таких ин- инструментальных средств проектирования можно назвать макроассемблер MASM32, AsmStudio и NASM. Эти и другие аналогичные инструменты про- проектирования сочетают в себе гибкость и быстроту ассемблера и современ- современный графический интерфейс. Многочисленные библиотеки функций, разра- разработанные для ассемблера, приблизили этот язык по своим функциональным возможностям к высокоуровневым средствам проектирования приложений. Поэтому в настоящее время противопоставление ассемблера как сложного языка другим языкам более высокого уровня как, якобы, более простым не имеет под собой реальных оснований. Эта книга посвящена использованию языка ассемблера в программах, соз- созданных с помощью Visual C++ .NET 2003v— наиболее мощной среды про- программирования на языке C++ на сегодняшний день. Материал книги рас- раскрывает два относительно независимых аспекта применения ассемблера: как самостоятельного инструмента разработки отдельных процедур в виде объ- объектных модулей и как встроенного средства в составе C++ .NET. Если вы обратили внимание, фирма Microsoft постоянно совершенствует встроенный ассемблер. Должен сразу заметить, что книга не является учебником по ассемблеру или C++ .NET и предполагает наличие у читателя определенных знаний в этих областях программирования. Для успешной разработки программ в Windows желательно знать принципы работы приложений в этой операционной среде. От читателя не требуется глубоких знаний архитектуры Windows, поскольку все необходимые сведе- сведения приводятся в процессе анализа программного кода примеров. Эта книга задумана как практическое пособие для программистов- разработчиков, желающих больше узнать о программировании на ассембле- ассемблере. Программисты на Visual C++ .NET почерпнут много полезных сведений для дальнейшей работы. Материал книги включает много примеров с анализом программного кода. Автор считает, что любой теоретический вопрос должен подкрепляться примером программного кода. Это наиболее эффективный и быстрый спо- способ научиться писать программы. Часть примеров является оригинальными разработками автора и нигде более не встречается. Все примеры программ являются работоспособными и проверены автором. Автор сознательно избегает длинных и сложных программ, поскольку при
Предисловие их анализе легко теряются ключевые моменты, ради которых эти программы и были разработаны. Каждый пример построен таким образом, чтобы его можно было легко адаптировать или модифицировать для дальнейшего ис- использования. В качестве инструмента разработки выбран Visual C++ .NET 2003. Что касается примеров на ассемблере, то .здесь используется макроассемблер MASM фирмы Microsoft. Читателям автор рекомендует использовать ас- ассемблер MASM32, который включает в себя компилятор ML версии 6.14 и компоновщик LINK версии 5.12 той же фирмы. Во всех примерах используется упрощенный синтаксис языка ассемблера и минимум высокоуровневых конструкций языка. Автор не приводит деталь- детального описания компилятора MASM, а упоминает лишь те сведения, которые необходимы для работы. Читатели, желающие углубить свои знания о работе компилятора, без труда смогут найти массу информации по этому вопросу в других источниках. Автор стремится рассматривать материал в определенной логической после- последовательности, избегая как нагромождения программного кода, так и из- излишнего теоретизирования. Автор отдает себе отчет в том, что в одной книге сложно рассмотреть все аспекты оптимизации программирования в Windows, тем не менее, он надеется, что материал книги окажется достаточ- достаточно полезным для программистов. Структура книги Книга задумана как практическое пособие по оптимизации программ, напи- написанных на C++ .NET 2003, при помощи ассемблера. Рассматриваются два основных аспекта применения этого языка. Во-первых, ассемблер может применяться как самостоятельное средство разработки отдельных модулей. Автономные компиляторы позволяют создавать как законченные приложе- приложения, так и отдельные объектные модули и библиотеки функций, находящие широкое применение при разработке приложений на C++ .NET. Во-вторых, среда программирования C++ .NET 2003 включает в себя очень мощные средства программирования на встроенном ассемблере. В книге подробно рассматриваются преимущества и недостатки применения отдель- отдельного компилятора и встроенного ассемблера. Структура книги построена таким образом, чтобы можно было изучать ма- материал как выборочно по отдельным главам, так и последовательно, начиная с первой главы. Это удобно, т. к. позволяет различным категориям читате- читателей выбирать тот материал, который им более всего интересен. Как начи- начинающие, так и опытные программисты смогут найти в ней необходимые сведения.
? Предисловие Автор делает упор на практический аспект применения ассемблера для по- повышения производительности приложений. Большое количество примеров позволяет лучше понять принципы разработки и оптимизации программ, а необходимый теоретический материал дается в контексте приводимых при- примеров. Описание программных средств ассемблера и языков высокого уров- уровня дается в том объеме, в каком это необходимо для понимания материала. Автор посчитал излишним помещать в книге полный справочный материал по компиляторам и компоновщикам макроассемблера и C++ .NET, это бы- было бы просто дублированием многочисленных литературных источников и фирменных руководств. Примеры программ построены таким образом, чтобы продемонстрировать ключевые аспекты техники применения ассемблера. Автор пошел по пути демонстрации, как правило, какого-либо одного аспекта применения ас- ассемблера в одном примере, поэтому алгоритм таких программ довольно простой, а сами приложения имеют небольшой размер. Автор сознательно не разрабатывал большие приложения и не пытался оптимизировать все, что возможно, в одной программе. Любое более-менее серьезное приложение имеет свой уникальный рецепт для повышения производительности, причем возможны самые разные комбинации тех или иных методов. В этой книге показано, как применить "строительные кирпичики" оптими- оптимизации — ассемблер ММХ и SSE расширений, аналоги библиотечных функ- функций C++ на ассемблере, команды строковых примитивов и многие другие. Практически все примеры построены на основе шаблона консольного приложения C++ .NET 2003. Для разработки объектных модулей с ис- использованием ассемблера применяется макроассемблер MASM 6.14 фирмы Microsoft и встроенный компилятор ассемблера среды программирования C++ .NET 2003. Программный код примеров подобран так, чтобы его можно было приме- применить в собственных разработках. Автор надеется, что предоставленные при- примеры задач будут действительно полезны программистам. Книга состоит из 15 глав, краткое описание каждой из них приведено далее. П Глава 1 "Оптимизация ассемблерного кода для процессоров Pentium". В этой главе рассматриваются общие вопросы ускорения работы вычис- вычислительных алгоритмов при помощи применения ассемблера. Анализ программного кода выполняется с учетом архитектурных особенностей современных процессоров. Кратко рассмотрены основные принципы технологий FPU, ММХ и SSE. ? Глава 2 "Оптимизация вычислительных алгоритмов с помощью ассембле- ассемблера". Материал этой главы посвящен наиболее важным аспектам языка ас- ассемблера с точки зрения повышения производительности программ. Здесь рассматриваются алгоритмы обработки математических выражений,
Предисловие 5 массивов данных и строк. Показано использование возможностей мате- математического сопроцессора, применение команд обработки строк. ? Глава 3 "Разработка и использование подпрограмм на ассемблере". Мате- Материал этой главы посвящен разработке и оптимизации подпрограмм на языке ассемблера. Рассмотрены различные варианты обработки данных в подпрограммах, использование регистров и памяти для работы с данны- данными. В рассматриваемом контексте этот материал дополняет главу 2. Здесь же рассматриваются наиболее общие вопросы интерфейса отдельных процедур, полностью написанных на ассемблере, с языками высокого уровня." Как и в предыдущей главе, для демонстрации материала приво- приводятся многочисленные примеры. ? Глава 4 "Оптимизация логических структур C++ с помощью ассемблера". Здесь основное внимание уделено оптимизации наиболее важных конст- конструкций языка C++ .NET — циклов и условных операторов. На практиче- практических примерах показаны различные варианты реализации таких конст- конструкций на языке ассемблера. ? Глава 5 "Интерфейс модулей на ассемблере с программами на C++". Ма- Материал главы посвящен применению отдельно скомпилированных ас- ассемблерных модулей в программах на C++. Рассматриваются вопросы построения программного интерфейса таких модулей с приложениями, разработанными в среде C++ .NET 2003. Подробно анализируются стан- стандарты и соглашения о вызовах функций, теоретический материал под- подкреплен примерами. ? Глава 6 "Особенности разработки и применения подпрограмм на ас- ассемблере". Эта глава является продолжением предыдущей. Если в главе 5 были рассмотрены основные стандарты и соглашения, используемые при компоновке ассемблерных модулей с программами на C++ .NET, то здесь основное внимание уделено вопросам применения параметров и выбора способов их передачи при вызове ассемблерных функций. ? Глава 7 "Компоновка ассемблерных модулей с программами на C++ .NET". В этой главе подробно рассматриваются вопросы компоновки программ на C++ .NET и автономных ассемблерных модулей. Затронуты вопросы, ранее почти не освещенные в литературе, касающиеся сборки приложений с использованием объектных модулей, разработанных на ас- ассемблере. ? Глава 8 "Разработка библиотек динамической компоновки (DLL) на ас- ассемблере". Библиотеки динамической компоновки являются одной из наиболее важных частей операционных систем Windows. Они включают в себя многочисленные процедуры и являются мощным средством для на- написания эффективных программ. Материал главы посвящен практиче- практическим аспектам создания и применения DLL. Рассматриваются варианты
_б Предисловие разработки библиотек как на языке ассемблера, так и с помощью средств C++ .NET. О Глава 9 "Базовые- структуры встроенного ассемблера Visual C++ .NET 2003". Эта глава посвящена применению встроенного ассемблера C++ .NET 2003 для разработки высокопроизводительных программ. Встроен- Встроенный ассемблер является весьма эффективным средством для повышения производительности приложений и обладает целым рядом преимуществ перед автономными компиляторами. Рассматриваются особенности про- программной архитектуры встроенного ассемблера среды разработки C++ .NET и его связь с основными структурами C++. ? Глава 10 "Встроенный ассемблер и оптимизация приложений. Техноло- Технологии ММХ и SSE". Здесь рассмотрены практические аспекты использова- использования встроенного ассемблера C++ .NET на практических примерах реше- решения задач вычислительного характера. Впервые для подобной литературы подробно рассмотрены расширения ассемблера для технологий ММХ и SSE в контексте практического применения в программировании на C++ .NET. П Глава 11 "Оптимизация мультимедийных приложений с помощью ас- ассемблера". Материал главы посвящен применению ассемблера в разра- разработке мультимедийных приложений. Здесь рассматриваются некоторые методы оптимизации приложений мультимедиа с помощью ассемблера, теоретический материал подкреплен практическими примерами. ? Глава 12 "Оптимизация многопоточных приложений с помощью ассемб- ассемблера". Концепция потокового выполнения приложений в Windows явля- является базисом, на котором построены операционные системы этого се- семейства. Использование потоков позволяет упростить работу программы и воспользоваться преимуществами параллельной обработки. Примене- Применение ассемблера в многопоточных приложениях может обеспечить допол- дополнительное увеличение производительности. Именно этим вопросам и по- посвящен материал этой главы. ? Глава 13 "Встроенный ассемблер C++ .NET и функции времени Windows". Большая часть приложений, работающих в среде Windows, в той или иной степени использует функции времени и таймеры. Необхо- Необходимость использования функций времени возникает тогда, когда дело ка- касается операций в реальном масштабе времени, при написании драйве- драйверов устройств и мультимедийных приложений. В этой главе на основе практических примеров показано, как можно использовать встроенный ассемблер для улучшения производительности приложений реального времени. ? Глава 14 "Ассемблер в задачах системного программирования Windows". Здесь рассматриваются варианты оптимизации задач системного про- программирования в операционных системах Windows. Материал главы де-
Предисловие . 7_ монстрирует некоторые аспекты оптимизации файловых операций, управления памятью и межпроцессных коммуникаций. ? Глава 15 "Оптимизация процедурно-ориентированных приложений и системных служб". Материал этой главы посвящен принципам использо- использования встроенного ассемблера C++ .NET 2003 в процедурно-ориенти- процедурно-ориентированных приложениях Windows и системных службах. Применение ас- ассемблера для каждого из этих типов программ имеет свои особенности, которые и показаны в этой главе. Материал книги дополнен справочником по системе команд процессоров Intel. Поскольку полная система команд насчитывает несколько сотен на- наименований, то приведены только наиболее часто используемые команды. Значительную помощь читателю окажет и прилагаемый CD, на котором за- записаны все примеры программ, приведенных в книге. Автор выражает огромную благодарность сотрудникам издательства "БХВ- Петербург" за подготовку материалов книги к изданию. За неоценимую помощь и поддержку при подготовке рукописи — особая признательность жене Юлии.
Введение Эта книга посвящена вопросам оптимизации программного обеспечения, написанного на языке C++ .NET, с помощью языка ассемблера. Термин "оптимизация" применительно к процессу разработки и отладки программ подразумевает улучшение каких-либо характеристик работы программного продукта. Под этим термином часто подразумевают и комплекс мер по улучшению показателей производительности программы. Сам процесс оптимизации программного обеспечения может выполняться как программистом (ручная оптимизация), так и в автоматическом режиме компилятором той среды разработки, в которой производится отладка при- приложения. Возможен и вариант, когда программист использует программу- отладчик третьей фирмы для выполнения отладки и оптимизации. Большинство разработчиков понимает, что в условиях жесткой конкуренции вопрос производительности является важнейшим условием успеха или не- неудачи программы на рынке программных продуктов. Без серьезной работы над улучшением производительности программного кода нельзя обеспечить конкурентоспособность приложения. Хотя все осознают необходимость и важность процесса оптимизации программного обеспечения, эта тема до сих пор является противоречивой и дискуссионной. Все споры вокруг этого процесса в основном затрагивают один вопрос: так ли уж необходимо про- программисту заниматься ручной оптимизацией своего приложения, если для этого есть готовые аппаратно-программные средства? Часть программистов считает, что улучшить производительность работы приложения без использования средств отладки самого компилятора нельзя, тем более что все современные компиляторы имеют встроенные (built-in) средства оптимизации программного кода. Отчасти это правда. На сего- сегодняшний день все без исключения программные средства разработки преду- предусматривают использование оптимизационных алгоритмов при генерации исполняемого модуля. Можно полностью положиться на компилятор ("все сделано до нас"), кото- который сгенерирует для вас оптимальный код, и вообще не заниматься улуч- улучшением качества программы. При этом в целом ряде случаев может и не понадобиться никаких доработок и улучшений. Например, при создании небольших офисных приложений или утилит тестирования сети оптимиза- оптимизация обычно не нужна. Однако в большинстве случаев обойтись без ручной оптимизации и полагать- полагаться только на стандартные возможности компиляторов нельзя. С проблемой улучшения производительности, хотите вы этого или нет, вам неизбежно при-
J? Введение дется столкнуться при разработке более или менее серьезных приложений, например, баз данных, любых. клиент-серверных или сетевых приложений, причем оптимизирующий компилятор той среды, в которой вы работаете, в большинстве случаев значительного выигрыша вам не обеспечит. Если программист разрабатывает программы, работающие в реальном вре- времени, такие как драйверы устройств, системные службы или промышлен- промышленные приложения, то без очень серьезной работы по ручной доводке кода до оптимальной производительности задача написания программы просто не будет выполнена. И дело здесь не в том, что средства разработки несовер- несовершенны и не обеспечивают того уровня оптимизации, какой от них требует- требуется. Любая более или менее серьезная программа имеет столько взаимосвя- взаимосвязанных параметров, что ни одно средство разработки не улучшит ее так, как это может сделать сам программист. Процесс оптимизации программ срод- сродни скорее искусству, чем "чистому" программированию, и трудно поддается алгоритмизации. Улучшение производительности программ — обычно трудоемкий процесс, занимающий значительное время. Хочется отметить, что не существует еди- единого критерия оптимизации. Более того, сам процесс оптимизации доволь- довольно противоречив. Например, если добиться уменьшения объема памяти, ис- используемого программой, то за это придется расплатиться быстродействием работы программы. Ни одна программа не может быть одновременно сверхбыстрой, сверхмалой по размеру и полнофункциональной для пользователя. К этому можно сколь угодно приблизиться, но получить идеальное приложение вам никогда не удастся. Хорошие программы обычно сочетают те или иные качества в разумных пропорциях, в зависимости от того, что важнее: скорость выполнения, раз- размер программы (как файла приложения, так и объема памяти, занимаемого работающим приложением) или удобство интерфейса пользователя. Для многих офисных приложений очень важным показателем является удобство интерфейса пользователя и как можно более высокая функцио- функциональность. Например, для пользователя программы электронного телефон- телефонного справочника тот факт, что программа работает на 10% быстрее или медленнее, особого значения не имеет. Размер такой программы, в принци- принципе, не критичен и также не имеет особого значения, т. к. объем современ- современных жестких дисков достаточно большой, чтобы поместить десятки и даже сотни таких электронных справочников. Программе может быть необходимо десятки мегабайт оперативной памяти для работы — это тоже сейчас не проблема. Но вот возможность удобной манипуляции данными для пользо- пользователя будет очень важной. Если приложение использует клиент-серверную модель обработки данных и взаимодействия с пользователем, как, например, большинство сетевых при-
Введение ^ 1J_ ложений, то критерии оптимизации здесь будут несколько иными. На пер- первое место могут выйти проблемы использования памяти (особенно для сер- серверной части приложения) и оптимизации сетевого взаимодействия с кли- клиентской частью. Приложения, работающие в режиме реального времени, критичны по син- синхронизации получения, обработки и, возможно, передачи данных за прием- приемлемые интервалы времени. Подобные программы требуют, как правило, оп- оптимизации по загрузке процессора и синхронизации с системными службами операционной системы. Если вы — системный программист и разрабатываете драйверы или сервисы для работы с операционной систе- системой, например, с Windows 2000, то неэффективный программный код в лучшем случае только замедлит работу всей операционной системы, а о худших последствиях можно только догадываться. Как видим, повышение производительности программ зависит от многих факторов и в каждом конкретном случае определяется тем, что эта програм- программа должна делать. Рассмотрим теперь брлее подробно, как можно выполнить оптимизацию программ, и проведем небольшой сравнительный анализ различных методов повышения производительности выполнения приложений. Простейший способ заставить приложения работать быстрее — это повы- повысить вычислительную мощь компьютера за счет установки более производи- производительного процессора или увеличения объема памяти, т. е. сделать апгрейд (upgrade) аппаратной части. В этом случае проблема производительности будет решена сама собой. Если вы сторонник такого подхода, то скорей всего окажетесь в тупике, т. к. будете все время зависеть от аппаратных решений. К слову сказать, многие ожидания насчет производительности новых поколений процессоров, новых типов памяти и архитектур системных шин оказываются явно преувеличен- преувеличенными. Их производительность на практике оказывается ниже декларируе- декларируемой фирмами-изготовителями. Так, например, новые микросхемы памяти, как правило, превосходят своих предшественников по объему хранимых данных, но отнюдь не по быстродействию. Производительность жестких дисков также растет медленнее, чем их объем. Если вы разрабатываете коммерческое приложение, то должны учитывать, что у большинства пользователей нет самых последних моделей процессора и быстродействующей памяти. К тому же, далеко не все пользователи горят желанием выложить деньги на новый компьютер, если их вполне устраивает то, что у них уже есть. Поэтому вряд ли стоит полагаться всерьез на решение проблем с программ- программным обеспечением при помощи только одной закупки нового оборудования.
12 Введение Далее мы будем рассматривать только алгоритмические и профаммные ме- методы повышения производительности работы приложений. Оптимизация может проводиться по следующим направлениям: ? тщательная проработка алгоритма разрабатываемой профаммы; ? учет существующих аппаратных средств компьютера и использование их оптимальным образом; G использование средств языка высокого уровня той среды, в которой раз- разрабатывается приложение; ? использование языка низкого уровня, т. е. ассемблера; О учет специфических особенностей процессора. Рассмотрим более подробно каждое из этих направлений. Этап разработки алгоритма вашего приложения — самый сложный во всей цепочке жизненного цикла профаммы. От того, насколько глубоко проду- продуманы все аспекты вашей задачи, во многом зависит успех ее реализации в виде профаммного кода. В общем случае изменения в структуре самой профаммы дают намного больший эффект, чем тонкая насфойка про- профаммного кода. Идеальных решений не бывает, и разработка алгоритма приложения всегда сопровождается ошибками и недоработками. Здесь важ- важно найти узкие места в алгоритме, наиболее влияющие на производитель- производительность работы приложения. Кроме того, как показывает практика, почти всегда можно найти способ улучшить уже разработанный алгоритм программы. Конечно, лучше всего тщательно разработать алгоритм в начале проектирования, чтобы избежать в дальнейшем многих неприятных последствий, связанных с доработкой фрагментов профаммного кода в течение короткого промежутка времени. Не жалейте времени на разработку алгоритма приложения — это избавит вас от головной боли при отладке и тестировании профаммы и сэкономит время. ¦ Следует иметь в виду, что алгоритм, эффективный с точки зрения произво- производительности профаммы, практически никогда не соответствует фебованиям постановки задачи на все 100%, и наоборот. Неплохие с точки зрения сфук- туры и читабельности алгоритмы, как правило, неэффективны в плане реа- реализации профаммного кода. Одна из причин — стремление разработчика упростить общую сфуктуру профаммы за счет использования везде, где только можно, высокоуровневых вложенных структур для вычислений. Уп- Упрощение алгоритма в этом случае неизбежно ведет к снижению производи- производительности профаммы.
Введение 13_ В начале разработки алгоритма довольно сложно оценить, каким будет программный код приложения. Чтобы правильно разработать алгоритм программы, необходимо следовать нескольким простым правилам. ? Тщательно изучить задачу, для которой будет разработана программа. ? Определить основные требования к программе и представить их в фор- формализованном виде. ? Определить форму представления входных и выходных данных и их структуру, а также возможные ограничения. ? На основе этих данных определить программный вариант (или модель) реализации задачи. ? Выбрать метод реализации задачи. ? Разработать алгоритм реализации программного кода. Не следует путать алгоритм решения задачи с алгоритмом реализации программного кода. В общем случае, они никогда не совпадают. Это самый ответственный этап разработки программного обеспечения! ? Разработать исходный текст программы в соответствии с алгоритмом реализации программного кода. ? Провести отладку и тестирование программного кода разработанного приложения. Не следует воспринимать эти правила буквально. В каждом конкретном случае программист сам выбирает методику разработки программ. Некото- Некоторые этапы разработки приложения могут дополнительно детализироваться, а некоторые вообще отсутствовать. Для небольших задач достаточно разрабо- разработать алгоритм, слегка подправить его для реализации программного кода и затем отладить. При создании больших приложений, возможно, понадобится разрабатывать и тестировать отдельные фрагменты программного кода, что может потребо- потребовать дополнительной детализации программного алгоритма. Для правильной алгоритмизации задач программисту могут помочь многочис- многочисленные литературные источники. Принципы построения эффективных алго- алгоритмов достаточно хорошо разработаны. Имеется немало хорошей литературы по этой теме, например, книга Д. Кнута "Искусство программирования". Обычно разработчик программного обеспечения стремится к тому, чтобы производительность работы приложения как можно меньше зависела от ап- аппаратуры компьютера. При этом следует принимать во внимание наихудший вариант, когда у пользователя вашей программы будет далеко не самая по- последняя модель компьютера. В этом случае "ревизия" работы аппаратной части часто позволяет найти резервы для улучшения работы приложения.
J4 Введение Первое, что нужно сделать, — проанализировать производительность ком- компьютерной периферии, с которой должна работать программа. В любом слу- случае, знание того, что работает быстрее, а что медленнее, поможет при разра- разработке программы. Анализ пропускной способности системы позволяет определить узкие места и принять правильное решение. Различные устройства компьютера имеют разную пропускную способность. Наиболее быстрыми из них являются процессор и оперативная память, от- относительно медленными — жесткий диск и CD-привод. Самыми медлен- медленными являются принтеры, плоттеры и сканеры. Основная часть Windows-приложений разрабатывается с графическим поль- пользовательским интерфейсом и активно использует графические возможности компьютера. В этом случае при разработке приложения необходимо учесть пропускную способность системной шины и графической подсистемы ком- компьютера. Практически все приложения активно используют ресурсы жесткого диска. В большинстве случаев производительность дисковой подсистемы оказывает значительное влияние на работу приложения. Если программа интенсивно использует ресурсы жесткого диска, например, часто выполняет запись- перемещение файлов, то при относительно медленном жестком диске неиз- неизбежно возникнут проблемы с производительностью. Приведем другой пример. Преимущественное использование регистров цен- центрального процессора может повысить производительность программы за счет уменьшения обмена по системной шине, как это случается при работе с оперативной памятью. Во многих случаях повысить производительность приложения можно путем кэширования данных. Это может помочь при дисковых операциях, при работе с мышью, устройством печати и т. д. Если вы разрабатываете коммерческое приложение, то обязательно выясни- выясните, с какой наихудшей аппаратной конфигурацией будет работать ваша программа. Все мероприятия по оптимизации проводите с учетом именно такой конфигурации аппаратных средств. Использование такого метода оптимизации обычно связано с анализом программного кода на предмет выявления узких мест (bottlenecks) в процес- процессе функционирования приложения. Обычно точки, в которых программа значительно замедляет работу, выявить не так просто. В этом разработчику могут помочь специальные программы, называемые профайлерами (profiler). Их назначение — определить производительность приложений, помочь при отладке и выявить точки программы, в которых производительность падает. Одной из наилучших программ этого класса является Intel VTune Perform- Performance Analyzer. Я рекомендовал бы использовать именно эту программу для отладки и оптимизации приложений.
Введение • 15 Встроенные средства отладки имеются и в языках высокого уровня. Совре- Современные компиляторы позволяют обнаруживать ошибки, однако они не пре- предоставляют никакой информации об эффективности выполнения toro или иного участка программы. Вот почему желательно иметь под рукой какой- нибудь хороший профайлер. Многие программисты предпочитают вести отладку приложений вручную. Это не самый худший вариант, если вы хорошо представляете себе работу приложения. В любом случае, как бы вы не проводили отладку, полезно об- обратить внимание на некоторые моменты, которые могут повлиять на произ- производительность работы приложения. ? Количество вычислений, выполняемых программой. Одним из условий повышения производительности приложения является уменьшение объе- объема вычислений. Работающая программа не должна вычислять одно и то же значение дважды. Вместо этого она должна рассчитать каждое значе- значение один раз и сохранить его в памяти для повторного использования. Существенного повышения быстродействия приложения можно добить- добиться, если преобразовать математические вычисления в обращения к таб- таблицам, которые могут быть сгенерированы заранее. ? Использование математических операций. Любое приложение, так или иначе, использует математические операции. Анализ эффективности вы- вычислений довольно сложен и в каждом конкретном случае зависит от многих факторов. Выигрыш в ^.производительности может дать использо- использование более простых арифметических операций для вычислений. Везде, где только можно, операции умножения и деления следует заменить со- соответствующим блоком команд сложения/вычитания. Если в программе используются операции с плавающей точкой, то старайтесь не использо- использовать команды обработки целых чисел, т. к. они замедляют работу прило- приложения. Еще один нюанс: используйте по возможности как можно мень- меньше операций деления. Производительность заметно падает и при использовании математических операций в циклах. Операции умножения на степень двойки можно заменить командами сдвига влево. ? Использование циклических вычислений и вложенных структур. Речь идет об использовании циклов while, for, switch, if. Циклические вы- вычисления упрощают структуру программы, но уменьшают производи- производительность. Внимательно просматривайте программный код на предмет поиска вложенных вычислений с использованием циклических структур. Полезно помнить несколько правил, которые помогают при оптимиза- оптимизации циклов: • никогда не следует делать в цикле то, что можно выполнить за его пределами; • по возможности,избавляйтесь от команд передачи управления внутри циклов.
16 Введение Вынос за пределы цикла даже одного или двух операторов способен улучшить показатели производительности. Эффективной работе прило- приложения способствуют и такие действия, как вычисление неизменяющихся величин за пределами циклов, разворачивание циклов и объединение от- отдельных циклов, выполняемых одно и то же количество раз, в единый цикл. Следует избегать использования большого количества команд в те- теле цикла. Старайтесь применять меньше вызовов подпрограмм из тела цикла, т. к. вычисление эффективных адресов процедур может значи- значительно замедлить работу процессора. Полезным в плане улучшения производительности будет и уменьшение количества передач управления в программе. Для этого можно, напри- например, преобразовать условные переходы таким образом, чтобы условие пе- перехода становилось истинным значительно реже, чем условие его отсут- отсутствия. Полезно также перемещать условия общего характера в начало ветвления последовательности переходов. Если в программе есть вызовы, после которых следует возврат в программу, то желательно преобразовать такие вызовы в переходы. Подытоживая этот пункт, можно сделать следующий вывод: желательно избавляться от переходов и вызовов везде, где только можно, особенно в тех точках программы, где на быстродействие влияет только процессор. Для этого программа должна быть организована так, чтобы она исполня- исполнялась прямым (линейным) последовательным образом с минимальным числом точек переходов. О Реализация механизма многопоточности (multithreading). Правильное использование этого механизма в программе может повысить ее произво- производительность, а неправильное — наоборот, замедлить. Как показывает практика, использование многопоточности эффективно применять для больших приложений, небольшие же программы начинают работать мед- медленнее. Возможность разделения выполняемого процесса на несколько потоков заложена в архитектуре операционных систем Windows. Много- поточность можно использовать для оптимизации программ. Необходимо помнить, что каждый поток требует дополнительных ресурсов памяти и процессора, поэтому при слабой аппаратной поддержке (медленный про- процессор или недостаточный объем памяти) все усилия по улучшению про- производительности этим методом могут оказаться неэффективными. ? Выделение часто повторяющихся однотипных вычислений в отдельные подпрограммы (процедуры). Очень распространенным является мнение, что использование подпрограмм всегда повышает производительность приложений, т. к. позволяет многократно применить один и тот же фрагмент кода для выполнения однотипных вычислений в разных местах программы. С точки зрения читабельности программы и понимания ал- алгоритма работы, это действительно так. Но "с точки зрения процессора"
Введение 17 выполнение программы по линейному алгоритму всегда (!) эффективнее, чем использование процедур. Каждый раз, когда вы используете про- процедуру, выполняется переход по другому адресу памяти с сохранением адреса возврата в основную программу в стеке. Это всегда вызывает за- замедление выполнения программы. Все сказанное не означает, что нужно отказаться от использования подпрограмм, нужно лишь разумно приме- применять их в своих разработках. Использование языка ассемблера — это один из наиболее действенных ме- методов оптимизации программ, и- во многом методы, используемые для по- повышения производительности, схожи с теми, что используются в языках высокого уровня. Однако язык ассемблера предоставляет программисту и ряд дополнительных возможностей. Я не буду повторять то, что уже сказано в контексте оптимизации с использованием средств языков высокого уров- уровня, а выделю методы, свойственные только ассемблеру. П Использование языка ассемблера во многом решает проблему избыточ- избыточности программного кода. Ассемблерный код более компактен, чем его аналог на языке высокого уровня. Чтобы убедиться в этом, достаточно сравнить дизассемблированные листинги одной и той же профаммы, на- написанной на ассемблере и на языке высокого уровня. Даже использова- использование опций оптимизации компилятора не устраняет избыточность кода приложения, сгенерированного компилятором языка высокого уровня. В то же время язык ассемблера позволяет разрабатывать короткий и эф- эффективный код. ? Профаммный модуль на ассемблере обладает, как правило, более высо- высоким быстродействием, чем написанный на языке высокого уровня. Это связано с меньшим числом команд, требуемых для реализации фрагмента кода. Меньшее число команд быстрее выполняется центральным процес- процессором, что, соответственно, повышает производительность профаммы. ? Можно разрабатывать отдельные модули полностью на ассемблере и присоединять их к программам на языке высокого уровня. Также можно использовать мощные встроенные средства языков высокого уровня для написания ассемблерных процедур непосредственно в теле основной профаммы. Такие возможности предусмотрены практически во всех языках высокого уровня. Эффективность использования встроенного ас- ассемблера может быть очень высока. Встроенный ассемблер позволяет до- добиваться максимального эффекта при оптимизации математических вы- выражений, профаммных циклов и обработки массивов данных в основной профамме. В основе оптимизации с учетом специфических особенностей процессора лежат особенности архитектуры конкретного типа процессора Intel. Он представляет собой расширение варианта оптимизации с использованием языка ассемблера.
18 Введение Мы будем рассматривать варианты оптимизации только для процессоров Pentium. Каждая последующая модель процессора обычно имеет дополни- дополнительные архитектурные улучшения по сравнению с предыдущей. В то же время все модели процессоров Pentium имеют и общие характеристики. По- Поэтому оптимизация на уровне процессора может проводиться как на основе общих характеристик всего семейства, так и с учетом особенностей каждой модели. Оптимизация программного кода на уровне процессора позволяет повысить производительность не только приложений на языке высокого уровня, но и процедур, написанных на ассемблере. Программисты, пишущие на языках высокого уровня, практически незнакомы с этой методикой оптимизации и используют ее относительно редко, хотя ее возможности практически без- безграничны. Разработчики ассемблерных программ и процедур иногда исполь- используют возможности новых типов процессоров. Какие возможности процессора можно использовать для оптимизации? Прежде всего, будет полезным выравнивание данных и адресов по границам 32-разрядных слов. Кроме того, все процессоры, начиная с 80386, обладают расширенными вычислительными возможностями, которые можно исполь- использовать для оптимизации программ. Такие возможности появились благодаря дополнительным командам и расширению возможностей адресации операн- операндов. Производительность программ можно увеличить, используя: П команды пересылки с нулевым или знаковым расширением (movzx или movsx); ? установки в байте значений "истина" или "ложь" в зависимости от содер- содержимого флагов центрального процессора, что позволяет избавиться от команд условного перехода (setz, set с и т. д.); П команды проверки, установки, сброса и сканирования битов (bt, btc, btr, bts, bsp, bsr); ? обобщенную индексную адресацию и режимы адресации с масштабиро- масштабированием индексов; ? быстрое умножение при помощи команды lea с использованием мас- масштабированной индексной адресации; ? перемножение 32-разрядных чисел и деление 64-разрядного числа на 32- разрядное; ? операции для обработки многобайтных массивов данных и строк. Команды процессора, выполняющие копирование и перемещение массивов многобайтных данных, требуют меньше циклов процессора, чем классиче- классические команды этой группы. Начиная с процессоров ММХ, появились ком- комплексные команды, сочетающие в себе несколько функций, которые ранее могли быть выполнены только несколькими отдельными командами. Значи-
Введение 19_ тельно расширилась группа команд для выполнения битовых операций. Эти команды также являются комплексными и позволяют выполнить несколько операций одновременно. Мы рассмотрим возможности, предоставляемые такими командами, в главе 10, когда будем анализировать встроенные сред- средства языков высокого уровня. Как вы уже убедились, большие возможности для оптимизации программ кроются в правильном использовании особенностей аппаратной архитекту- архитектуры процессора. Это довольно сложная сфера, т. к. требует знания методов обработки данных и выполнения команд процессора на уровне аппаратной части. Могу с уверенностью утверждать, что возможности для оптимизации здесь практически безграничны. Естественно, оптимизация на уровне процессора имеет свои особенности. Например, если программа должна работать с процессорами нескольких по- поколений, то оптимизация должна учитывать общие особенности всех этих устройств. Здесь представлен далеко не полный перечень возможных вариантов опти- оптимизации программного кода приложений. Как очевидно, большие резервы для повышения эффективности работы программы кроются в самой про- программе. В книге основное внимание будет уделено оптимизации программ- программного кода с использованием языка ассемблера, поэтому далее рассмотрим более детально, как решаются такие задачи. Язык ассемблера, как средство улучшения производительности приложений, написанных на языках высокого уровня, используется очень широко. Ра- Разумное сочетание в одном приложении модулей, написанных на языке вы- высокого уровня и на ассемблере, позволяет достичь как высокого быстродей- быстродействия работы программы, так и уменьшения размера исполняемого кода.#В настоящее время такое сочетание используется настолько часто, что фирмы- разработчики компиляторов уделяют особое внимание интерфейсу программ на языках высокого уровня с процедурами на ассемблере. Современные компиляторы языков высокого уровня имеют, как правило, встроенный ас- ассемблер. На практике применяются два варианта совместного использования ассемб- ассемблера и языков высокого уровня. В первом случае используется отдельный файл объектного модуля, в котором располагается одна или несколько про- процедур обработки данных. Вызов этих процедур осуществляется программой, написанной с использованием высокоуровневой среды разработки, напри- например, Visual C++ .NET. В исходном тексте приложения на языке высокого уровня ассемблерная процедура объявляется соответствующим образом, после чего ее можно вы- вызвать из любой точки основной программы. Внешний объектный модуль на ассемблере присоединяется к основной программе на этапе компоновки.
20 Введение Файл с исходным текстом процедуры обычно имеет расширение asm и ком- компилируется одним из распространенных пакетов, таких как Microsoft Macro Assembler (MASM), Borland Turbo Assembler (TASM 5.0) или Netwide Assem- Assembler (NASM). Последний компилятор превосходит первые два по своим воз- возможностям, однако так сложилось, что в странах СНГ наиболее популяр- популярными являются все же компиляторы MASM и TASM. Преимущества отдельно компилируемых модулей на ассемблере — это воз- возможность использования программного кода в приложениях, написанных на разных языках и даже в разных операционных средах, и независимость процесса разработки и отладки программного кода процедур. К недостат- недостаткам, пожалуй, можно отнести некоторые сложности компоновки разрабо- разработанного модуля с основной программой на языке высокого уровня. При этом необходимо четко представлять механизм вызова внешних процедур и передачи параметров в вызываемую процедуру. Преимущества такого подхо- подхода — многократное использование разработанных на ассемблере объектных модулей или библиотек функций. В этом случае программист должен поза- позаботиться об интерфейсе ассемблерного модуля с программой, написанной на языке высокого уровня. Вопросы компоновки ассемблерных модулей и программ на языке C++ подробно будут рассматриваться в главе 7. Второй вариант совместного использования ассемблера и языков высокого уровня основан на применении встроенного ассемблера. Разработка про- процедур на встроенном ассемблере удобна, в первую очередь, благодаря быст- быстроте отладки. Так как процедура разрабатывается в теле основной програм- программы, то не требуется специальных средств для компоновки такой процедуры с вызывающей программой. Не нужно также заботиться о порядке передачи параметров в вызываемую процедуру и о восстановлении стека. К недостат- недостаткам этого метода оптимизации можно отнести определенные ограничения, которые накладывает среда программирования на работу ассемблерных мо- модулей, а также то, что процедуры, разработанные на встроенном ассемблере, нельзя преобразовать во внешние отдельно используемые модули. Практически все современные средства разработки ассемблерных программ имеют в своем составе интегрированный отладчик, так же как и языки вы- высокого уровня. И хотя такой отладчик обычно предоставляет меньший уро- уровень сервиса по сравнению с языками высокого уровня, он вполне достато- достаточен для анализа программного кода. Несмотря на то, что ассемблер воспринимается многими программистами только как вспомогательное средство для улучшения программ, его значение как самостоятельного средства разработки высокоэффективных приложений в последнее время очень изменилось. До сих пор существует некий стереотип, касающийся разработки приложе- приложений на ассемблере. Среди многих программистов, пишущих на языках вы- высокого уровня, бытует мнение о сложности ассемблера, плохой структури-
Введение 21_ руемости программного обеспечения и плохой переносимости кода при переходе на другие платформы. Возможно, многие помнят разработку про- программ на ассемблере в MS-DOS, что действительно требовало немалых уси- усилий. Кроме того, отсутствие в то время современных средств программиро- программирования на ассемблере замедляло разработку сложных проектов. В последнее время ситуация изменились благодаря появлению принципи- принципиально новых и эффективных средств быстрой разработки на языке ассемб- ассемблера. Специально для этого были разработаны мощные средства быстрого проектирования (Rapid Application Development — RAD), такие как MASM32, Visual Assembler, RADASM. Размер и быстродействие оконного приложения SDI (single-document interface), написанного на ассемблере, просто впечатляет! Такие средства разработки имеют, как правило, компиляторы ресурсов, большие библиотеки готовых к Использованию функций и мощные средства отладки. Можно смело утверждать, что разработка программ на ассемблере стала столь же легкой, как и на языках высокого уровня. Основная причина, по которой ассемблер не применялся массово для раз- разработки программ, — отсутствие средств быстрого проектирования — исчез- исчезла. Какие приложения можно проектировать на ассемблере? Проще отве- ответить на другой вопрос — что не следует писать на ассемблере. Небольшие и средние по объему 32-разрядные приложения для Windows можно целиком написать на ассемблере. Однако при разработке сложной программы, тре- требующей применения самых современных технологий, лучше использовать языки высокого уровня с последующей оптимизацией отдельных участков кода на ассемблере. Существует еще одна проблема использования ассемблера, связанная с тем, что этот язык рассчитан на разработку процедурно-ориентированных при- приложений и не использует методы объектно-ориентированного программиро- программирования (ООП). Именно это приводит к некоторым ограничениям при ис- использовании ассемблера. Тем не менее, это никак не мешает применять язык ассемблера для написания классических Windows-приложений про- процедурно-ориентированного типа. Современные средства разработки программ на ассемблере не только позво- позволяют создать графический интерфейс пользователя, но и сохраняют фунда- фундаментальное преимущество ассемблера: фантастически малый размер испол- исполняемого модуля. Короткие быстрые приложения на ассемблере находят применение там, где размеры кода и его быстродействие являются критиче- критическими параметрами. Сферами применения таких приложений являются сис- системы реального времени, системные утилиты и программы, а также драйве- драйверы устройств. Программы на ассемблере управляют как периферийным оборудованием персонального компьютера (ПК), так и нестандартными устройствами, при-
22 Введение соединенными к ПК. Минимальные размеры программного кода обеспечи- обеспечивают высокое быстродействие работы таких устройств. Приложения реаль- реального времени используются повсеместно в системах управления в промыш- промышленности, научных и лабораторных исследованиях, в военных разработках. Особенность системных программ и утилит состоит в том, что они очень тесно взаимодействуют с операционной системой, и скорость выполнения таких приложений может существенно повлиять на общую производитель- производительность всей системы. Это в значительной степени относится и к разработке драйверов периферийных устройств компьютера и системных служб. Средства разработки на ассемблере позволяют создавать и быстрые утилиты командной строки (консольные приложения). Использование в таких ути- утилитах системных вызовов Windows позволяет выполнить очень многие сложные функции (копирование файлов, функции поиска и сортировки, обработка и анализ математических выражений и т. д.) с очень высоким бы- быстродействием. Другой важной областью применения ассемблера является разработка драй- драйверов нестандартных и специализированных устройств, управляемых при помощи ПК. В таких случаях использование программ на языке ассемблера будет очень эффективным. Можно привести много примеров такого исполь- использования ассемблера. Это и системы обработки данных на базе ПК с исполь- использованием выносных устройств, одноплатные компьютеры с флэш-памятью, системы диагностики и тестирования различного оборудования. Необходимо также упомянуть еще об одном аспекте применения языка ас- ассемблера, достаточно экзотическом, но, тем не менее, используемом. Ос- Основная программа пишется на ассемблере, а вспомогательные модули — на любом другом языке, например, на C++. При этом основная программа ис- использует, как правило, мощные библиотечные функции языка высокого уровня, например, математические или строковые. Кроме того, если для разработки интерфейса используются вызовы WIN API (Application Pro- Programming Interface), то программа получается очень мощной. Конечно, на- написание таких программ требует от программиста незаурядных знаний ас- ассемблера и языков высокого уровня. Мы рассмотрели далеко не все методы улучшения качества программного обеспечения. Существует масса трюков и ухищрений, которыми пользуются опытные программисты для улучшения показателей производительности. Оптимизация программ, как я уже упоминал, процесс творческий, и каждый программист весьма индивидуален в выборе методики отладки своих про- программ.
ЧАСТЬ I Основы эффективного ПРОГРАММИРОВАНИЯ НА АССЕМБЛЕРЕ
Глава 1 Оптимизация ассемблерного кода для процессоров Pentium Применение языка ассемблера — это одно из наиболее действенных средств оптимизации программ, и во многом методы, используемые для повышения производительности, схожи с теми, что используются в языках высокого уровня. Однако язык ассемблера предоставляет профаммисту и ряд допол- дополнительных возможностей. Я не буду повторять то, что уже сказано в контек- контексте оптимизации с использованием языков высокого уровня, а выделю ме- методы, свойственные только ассемблеру. Использование языка ассемблера во многом решает проблему избыточности профаммного кода. Ассемблерный код более компактен, чем его аналог на язы- языке высокого уровня. Чтобы убедиться в этом, достаточно сравнить дизассембли- рованные листинги одной и той же профаммы, написанной на ассемблере и на языке высокого уровня. Ассемблерный код, полученный в результате компиля- компиляции на языке высокого уровня, даже с использованием опций оптимизации, не устраняет избыточность кода приложения. В то же время язык ассемблера по- позволяет разрабатывать короткий и эффективный код. Профаммный модуль на ассемблере обладает, как правило, более высоким бысфодействием, чем написанный на языке высокого уровня. Это связано с меньшим числом команд, требуемых для реализации фрагмента кода. Меньшее число команд бьклрее выполняется центральным процессором, что, соответственно, повышает производительность профаммы. Несмотря на очевидные преимущества языка ассемблера' для оптимизации приложений, немногие профаммисты широко применяют его на практике. Одна из основных причин — кажущаяся сложность этого языка и необходи- необходимость хороших знаний архитектуры процессора. Конечно, ассемблер сложнее, чем языки высокого уровня, но время, пофаченное на его изучение, с лихвой окупится при написании высокопроизводительных приложений. Рассмотрим возможные направления оптимизации приложений с помощью ассемблера. Первый вариант — включить в профаммный код на C++ от- отдельный ассемблерный модуль, в который поместить функцию или фуппу
_26 Часть I. Основы эффективного программирования на ассемблере функций, выполняющих определенные вычисления. Такие модули разраба- разрабатываются отдельно от основной программы и компилируются с помощью автономного компилятора ассемблера, например, MASM фирмы Microsoft или TASM фирмы Borland. Получившийся файл объектного модуля с рас- расширением OBJ можно затем включить в проект приложения на C++ .NET и использовать функции этого модуля в соответствии с соглашениями о вызо- вызовах. Такой подход имеет определенные преимущества по сравнению с ос- остальными: ? разработанный объектный модуль можно использовать в разных прило- приложениях; ? не нужно особым образом (как это делается в библиотеках DLL) разраба- разрабатывать интерфейс функций для доступа к ним из других программ; ? ассемблерный модуль проще отлаживать, используя для этого его собст- собственную среду разработки. Следует отметить, что отдельно разработанные ассемблерные модули могут содержать несколько взаимосвязанных функций, составляющих целый вы- вычислительный блок. Второй вариант совместного использования ассемблера и языков высокого уровня основан на применении встроенного ассемблера. Разработка процедур на встроенном ассемблере удобна, в первую очередь, благодаря быстроте от- отладки. Так как процедура разрабатывается в теле основной программы, то не требуется специальных средств для компоновки такой процедуры с вызываю- вызывающей программой. Не нужно также заботиться ни о порядке передачи парамет- параметров в вызываемую процедуру, ни о восстановлении стека. Реализация функ- функций на встроенном ассемблере не требует написания пролога и эпилога, как это необходимо в случае разработки отдельных ассемблерных модулей. К недостаткам этого метода оптимизации можно отнести определенные ог- ограничения, которые накладывает среда программирования на работу ас- ассемблерных модулей, а также то, что процедуры, разработанные на встроен- встроенном ассемблере, нельзя преобразовать во внешние отдельно используемые модули. Какие части программного кода, написанные на C++ .NET, можно оптими- оптимизировать с помощью ассемблера? Прежде Всего, КОНСТРУКЦИИ типа while, do ... while, if ... else, switch ... case. Подобные конструкции могут быть легко заменены их ассемблерными эквивалентами. Необходимость в такой замене возникает довольно часто, особенно в случае многократно повторяющихся однотип- однотипных вычислений. Сэкономив несколько инструкций в каждом цикле таких вычислений, можно добиться существенного выигрыша в производительно- производительности приложения. Это утверждение справедливо как для отдельно скомпили- скомпилированных модулей, так и для ассемблерных блоков и функций C++ .NET.
Глава 1. Оптимизация ассемблерного кода для процессоров Pentium 27_ Организация вычислительных алгоритмов внутри циклов while и do ... while также оптимизируется с помощью ассемблера. Если вычисле- вычисления внутри такого блока можно реализовать с помощью нескольких ассемб- ассемблерных команд, то лучше всего подходит встроенный ассемблер. Примене- Применение отдельно скомпилированных модулей в этом случае не дает должного эффекта, а может даже замедлить работу программы. Дело здесь в том, что отдельный ассемблерный модуль должен содержать законченные функции, требующие каждый раз выполнения команд пролога-эпилога при вызове. Это означает, что каждый раз при вызове функций из отдельного модуля требуется сохранять определенные регистры в стеке и затем их восстанавли- восстанавливать. На это может потребоваться несколько команд ассемблера. Для не- небольших программ замедление будет незаметно, но для серьезных приложе- приложений оно будет существенным. Конструкции if ... else труднее оптимизируются, но и здесь существуют определенные приемы, позволяющие добиться неплохих результатов. На- Например, вместо вычисления двух условий перехода можно использовать од- одно условие и один оператор присваивания. Большие резервы для оптимизации приложений на C++ .NET связаны с оптимизацией математических операций. Дело в том, что при циклических вычислениях многократно повторяющийся фрагмент программного кода может быть реализован на ассемблере, причем несколькими способами. Операции с целыми числами обычно легко переводятся на язык ассемблера, намного сложнее это сделать для чисел с плавающей точкой. Существенную роль в оптимизации математических операций играет математический со- сопроцессор или FPU (Floating-Point Unit, устройство для выполнения опера- операций с плавающей точкой). Сопроцессор добавляет дополнительные арифметические возможности в систему, но не замещает ни одну команду основного процессора. Команды add, sub, mui и div выполняются центральным процессором, а математиче- математический сопроцессор выполняет дополнительные, более эффективные команды арифметической обработки. С точки зрения программиста, система с со- сопроцессором выглядит как единый процессор с большим набором команд. Особую роль в процессе оптимизации приложений играют технологии SIMD (Single Instruction — Multiple Data, одна команда — много данных), реализованные в виде целочисленного расширения ММХ (MultiMedia extensions, расширенный набор команд для мультимедийных приложений) и расширения для вычислений с плавающей точкой SSE (Streaming SIMD Extensions), позволяющие параллельно обрабатывать несколько операндов. Эти технологии появились относительно недавно, в последних поколениях процессоров. Для их успешного использования программист должен знать архитектуру процессора, систему команд и понимать, как используются те или иные функциональные части процессора (регистры, кэш1, конвейер ко-
28_ Часть I. Основы эффективного программирования на ассемблере манд, арифметико-логическое устройство, блок обработки данных с пла- плавающей точкой и т. п.). Следует отметить, что, поскольку язык ассемблера является наиболее близ- близким к машинному языку, то и все преимущества новых поколений процес- процессоров немедленно отражаются и в программировании с использованием ас- ассемблера. Ранние модели процессоров были не очень сложны в плане архитектуры, включали в свой состав небольшое количество команд ассемблера и опери- оперировали с весьма ограниченным набором регистров. Эти обстоятельства не позволяли серьезно оптимизировать приложения, применяя ассемблер. По- Поэтому ассемблер в основном использовался для доступа к аппаратным ре- ресурсам персональных компьютеров, написания драйверов устройств. Дол- Должен заметить, что и более ранние типы процессоров Intel включают дополнительные команды, которые редко применяются разработчиками, но позволяют сделать программный код более эффективным. Оптимизация программ на языке высокого уровня с использованием ас- ассемблера — довольно кропотливое занятие, однако с ее помощью, по раз- разным оценкам, можно добиться повышения производительности программ от 3—4 до 14—17 процентов. Программы на языках высокого уровня могут быть улучшены как за счет применения ассемблерного кода в отдельных участках программы, так и за счет реализации вычислительных алгоритмов полностью на ассемблере. На практике эффективность применения языка ассемблера достигается при: ? оптимизации циклических вычислений; ? оптимизации обработки больших объемов данных за счет применения специальных команд для манипуляций с символьными данными (команды строковых примитивов). На самом деле команды строковых примитивов.позволяют обрабатывать не только символьные, но и число- числовые данные; П оптимизации математических вычислений. Здесь следует сказать о том, что повышение производительности приложений достигается как с по- помощью обычных команд арифметических операций, так и с помощью команд математического сопроцессора (FPU). Особое место занимает технология SIMD, позволяющая в несколько раз повысить производи- производительность приложений. Кроме перечисленных выше пунктов большое значение имеет правильное сочетание ассемблера и языка высокого уровня, которое зависит от опыта и знаний самого разработчика, хотя даже в такой сложной сфере, как разра- разработка оптимальных программ с помощью ассемблера, существуют опреде- определенные эмпирические правила, позволяющие добиться больших или мень- меньших успехов.
Глава 1. Оптимизация ассемблерного кода для процессоров Pentium 29_ Однако простая замена, к примеру, цикла while в программе на языке C++ на ее ассемблерный эквивалент может и не дать ожидаемого выигрыша в производительности. То же самое касается и других ассемблерных аналогов операторов языка высокого уровня. Во многих случаях выигрыш в произво- производительности достигается лишь при тщательном анализе того или иного фрагмента программного кода. Тот же цикл while можно реализовать не- несколькими способами с помощью ассемблера. При этом реализация может даже отличаться от классических вычислительных схем, описанных в лите- литературе по языку ассемблера. Для того чтобы оптимизировать тот или иной фрагмент программы на C++ с помощью ассемблера, мало знать команды ассемблера и их синтаксис. Необходимо понимать, как работают эти коман- команды в тех или иных комбинациях, ведь можно применить ассемблер и. не по- получить никакого выигрыша! Весь последующий материал этой главы посвящен анализу того, как можно построить высокопроизводительные вычислительные алгоритмы на языке ассемблера. Я не буду касаться темы оптимизации высокоуровневых конст- конструкций языка C++ .NET 2003, это отдельная тема и рассматриваться она будет в главе 4. Здесь же будут рассмотрены только принципы оптимального программирования на языке ассемблера. Начну с программирования циклических вычислений на ассемблере. В наи- наиболее общем виде цикл представляет собой последовательность команд, на- начинающихся с метки и возвращающихся на нее после выполнения этой по- последовательности. На ассемблере цикл обычно выглядит так: label: <команды> jmp label Например, последовательность команд mov EDX, 0 Label: <ассемблерный код> inc EDX cmp EDX, 1000000 je OutLoop jmp Label OutLoop: представляет собой простой цикл. В этом цикле выполняется увеличение содержимого регистра edx от 0 до 1 000 000 и при достижении этого значе- значения передается управление на метку OutLoop. Этот фрагмент кода нельзя назвать оптимальным, поскольку для анализа условия выхода из цикла и самого выхода из цикла используется несколько
3? Часть I. Основы эффективного программирования на ассемблере команд. Дальнейшего улучшения программного кода можно достичь, если проанализировать, как работает та или иная циклическая последователь- последовательность операторов. Если цикл имеет фиксированное число итераций, то ре- решение для его оптимизации на ассемблере выглядит довольно просто (листинг 1.1). mov EDX, 1000000 label: Ассемблерный код> dec EDX jnz label Как видно из этого листинга, значение счетчика цикла помещается в ре- регистр edx. Этот фрагмент более эффективен, чем предыдущий. Коман- Команда декремента устанавливает флаг zf, когда счетчик становится равным 0. В этом случае происходит выход из цикла, иначе цикл продолжается. Как видите, этот фрагмент кода требует меньшего числа операторов и будет вы- выполняться быстрее. Наконец, можно разработать еще более эффективный вариант цикла с дек- декрементом. В этом случае можно использовать команду ассемблера, запись которой в псевдокоде выглядит как cmovcc, где последние две литеры обо- обозначают одно из условий (eq, le, ge и т. п.). Цикл с декрементом, показан- показанный в листинге 1.1, легко модифицируется (листинг 1.2). lea EAX, L1 lea ECX, L2 mov EDX, 1000000 LI: Ассемблерный код>
Глава 1. Оптимизация ассемблерного кода для процессоров Pentium 31_ dec EDX cmovz ЕАХ, ЕСХ jmp ЕАХ L2: Команда cmovz в этом фрагменте кода выполняет две функции: анализ флага zf и пересылку данных в регистр еах в случае установки zf. Следую- Следующая за cmovz команда jmp использует содержимое регистра еах для перехода на нужную ветвь программы. Предыдущие фрагменты программного кода показывают, что оптимизация циклов, написанных на ассемблере, во многом зависит от конкретной зада- задачи, и единого рецепта здесь не существует. Например, программа выполняет какие-либо преобразования над данными размером в 1 байт, находящимися в массиве, и необходимо инкрементировать адрес байта в каждой итерации цикла. Предположим, что адрес источника находится в регистре esi, адрес приемника в регистре edi, а значение счетчика байтов помещено в регистр есх. В этом случае циклическое выполнение алгоритма обработки может быть реализовано следующим образом (листинг 1.3). Комментарии в исход- исходном тексте начинаются точкой с запятой. ; помещаем адрес источника ESI mov ESI, src ; помещаем адрес приемника в EDI mov EDI, dst ; помещаем значение счетчика байт в ЕСХ mov ЕСХ, count ; прибавим адрес в ESI к ЕСХ ; чтобы получить условие выхода из цикла add ЕСХ, ESI
32 Часть I. Основы эффективного программирования на ассемблере label: mov AL, [ESI] inc ESI ; обработка байта <команды обработки байта> ; записьшаем результат обработки в приемник mov [EDI], AL inc EDI ; проверка на выполнение условия выхода . cmp ECX, ESI ; если условие не выполняется, повторяем цикл jne label Хочу отметить, что даже хорошо оптимизированный цикл иногда не работа- работает столь быстро, как ожидает разработчик. Для дальнейшего повышения эффективности используют так называемое разворачивание (unrolling) цик- цикла. Этот термин означает на самом деле, что цикл должен выполнять боль- больше действий в одной итерации для уменьшения количества итераций. Этот способ дает неплохой эффект, и сейчас мы рассмотрим два фрагмента про- программного кода, в которых используется разворачивание циклов. В качестве исходного (не оптимизированного) фрагмента кода возьмем, на- например, копирование данных размером в двойное слово из одного буфера памяти в другой. Исходный текст фрагмента показан в листинге 1.4. Ли с г и н г '!. 4 К о г i и \; о -з з ;¦¦< \ле /л. о < :¦¦ г н s >; х с л о о ,'до о п т и v и::, i;.; и *<) поместим адреса источника и приемника данных в регистры ESI и EDI mov ESI, src mov EDI, dst
Глава 1. Оптимизация ассемблерного кода для процессоров Pentium 33_ ; поместим значение счетчика байт в ЕСХ mov ЕСХ, count ; считаем двойные слова, поэтому значение ; в ЕСХ делим на 4 shr ЕСХ, 2 label: mov ЕАХ, [ESI] add ESI, 4 mov [EDI], EAX add EDI, 4 dec ECX jnz label Чтобы развернуть цикл, будем одновременно выполнять копирование двух двойных слов. Исходный текст оптимизированного фрагмента кода показан в листинге 1.5 (необходимые изменения выделены жирным шрифтом). mov ESI, src mov EDI, dst ; значение счетчика поместим в регистр ЕСХ mov ECX, count ; значение счетчика делим на 8 (используются двойные ; слова!) shr ECX, 3 label: ; читаем первое двойное слово в регистр ЕАХ
34 Часть /. Основы эффективного программирования на ассемблере mov EAX, [ESI] ; читаем второе двойное слово в регистр ЕВХ mov ЕВХ, [ESI + 4] ; записываем первое двойное слово в регистр EDI mov [EDI], EAX ; записываем второе двойное слово по адресу в EDI на 4 ; больше предыдущего mov [EDI + 4], ЕВХ ; продвигаем адреса источника и приемника так, чтобы они ; указывали на следующее двойное слово add ESI, 8 add EDI, 8 ; анализ конца цикла dec ECX jnz label Применение подобной техники уменьшит замедление программы, привне- привнесенное циклом, наполовину. Можно и далее развернуть цикл, если опери- оперировать не двумя, а четырьмя двойными словами. Еще один пример разворачивания циклов. Пусть имеется массив из 20-ти це- целых чисел. Необходимо присвоить элементам массива с четными номерами (О, 2, 4 и т. д.) значение 0, а элементам с нечетными номерами — значение 1. Решение задачи "в лоб" показано в листинге 1.6. ; помещаем число элементов массива в регистр ЕСХ
Глава 1. Оптимизация ассемблерного кода для процессоров Pentium 35_ mov ЕСХ, 20 ; помещаем адрес первого элемента массива в регистр ESI lea ESI, il ; помещаем делитель 2 в регистр ЕВХ для использования при определении' ; четного-нечетного элемента mov ЕВХ, 2 next: ; помещаем счетчик элементов в регистр ЕАХ mov ЕАХ, ЕСХ ; определяем: четный или нечетный порядковый номер элемента массива div ЕВХ стар EDX, 0 ; если нечетный, присваиваем элементу значение 1 jne set_l ; если четный, присваиваем элементу значение 0 mov DWORD PTR [ESI], 0 jmp LI set_l: mov DWORD PTR [ESI], 1 LI: ; переход к адресу следующего элемента массива add ESI, 4 loop next
jg Часть I. Основы эффективного программирования на ассемблере Этот фрагмент программного кода можно оптимизировать, если опериро- оперировать в одной итерации двумя двойными словами. Исходный текст модифи- модифицированного варианта кода показан в листинге 1.7 (курсивом выделены команды ассемблера, которые мы убираем). mov EDX, О mov ECX, 20 lea ESI, il mov EBX, 2 next mov E?X, div EBX cmp EDX, 0 mov DWORD PTR [ESI], 0 mov DWORD PTR [ESI+4] , 1 add EDX, 2 ; jmp LI ;set_l: ; mov DWORD PTR [ESI], 1 ;L1: cmp EDX, 19 jae ex add ESI, 8 jmp next ex: В исходном тексте этого фрагмента кода есть существенные отличия по сравнению с исходным текстом листинга 1.6. Мы избавились от команд де- деления и одновременно уменьшили число итераций в два раза (команда add edx, 2 выделена жирным шрифтом). В каждой итерации обрабатыва- обрабатываются одновременно два элемента массива (команды mov DWORD PTR [ESI], о и mov dword ptr [esi+4], l выделены жирным шрифтом). В конце ка- каждой итерации содержимое регистра esi увеличивается на 8 с помощью команды add esi, 8, указывая на следующую пару элементов.
Глава 1. Оптимизация ассемблерного кода для процессоров Pentium 37_ Разработаем простое консольное приложение в C++ .NET 2003, в котором используется оптимизированный алгоритм. В этом приложении вызывается функция initarr из отдельного ассемблерного модуля, которая и выполняет обработку массива. Я пока не буду анализировать интерфейс ассемблерных функций и про- программы на C++, это будет сделано в следующих главах. Основная задача этого примера — продемонстрировать технику оптимизации ассемблерного кода, поэтому сосредоточимся именно на ней. Исходный текст оптимизированного ассемблерного модуля, содержащего функцию initarr и адаптированного для компиляции в среде макроассемб- макроассемблера MASM 6.14, показан в листинге 1.8. .686 .model flat public _initarr .code _initarr proc ; пролог функции push EBP mov EBP, ESP ; в. регистр ЕСХ помещается размер массива, а в регистр ; ESI — адрес первого элемента mov ЕСХ, DWORD PTR [ЕВР+12] mov ESI, DWORD PTR [EBP+8] mov EDX, 0 dec ECX next: mov DWORD PTR [ESI], 0 mov DWORD PTR [ESI+4], 1 add EDX, 2 cmp EDX, ECX jae ex
38 Часть I. Основы эффективного программирования на ассемблере add ESI, 8 jmp next ex: ; эпилог функции pop EBP ret _initarr endp end Исходный текст консольного приложения C++ .NET показан в листинге 1.9. ¦ Листинг 1.3. Инициализации и отображение ьяяссивы целых чясел // UNROLL_LOOP_OPT.срр : Defines the entry point for the console // application. #include "stdafx.h" extern "C" void initarr(int* pil/ int isize); int _tmain(int argc, _TCHAR* argv[]) { int il[20]; printf ("UNROLLING LOOP DEMO\n\n"); printf("il[20] = "); initarr(il, 20); for (int cnt = 0;cnt < 20;cnt++) printf("%d ", il[cnt]); , getchar(); return 0; Окно приложения (рис. 1.1) демонстрирует содержимое массива после ини- инициализации. Мы рассмотрели некоторые наиболее общие варианты оптимизации цикли- циклических вычислений на ассемблере. Хотя существует и много других спосо- способов, но все они имеют определенную специфику, связанную с типом про-
Глава 1. Оптимизация ассемблерного кода для процессоров Pentium 39 цессора. Детальный анализ оптимизации циклов для различных типов про- процессоров (Pentium Pro, Pentium II, Pentium III, Pentium 4) выходит за рамки этой книги. Рис. 1.1. Окно приложения, демонстрирующего результат работы ассемблерной функции с оптимизированным циклом Еще одним фактором, оказывающим существенное влияние на производи- производительность, является использование команд условных переходов. Наиболее общая рекомендация (она касается всех типов процессоров) — желательно избегать условных переходов с использованием флагов. Это связано с наличием в последних поколениях процессоров Pentium мик- микропрограммных средств предсказания ветвлений и особенностями их функ- функционирования. Иногда можно достичь того же эффекта, что и в случае ис- использования команд условных переходов, если выполнить некоторые манипуляции с битами. В качестве примера рассмотрим вычисление абсо- абсолютного значения числа со знаком с использованием и без использования команд условных переходов. Число находится в регистре еах. Далее представлен фрагмент кода, выполняющий вычисление модуля числа обычным способом, с использованием команд условных переходов (лис- (листинг 1.10). стр ЕАХ, 0 jge ex neg EAX ex:
40 Часть I. Основы эффективного программирования на ассемблере Следующий фрагмент кода выполняет то же самое, но без команды jge (листинг 1.11). cdq xor EAX,EDX sub EAX,EDX Для подобных вычислений очень полезным оказывается использование флага переноса cf. Рассмотрим еще один пример, в котором, при его стан- стандартной реализации, присутствуют команды условных переходов. Пусть тре- требуется сравнить два целых числа (назовем их ii и i2) и присвоить ii значе- значение i2, если 12 < ii. Для большей ясности представим это в виде оператора C++ .NET: if (b < а) а = b; Классический вариант ассемблерного кода для этой задачи представлен в листинге 1.12. '.'.2 ФраГМСаТ аССО^ЬлсрПОГС KU.aa ?Г:',-. Ш..!'?ИСЛ J/НИИ Lit *ip3>t:-.-1 j t'ijs '.. i::' ¦¦.':: •¦¦ ¦¦-. '¦¦ : " h С И С i i О Г; '<¦ j :i С R Я '¦< И 'г \" '¦•¦ О М J У :.; у С Л О F i \ ¦ i ¦ ¦ .< '¦' е % Кг- X О ДО В запомнить содержимое переменной il в регистре ЕАХ mov ЕАХ, DWORD PTR II сравнить содержимое регистра ЕАХ со значением переменной i2 cmp ЕАХ, DWORD PTR 12 если il > i2, il присвоить i2 jae set_i2_to__il jmp ex set i2 to il:
Глава 1. Оптимизация ассемблерного кода для процессоров Pentium ?/_ ; обменять содержимое ЕАХ и i2 xchg EAX, DWORD PTR 12 ; запомнить содержимое ЕАХ в переменной il mov DWORD PTR II, ЕАХ ex: Реализация этого алгоритма без использования команд условных переходов выглядит более изящно и выполняется быстрее (листинг 1.13). Листинг 1.13. Фрошокт ассемблерного код л цля оычисяония и .:. a {Ь < -л) а - Ь б с 2 и с п о л ь з о з а н и л команд у с л ::> г? п <¦ ¦,; х п о р о > о п о с mov ЕАХ, DWORD PTR II mov EDX, DWORD PTR I2 sub EDX,EAX sbb ECX,ECX and ECX,EDX add EAX,ECX mov DWORD PTR II, EAX В следующем примере осуществляется выбор одного из двух чисел в соот- соответствии с псевдокодом if (il.'= 0) il = i2; else il = i3; Классический вариант, в котором используются команды условных перехо- переходов, можно представить следующим исходным текстом (листинг 1.14). ; запомнить содержимое переменных il — i3 в регистрах ЕАХ, EDX, ECX
_4? Часть I. Основы эффективного программирования на ассемблере mov EAX, DWORD PTR II mov EDX, DWORD PTR 12 mov ECX, DWORD PTR 13 ; il = 0? amp EAX, 0 ; нет, il = i2 jne set_i2_to_il ; да, il = i3 mov EAX, ECX jmp ex set_i2_to_il: mov EAX, EDX ex: mov DWORD PTR 13, EAX Далее представлен исходный текст фрагмента кода без команд условных пе- переходов (листинг 1.15). mov EAX, DWORD PTR II mov EDX, DWORD PTR 12 mov ECX, DWORD PTR 13 cmp EAX,1 sbb EAX,EAX xor ECX,EDX and EAX,ECX xor EAX,EDX mov DWORD PTR 13, EAX
Глава 1. Оптимизация ассемблерного кода для процессоров Pentium 43_ Хочу заметить, что производительность приложения в целом во многом за- зависит от того, как такой фрагмент кода взаимодействует с остальной ча- частью приложения. Если избежать команд условных переходов нельзя, то можно попытаться оптимизировать фрагмент кода, правильно организовав ветвления. Что это означает? * Рассмотрим такой пример. Пусть имеется участок, на котором выполняется последовательность команд test ЕАХ,ЕАХ j z label_A <команды ветви 1> jmp label_B label_A: <команды ветви 2> label_B: Предположим, что команды ветви 1 выполняются намного чаще, чем команды ветви 2. В этом случае на работе программы будут сказываться за- задержки, вызванные сбросом и реинициализацией блока ветвлений процес- процессора из-за частой непредсказуемости переходов на другую ветвь кода. Если организовать цикл по-другому, например, как в этом фрагменте: test ЕАХ, ЕАХ jnz label_A <команды ветви 2> jmp label_B label_A: <команды ветви 1> label В:
44_ Часть I. Основы эффективного программирования на ассемблере то количество "попаданий" блока ветвлений будет намного больше, и произ- производительность вычислений на этом участке кода возрастет. Оптимизация циклических вычислений, конечно же, не исчерпывается приведенными примерами. Для решения подобных задач требуется помимо знания ассемб- ассемблера еще и достаточно хорошее понимание принципов работы процессора и его основных функциональных узлов. Команды безусловных переходов и вызовов функций также оказывают влияние на производительность программ, поэтому следует по возможности уменьшать их число в программе. Каким образом эти команды могут замед- замедлить производительность приложения? Команды безусловных переходов и вызовов являются комплексными по природе и требуют от процессора значительного числа микроопераций, свя- связанных с формированием исполнительных адресов, реорганизации очереди предвыборки команд, особенно, если в фрагменте кода присутствует цепоч- цепочка из подобных команд. Кроме того, и это очень существенно сказывается на производительности, команды безусловных переходов могут "выталкивать" другие команды пере- переходов из специального буфера памяти процессора, где хранятся адреса воз- возможных переходов. Этот буфер называется ВТВ (Branch Target Buffer) и иг- играет очень важную роль в организации ветвлений и переходов программ. Алгоритм замены неиспользуемых команд буфера организован по принципу случайных выборок, поэтому могут возникать весьма существенные задерж- задержки в случае множественных ветвлений программы. Уменьшить количество команд безусловных переходов и ветвлений можно путем реструктуризации программного кода. В каждом конкретном случае существует свой вариант решения, но все же можно порекомендовать и не- некоторые общие приемы: ? если после одной из команд jump следует другая, то можно заменить эту последовательность одной командой перехода на последнюю метку; ? переход на команду возврата ret лучше заменять самой командой возврата. Например, фрагмент кода какой-либо функции (назовем ее myproc), содер- содержащий последовательность команд myproc proc < ассемблерный код> jmp ex ex: ret myproc endp
Глава 1. Оптимизация ассемблерного кода для процессоров Pentium 45_ следует заменить более эффективной последовательностью (изменения вы- выделены жирным шрифтом): myproc proc < ассемблерный код> ret ret myproc endp Команда jmp ex перехода на метку, где происходит выход из функции myproc, заменена командой ret. Необходимость в других командах ret оп- определяется алгоритмом работы функции. Если необходимо вызывать из одной функции (процедуры) другую, то очень желательно избегать так называемого двойного возврата (double return). Если бы это удалось сделать, то исчезла бы необходимость манипуляций с указа- указателем стека. Подобные манипуляции нарушают работу механизма прогнози- прогнозирования процессора. Было бы неплохо заменить, например, команду вызова функции call myproc ret командой безусловного перехода jmp myproc. При этом возврат из основной функции выполнялся бы командой ret процедуры myproc. Возможно, по- подобные манипуляции сложны для понимания, поэтому я поясню это на примере. Сейчас мы разработаем простое консольное приложение C++ .NET, в кото- котором будет выполняться вызов ассемблерной функции (назовем ее fcaii). Функция f call вычисляет разность двух целых чисел (переменные ii и i2 в основной программе), затем вызывает функцию mui3 для умножения полу- полученной разности на число 3. Этот результат и возвращается в основную программу для отображения на экране. Исходный текст неоптимизированного ассемблерного модуля для компиля- компиляции в среде макроассемблера MASM 6.14 показан в листинге 1.16. .686 .model flat
46^ Часть /. Основы эффективного программирования на ассемблере public _fcall .code _fcall proc push EBP mov EBP, ESP ; сохранение значения переменной Ив регистре ЕАХ mov ЕАХ, DWORD PTR [ЕВР+8] sub ЕАХ, DWORD PTR [EBP+12] call _mul3 pop EBP ret _fcall endp _mul3 proc mov EBX, 3 imul EBX ret _mul3 endp end Переменные и и i2 передаются через стек. Далее находится их разность, которая помещается в регистр еах. Эти действия выполняются с помощью команд mov ЕАХ, DWORD PTR [EBP+8] sub ЕАХ, DWORD PTR [EBP+12] Далее вызывается функция mui3, выполняющая умножение результата в еах на 3. Обратите внимание, что вызов функции mui3 выполняется с помощью команды call. Возврат в основную программу на C++ осуществляется через две команды ret. Для оптимизации ассемблерного кода изменим его так, как показано в листинге 1.17 (изменения и дополнения выделены жирным шрифтом). .686 .model flat public fcall
Глава 1. Оптимизация ассемблерного кода для процессоров Pentium 47 . code _fcall proc push EBP mov EBP, ESP mov EAX, DWORD PTR [EBP+8] ;il sub EAX, DWORD PTR [EBP+12] ;i2 pop EBP jmp mul3 _fcall endp _mul3 proc mov EBX, 3 imul EBX ret _mul3 endp end Как видно из листинга, команда call _mui3 исчезла, а команда pop ebp на- находится перед командой перехода на метку процедуры. Перед выполнением команды jmp _mui3 в стеке находится адрес возврата в основную программу. Возврат после умножения содержимого регистра еах на 3 выполняется командой ret функции mui3. Исходный текст консольного приложения C++ .NET 2003, использующий результат вычислений предыдущего листинга, представлен в листинге 1.18. // REPLACE_CALL_WITH_JMP_DEMO.cpp : Defines the entry point for the con- // sole application. #include "stdafx.h" extern "C" int fcall(int il, int i2); int _tmain(int argc, JTCHAR* argv[]) { int il, i2; printf("AVOIDING DOUBLE RETURN IN ASM FUNCTIONS\n"); while (true)
48 Часть I. Основы эффективного программирования на ассемблере printf("XnEnter il: ") ; scanf("%d", &il); printf("Enter i2: "); scanf("%d", &i2); printf("(il - i2)*3 = %d\n", fcall(il, i2) } return 0; Окно работающего приложения показано на рис. 1.2. Рис. 1.2. Окно приложения, демонстрирующего вызовы ассемблерных функций с одной командой возврата Еще один способ устранения избыточных команд безусловных переходов заключается в том, чтобы продублировать фрагмент профаммного кода, на который передается управление командой jmp. Рассмотрим практический пример. Пусть имеется массив целых чисел, из которого необходимо выбрать все положительные числа и сохранить в одном массиве, а также выбрать все отрицательные числа и сохранить их в другом массиве. Предположим, что исходный массив содержит 8 элементов. Для упрощения полагаем, что по- половина элементов положительные, а другая половина — отрицательные чис- числа. Два вспомогательных массива имеют размерность, равную 4. Исходный текст ассемблерной функции, выполняющей необходимые манипуляции, показан в листинге 1.19.
Глава 1. Оптимизация ассемблерного кода для процессоров Pentium 49 push EBX ; помещаем адрес исходного массива в регистр ESI, ; адрес массива отрицательных чисел — в регистр EDI и ; адрес массива положительных чисел — в регистр ЕВХ lea ESI, il lea EDI, ineg lea EBX, ipos ; помещаем размер исходного массива Ив регистр EDX mov EDX, 8 next_int: ; проверка равенства элемента массива il нулю cmp DWORD PTR [ESI], О ; если элемент больше или равен 0, он записывается в массив ipos jge storejpos ; иначе он сохраняется в массиве отрицательных чисел ineg: mov EAX, DWORD PTR [ESI] mov DWORD PTR [EDI], EAX add EDI, 4 ; здесь выполняется переход на общую ветвь программы jmp next store_pos: mov EAX, DWORD PTR [ESI]
j>0 Часть /. Основы эффективного программирования на ассемблере mov DWORD PTR [EBX], EAX add EBX, 4 next: dec EDX jz ex add ESI, 4 jmp next_int ex: pop EBX В этом фрагменте программного кода оптимизация достигается путем ис- исключения команды безусловного перехода jmp next (выделенной жирным шрифтом). Вместо нее можно поместить дублирующий фрагмент кода. Ис- Исходный текст модифицированного варианта программы представлен в лис- листинге 1.20. г) и ;.:¦ t и н! 1.2 О Б;,; 6 о р п о л о ж и т с л ь н»•> i х й о т р у-, ц ¦ г г t; л;.,« ь <х чисел и ? м а с ¦'; и о о push EBX lea ESI, il lea EDI, ineg lea EBX, ipos mov EDX, 8 next_int: cmp DWORD PTR [ESI], 0 jge store_pos mov EAX, DWORD PTR [ESI] mov DWORD PTR [EDI], EAX add EDI, 4 dec EDX jz ex add ESI,4 jmp next_int
Глава 1. Оптимизация ассемблерного кода для процессоров Pentium 51 store_j?os: mov ЕАХ, DWORD PTR [ESI] mov DWORD PTR [EBX], EAX add EBX, 4 dec EDX jz ex add ESI, 4 jmp next_int ex: pop EBX Изменения, сделанные в исходной программе, выделены жирным шрифтом. Как видно из исходного текста, исключение команды jmp next потребовало вставки нескольких команд из общей ветви, обозначенной в листинге 1.19 как next. Еще одно важное замечание, касающееся циклических вычислений. Суще- Существующие КОМаНДЫ loop И ИХ МОДИфикаЦИИ (loope, loopne, loopz, loopnz) В целом замедляют работу программы и являются анахронизмом для совре- современных моделей процессоров, поэтому при написании быстрых приложе- приложений лучше заменять их комбинацией команд стр и jmp. Например, цикл с использованием в качестве счетчика регистра есх и коман- команды loop (один из наиболее часто используемых циклов), который имеет вид mov ЕСХ, counter label_l: <команды ассемблера> loop label_l является наименее оптимальным с точки зрения производительности. Недостатком этого цикла, помимо плохой оптимизируемое™ команды loop самим процессором, является и то, что в качестве счетчика цикла можно применить только регистр есх. Второй большой минус такого цикла заклю- заключается в том, что регистр есх уменьшается на 1, что делает довольно труд- трудным разворачивание цикла для работы с другим шагом.
52_ Часть I. Основы эффективного программирования на ассемблере Цикл loop можно заменить комбинацией команд с неплохими показателями производительности mov ЕСХ, counter label_l: <команды ассемблера> dec ЕСХ jnz label_l В этом фрагменте кода можно декрементировать счетчик есх на величину, отличную от 1, что дает определенные преимущества для оптимизации кода. Немного худший вариант, но тоже работающий быстрее цикла loop, может иметь вид: xor EDX, EDX label_l: <команды ассемблера> inc EDX cmp EDX, counter jb label_l Здесь необходимо анализировать выход из цикла с помощью команды cmp, что требует дополнительного времени работы процессора. Этот цикл также может быть относительно легко развернут. Многие программы значительную часть времени расходуют на выполнение операций, связанных с копированием и перемещением больших объемов данных. Процессоры Intel имеют для этих целей целый ряд команд, позво- позволяющих тем или иным способом ускорить выполнение операций над мно- многобайтовыми массивами и строками. Разработан целый ряд инструкций процессора, названных командами строковых примитивов, специально предназначенных для этих целей. К группе команд строковых примитивов относятся команды movs, lods, cmps, seas. Если применять эти" команды без префикса повторения rep,
Глава 1. Оптимизация ассемблерного кода для процессоров Pentium 53_ производительность работы приложения снизится. Они плохо оптимизи- оптимизируемы процессором, поскольку комплексный характер каждой из этих команд требует изрядного количества машинных циклов. Хорошей альтер- альтернативой таким инструкциям является программный код, написанный с по- помощью обычных команд. Применение команд строковых примитивов с префиксом повторения rep используется при копировании и перемещении данных, и при этом обеспе- обеспечивается хорошая производительность. Лучше всего работают команды, мнемоника которых указывает на размер операндов (lodsb, lodsw, lodsd, movsb, movsw и т. д.). Например, копирование 100 двойных слов, находящих- находящихся в буфере памяти src, в буфер памяти dst можно выполнить с помощью последовательности команд mov ECX, 100 lea ESI, src lea EDI, dst eld rep movsd Неплохой альтернативой этому фрагменту кода является следующая после- последовательность команд: .' . . mov ECX, 100 lea ESI, src lea EDI, dst next: mov EAX, [ESI] mov [EDI], EAX add ESI., 4 add EDI, 4 dec ECX jnz next Если пойти еще дальше и развернуть цикл так, чтобы выполнять копирова- копирование сразу двух двойных слов в одной итерации, то полученный программ- программный код будет превосходить аналог с командой rep movsd: mov' ECX, 100 lea ESI, src
54 Часть /. Основы эффективного программирования на ассемблере lea EDI, dst next: mov EAX, [ESI] mov [EDI], EAX mov EAX, [ESI+4] mov [EDI+4], EAX 1 add ESI, 8 add EDI, 8 sub ECX, 2 jnz next Как уже говорилось ранее, ассемблер является эффективным средством оптимизации математических операций, операций с массивами данных, об- обмена данными с аппаратными средствами компьютера. Интенсивные мате- математические вычисления присутствуют в большинстве приложений, и повы- повышение эффективности таких вычислений часто связано с применением ассемблера. Решающее значение для написания высокопроизводительных приложений имеет знание аппаратно-программной архитектуры математи- математического сопроцессора FPU и технологии SIMD. В этой главе мы остановимся на принципах работы математического сопро- сопроцессора и возможности оптимизации приложений. Практические примеры работы с математическим сопроцессором будут рассмотрены в главе 2. Там же мы кратко коснемся истории развития сопроцессоров. Программная модель FPU может быть представлена в виде совокупности нескольких групп регистров. Это регистры стека сопроцессора (st (о), stA), ... stG)), служебные регистры (регистр состояния, управляющий регистр и регистр состояния тегов), регистр-указатель данных и регистр- указатель команд. К любому из вышеперечисленных регистров программа может получить доступ либо напрямую, либо косвенно. Для программирования сопроцессо- сопроцессора в основном используются регистры st@) , ..., stG) и биты со, ci, С2 и сз регистра состояния. Регистры сопроцессора функционируют как обычный стек основного про- процессора. Но у этого стека имеется ограниченное число позиций — только 8. Сопроцессор имеет еще один регистр, труднодоступный для программи- программиста. Он представляет собой слово, содержащее "метки" каждой позиции стека. Такой регистр позволяет сопроцессору отслеживать, какие из пози- позиций стека используются, а какие свободны. Любая попытка поместить объект в стек на уже занятую позицию приведет к возникновению исклю- исключительной ситуации.
Глава 1. Оптимизация ассемблерного кода для процессоров Pentium 55 Программа заносит данные в стек сопроцессора с помощью команды за- загрузки, которая помещает данные в вершину стека. Если число в памяти записано не во временном действительном формате, то сопроцессор преоб- преобразует его в 80-битовое представление во время выполнения команды за- загрузки. Команды записи извлекают значение из стека сопроцессора и помещают их в память. Если необходимо преобразование формата данных, сопроцессор выполняет его как часть операции записи. Некоторые формы операции за- записи оставляют вершину стека нетронутой для дальнейших действий. После того как данные помещены в стек сопроцессора, они могут быть ис- использованы любой командой. Инструкции процессора допускают как дейст- действия между регистрами, так и действия между памятью и регистрами. По аналогии с основным процессором, из любых двух операндов арифметиче- арифметической операции как минимум один должен находиться в регистре. У сопро- сопроцессора один из операндов должен быть всегда верхним элементом стека, а другой операнд может быть взят из памяти, либо из стека регистров. Стек регистров всегда является приемником результата любой арифметиче- арифметической операции. Непосредственно записать результат в память той же коман- командой, которая выполнила вычисления, процессор числовой обработки не мо- может. Для пересылки операнда обратно в память необходимо воспользоваться отдельной командой записи или командой извлечения из стека с последую- последующей записью в память. Все команды математического сопроцессора начинаются с буквы f, чтобы отличить их от команд основного процессора. Условно команды сопроцес- сопроцессора можно разделить на несколько групп: команды записи и чтения, команды сложения и вычитания, команды умножения и деления, команды сравнения, команды трансцендентных функций и дополнительные команды. Математический сопроцессор предоставляет программисту поддержку на аппаратном уровне алгоритмов для вычисления тригонометрических функ- функций, логарифмов и степеней. Подобные вычисления являются абсолютно прозрачными для разработчика программного обеспечения и не требуют от него написания каких-либо собственных алгоритмов. Математический сопроцессор позволяет выполнять вычисления с очень вы- высокой точностью (до 18-ти разрядов). Если подобные вычисления проводить без привлечения сопроцессора, то точность результата будет ниже. Использование языка ассемблера для программирования сопроцессора дает существенный выигрыш при разработке высокопроизводительных приложе- приложений. Это обусловлено тем, что система команд математического сопроцес- сопроцессора предоставляет программисту практически все средства для реализации большинства вычислительных алгоритмов. Если даже какие-либо команды отсутствуют, то легко можно подобрать эквивалент таких команд, состоя-
56^ Часть I. Основы эффективного программирования на ассемблере щий из нескольких ассемблерных инструкций. Следует сказать, что с помо- помощью ассемблерных команд сопроцессора можно реализовать такие опера- операции, которые с трудом могут быть написаны на C++ или не могут быть на- написаны вообще. Что же касается математических функций стандартных библиотек C++, то их аналоги на языке ассемблера зачастую имеют более высокие показатели производительности и меньший размер программного кода. Язык ассембле- ассемблера позволяет программисту написать собственные функции, часто более эффективные, чем аналогичные им функции математической библиотеки Visual C++ .NET. Работа процессоров семейства Intel вплоть до пятого поколения определяет- определяется следующей схемой: каждая инструкция выполняет действия над одним или двумя операндами. Операнды находятся как в регистрах процессора, так и в памяти. Выполнение повторяющихся или однотипных действий над не- несколькими операндами требует применения либо циклов, либо рекурсивных вызовов тех или иных участков программного кода. В приложениях мультимедиа, 2D/3D-графики, коммуникационных и ряде других современных задач необходимость выполнения однотипных действий возникает достаточно часто и в больших объемах. Оптимизировать решение этих задач была призвана технология SIMD. Для ее реализации использова- использовали регистры FPU, построив на их базе вычислительный блок ММХ. Тради- Традиционный FPU содержит восемь 80-разрядных регистров для хранения и об- обработки чисел в формате с плавающей точкой. Эти регистры образуют стек FPU и в инструкциях адресуются через специ- специальный указатель стека. Блок ММХ физически использует по 64 младших бита этих регистров, причем эти'регистры адресуются прямо (mmo.. .mm7). В расширении ММХ используются новые типы упакованных данных, раз- размещаемых в 64-битных регистрах: ? упакованные байты (Packed byte) — 8 байт; ? упакованные слова (Packed word) — 4 слова; П упакованные двойные слова (Packed doubleword) — 2 двойных слова; ? учетверенное слово (Quadword) — 1 слово. Технология ММХ является существенным улучшением архитектуры микро- микропроцессоров Intel. Она разработана для ускорения выполнения мультиме- мультимедийных и коммуникационных программ. Объемы данных и сложность их обработки современными персональными компьютерами возрастают экспо- экспоненциально, что требует от микропроцессоров существенного увеличения производительности. Каждая инструкция ММХ выполняет действие сразу над всем комплектом операндов (8, 4, 2 или 1), размещенных в адресуемых регистрах. Еще одна
Глава 1. Оптимизация ассемблерного кода для процессоров Pentium 57_ особенность ММХ — поддержка арифметики с насыщением (saturating arithmetic). Ее отличие от обычной арифметики с циклическим переполне- переполнением заключается в том, что при возникновении переполнения в результате фиксируется максимальное возможное значение для данного типа данных, а перенос игнорируется. В случае переполнения снизу в результате фиксиру- фиксируется минимальное возможное значение. Граничные значения определяются типом (знаковый или беззнаковый) и разрядностью переменных. Такой ре- режим вычислений удобен, например, для определения цветов. Новые инст- инструкции (всего их 57) можно разделить на следующие группы команд: П арифметические, среди которых сложение и вычитание в разных режи- режимах, умножение и комбинация умножения и сложения; ? сравнения элементов данных на равенство или по величине; О преобразования форматов; ? логические ("И", "И-НЕ", "ИЛИ" и "исключающее ИЛИ"), выполняемые над 64-битными операндами; ? сдвигов — логических и арифметических; ? пересылки данных между регистрами ММХ и целочисленными регист- регистрами или памятью; ? очистки ММХ — установка признаков пустых регистров в слове тегов. Различие в способе адресации регистров, несовпадение форматов данных ММХ и FPU и некоторые другие нюансы не позволяют чередовать инст- инструкции FPU и ММХ. Блок FPU/MMX может работать либо в одном, либо в другом режиме. Если, к примеру, в цепочку инструкций FPU нужно вкли- вклинить инструкции ММХ, после чего продолжить вычисления FPU, то перед первой инструкцией ММХ приходится сохранять контекст (состояние реги- регистров) FPU в памяти, а после этих инструкций снова загружать контекст. На эти сохранения и загрузки расходуется процессорное время. В резуль- результате выигрыш от идеи SIMD можно полностью потерять. Совпадение ре- регистров ММХ и FPU оправдывают тем, что для сохранения контекста ММХ при переключении задач не требовалось доработок в операционной системе — контекст ММХ сохраняется тем же способом, что и FPU. Та- Таким образом, операционным системам было все равно, какой процессор установлен — с ММХ или.без. Но для того, чтобы реализовать преимуще- преимущества SIMD, приложения должны "уметь" ими пользоваться (и не проиграть на переключениях). В Visual C++ .NET поддержка MMX-расширения осуществляется через ме- механизм собственных функций (intrinsics). Все объявления собственных функций содержатся в файле заголовка mmintrin.h. Программист может ис- использовать собственные функции в своих программах.
58 Часть I. Основы эффективного программирования на ассемблере Собственные функции MMX-расширения делятся на несколько групп: ? общего назначения. Сюда входят функции упаковки/распаковки, очистки ММХ-регистров, пересылки; ? сравнения; ? арифметических операций; ? сдвига; ? логические. Любая собственная функция может быть представлена эквивалентным ас- ассемблерным кодом. Для манипуляций с 64-битными переменными в C++ .NET определена переменная типа тб4. Все собственные функции исполь- используют в той или иной степени такие переменные, хотя нигде не фигурируют регистры пипО.. .mm7. Дизассемблированный код любой собственной функ- функции является избыточным и может быть заменен командами ассемблера. Встроенный ассемблер C++ .NET понимает ММХ-инструкции и генерирует соответствующий код. Практические примеры использования ММХ- технологии мы будем подробно рассматривать в главе 10. Начиная с процессора Pentium III, появилось так называемое потоковое расширение SSE. Использование этой технологии призвано увеличить про- производительность средств мультимедиа и коммуникаций. Это расширение (которое включает новые регистры, типы данных и инструкции) призвано ускорить выполнение приложений для мобильного видео, комбинированной графики с видео, обработки изображений, звукового синтеза, синтеза речи и сжатия, телефонии, видеосвязи, 2D- и SD-графики. Приложения этих типов обычно используют алгоритмы с интенсивными вычислениями, выполняя повторяющиеся действия на больших множествах простых элементов данных. Особенностями таких приложений являются: ? большой объем данных; ? большая часть операций производится над целыми числами малой длины; ? для графических приложений — операции с 8-битными значениями цве- цвета пикселов; ? для аудиоприложений — операции с 16-битными аудиовыборками звуко- звуковых сигналов; ? присутствие параллелизма в вычислениях. Новые типы процессоров имеют реализованный на аппаратном уровне дополнительный блок из восьми 128-разрядных регистров, названных ХММ. Каждый из регистров ХММ может оперировать сразу с четырьмя 32-разрядными числами в формате с плавающей точкой. Блок позволяет одной инструкцией выполнять операции сразу над четырьмя 32-разряд-
Глава 1. Оптимизация ассемблерного кода для процессоров Pentium 59 ными операндами. Такое выполнение инструкций процессора называется параллельным. Инструкции с регистрами ХММ могут работать и в так называемом- скаляр- скалярном режиме вычислений. В этом случае операции выполняются над младшим 32-битным словом. Поскольку SSE-расширение реализовано апгтаратно, то при выполнении новых инструкций блок FPU/MMX не используется. Такое раздельное выполнение команд FPU/MMX и SSE позволяет эффек- эффективно использовать комбинации целочисленных инструкций ММХ с инст- инструкциями SSE над операндами с плавающей точкой. В этом случае регистры математического сопроцессора используются для целочисленных вычисле- вычислений ММХ, а вычисления с плавающей точкой выполняет блок SSE. . Для работы с SSE-расширением в набор инструкций целочисленного MMX- расширения введены 12 новых команд: О вычисление среднего, минимума, максимума, суммарной разности; ? беззнаковое умножение и несколько инструкций, связанных с переста- перестановками элементов. SSE-расширение включает в себя инструкции следующих типов: ? арифметические (сложение, вычитание, умножение, деление, извлечение квадратного корня, нахождение минимума и максимума); ? сравнения; ? преобразования (связывают между собой целочисленные форматы ММХ и форматы с плавающей точкой ХММ); О логические (включают операции "И", "ИЛИ",/'И-НЕ" и "Исключающее ИЛИ" над операндами в ХММ); ? перемещения данных и перераспределения (служат для обмена данными между блоком ХММ и памятью или целочисленными регистрами про- процессора, а также выполняют перестановку элементов упакованных опе- операндов); ? управления состоянием (служат для сохранения и загрузки дополнитель- дополнительного регистра состояния ХММ. В эту группу входят и инструкции быст- быстрого сохранения/восстановления состояния MMX/FPU и SSE). Дополнительно в SSE введены новые инструкции управления содержимым кэша: инструкции записи содержимого регистров ММХ и ХММ в память в обход кэша. Назначение этих инструкций — избежать излишнего загрязне- загрязнения кэш-памяти. Кроме того, появилась возможность подгружать требуемые данные в кэш до вызова инструкций, использующих эти данные. Приложению, рассчитывающему получить в распоряжение не только базо- базовые ресурсы 32-разрядного процессора, приходится определять тип процес-
60 Часть I. Основы эффективного программирования на ассемблере сора. К счастью, это делается просто, с помощью инструкции cpuid. В про- процессорах, поддерживающих SSE, по инструкции cpuid теперь можно полу- получить и уникальный 64-битный идентификатор процессора. SSE-расширение процессоров Pentium, как и ММХ, поддерживается с по- помощью собственных функций C++ .NET. Все определения собственных функций SSE-расширения находятся в файле xmmintrin.h. Для удобства ра- работы со 128-битными переменными используется тип ml28. Практические примеры использования SSE-расширения для профаммирования приложе- приложений приведены в главе 10. В этой главе мы рассмотрели различные варианты оптимизации ассемблер- ассемблерного кода профамм применительно к особенностям функционирования процессоров Intel Pentium. Приведенные примеры профаммного кода по- позволяют наилучшим образом использовать аппаратные возможности этих процессоров.
Глава 2 Оптимизация вычислительных алгоритмов с помощью ассемблера В этой главе будут рассмотрены аспекты программирования на ассемблере, которые делают его действительно полезным и эффективным языком для оптимизации приложений. Предполагается, что читатель знаком с системой команд языка. Материал главы раскрывает основы построения эффективных алгоритмов обработки данных на языке ассемблера. Ассемблер чаще всего применяется для программирования математических алгоритмов, задач быстрой сортировки и поиска данных в массивах, для оп- оптимизации циклически повторяющихся вычислений. При разработке про- программ на C++ очень часто решаются подобные задачи, и они занимают зна- значительную часть времени. В процессе анализа и построения алгоритмов используются команды ас- ассемблера вплоть до процессоров Pentium 4. Последние модели процессоров включают команды, позволяющие выполнять быструю обработку массивов данных, а также комплексные команды, позволяющие оптимизировать сам алгоритм вычислений. Прежде чем окунуться в программирование на ассемблере, необходимо оп- определиться с инструментами разработки программ. Для демонстрации при- примеров вычислительных алгоритмов я буду использовать консольные прило- приложения C++ .NET, а сами алгоритмы будут написаны на встроенном ассемблере C++ .NET 2003. Фрагменты программного кода на встроенном ассемблере построены таким образом, чтобы можно было анализировать вычислительные алгоритмы без изучения глубинных аспектов встроенного в C++ ассемблера (это мы сделаем в последующих главах). Начнем с математических вычислений. Практически все приложения ис- используют те или иные операции, связанные с математическими вычисле- вычислениями, начиная от простейших (сложение и вычитание) и заканчивая реше- решением систем уравнений. Математические действия могут использовать как обычные команды процессора, например, add, sub, mul, div, так и специ- специальные команды математического сопроцессора. 3 Зак. 243
62 Часть I. Основы эффективного программирования на ассемблере Арифметические команды любого микропроцессора привлекают к себе наи- наибольшее внимание. Практически в каждой программе выполняются опреде- определенные арифметические вычисления с помощью таких команд. Хотя их не- немного, они выполняют большинство преобразований данных в процессоре. В реальных же условиях арифметические команды занимают лишь малую часть всех исполняемых команд, но имеют определенную специфику вы- выполнения. Среда Visual C++ .NET 2003 отличается многообразием и мощью своих ма- математических функций. Однако в основе математических библиотек C++ лежит относительно простой набор команд процессора и сопроцессора. В языке ассемблера нет таких комплексных функций и библиотек, однако можно разработать свои, ничем не уступающие C++, а зачастую и превос- превосходящие их по своим возможностям^ функции. Рассмотрим некоторые аспекты построения эффективных вычислительных алгоритмов с использованием языка ассемблера. Большие возможности для оптимизации математических вычислений кроются в правильном использо- использовании операций с плавающей точкой. Часто программисты разрабатывают собственные функции на ассемблере, превосходящие по вычислительным возможностям и быстродействию аналогичные им библиотечные функции C++. Необходимость в написании таких функций возникает, как правило, при решении задач обработки данных в реальном масштабе времени, при написании драйверов устройств и системных служб. Для успешного решения таких задач необходимо достаточно хорошо знать систему команд и особенности работы математического сопроцессора или блока операций с плавающей точкой процессоров Intel. Самые ранние модели процессоров фирмы Intel не имели аппаратной под- поддержки для операций с плавающей точкой. Все операции такого типа вы- выполнялись как процедуры. В состав такой процедуры входили обычные арифметические команды. Для ранних моделей был разработан дополни- дополнительный кристалл, получивший название математического сопроцессора. В состав математического сопроцессора входили команды, с помощью кото- которых операции с плавающей точкой выполнялись намного быстрее, чем при использовании процедуры из обычных арифметических команд. Начиная с процессоров Pentium (и некоторых моделей Intel 486), математиче- математический сопроцессор как отдельное устройство перестал существовать. Вместо него в состав процессоров входит блок операций с плавающей точкой (FPU — Floating-Point Unit), однако он программируется как отдельный модуль. Сопроцессор добавляет арифметические возможности в систему, но не за- замещает ни одну команду основного процессора. Команды add, sub, mul и div, например, выполняются процессором, а математический сопроцессор выполняет дополнительные, более эффективные команды арифметической обработки.
Глава 2. Оптимизация вычислительных алгоритмов с помощью ассемблера 63_ С точки зрения программиста, система с сопроцессором выглядит как еди- единый процессор с большим набором команд. Как уже отмечалось в главе 1, программная модель сопроцессора может быть представлена как совокупность регистров. Они могут быть разделены на три группы: 1. Регистры стека сопроцессора. Их 8, и они именуются как st@), st(D, stB), ..., stG). Числа с плавающей точкой запоминаются как 80- битные числа расширенного формата. Стек регистров организован по принципу LIFO (Last-In, First-Out — "последним пришел — первым ушел"). Регистр st@) всегда указывает на вершину стека. Вновь посту- поступающие в сопроцессор числа добавляются в вершину стека. Находящиеся в стеке числа опускаются вниз стека, освобождая тем самым место для других числовых величин. 2. Служебные регистры. В их число входят: регистр состояния, отражающий информацию о состоянии процессора, управляющий регистр (для управ- управления режимами работы сопроцессора) и регистр состояния тегов, отра- отражающий состояние регистров st (о) ... st G). 3. Регистр-указатель данных и регистр-указатель команд. Эти регистры предназначены для обработки исключительных ситуаций. Программа может получить доступ к любому из вышеперечисленных реги- регистров либо напрямую, либо косвенно. Для программирования сопроцессора в основном используются регистры st @), ..., st G) и биты со, ci, С2 и сз регистра состояния. Регистры сопроцессора функционируют как обычный стек, такой же, как и стек основного процессора. Но у этого стека имеется ограниченное число позиций — только 8. Сопроцессор имеет еще один регистр, труднодоступ- труднодоступный для программиста. Он представляет собой слово, содержащее "метки" каждой позиции стека. Такой регистр позволяет сопроцессору отслеживать, какие из позиций стека используются, а какие свободны. Любая попытка помещения объекта в стек на уже занятую позицию приводит к возникно- возникновению особой ситуации — недействительной операции. Программа заносит данные в стек сопроцессора с помощью команды за- загрузки, и все команды загрузки помешают данные в вершину стека. Если число в главной памяти записано не во временном действительном формате, сопроцессор преобразует его в 80-битное представление во время выполне- выполнения команды загрузки. Аналогично, команды записи извлекают значение из стека сопроцессора и помещают их в главную память, и если необходимо преобразование формата данных, сопроцессор выполняет его как часть опе- операции записи. Некоторые формы операции записи оставляют вершину стека нетронутой для дальнейших действий.
64 Часть I. Основы эффективного программирования на ассемблере После того как данные помещены в стек сопроцессора, они могут быть ис- использованы любой командой. Инструкции процессора допускают как дейст- действия между регистрами, так и действия между памятью и регистрами. По аналогии с основным процессором, из любых двух операндов арифметиче- арифметической операции хотя бы один должен находиться в регистре. У сопроцессора один из операндов должен быть всегда верхним элементом стека, а другой операнд может быть взят из памяти либо из стека регистров. Стек регистров всегда должен быть приемником результата любой арифме- арифметической операции. Непосредственно записать результат в память той же командой, которая выполнила вычисления, процессор числовой обработки не может. Для пересылки операнда обратно в память необходимо восполь- воспользоваться отдельйой командой записи (или командой извлечения из стека с записью в память). Все команды математического сопроцессора начинаются с буквы f, чтобы отличить их от команд основного процессора. Условно команды сопроцес- сопроцессора можно разделить на несколько групп: ? команды записи и чтения; П команды сложения/вычитания; ? команды умножения/деления; О команды сравнения; ? команды трансцендентных функций; ? дополнительные команды. Рассмотрим более детально эти группы команд. Команда записи имеет два варианта. Одна из модификаций этой команды извлекает число с вершины стека и записывает его в ячейку памяти. Выпол- Выполняя эту команду, сопроцессор преобразует данные из временного действи- действительного формата в желаемую внешнюю форму. Для этой команды опреде- определены коды операций fst и fist. Эти команды позволяют занести значение вершины стека в любой регистр внутри стека. Второй вариант команды записи, кроме записи данных, изменяет положе- положение указателя стека. Команды fstp (как и команды fistp и fbstp), выпол- выполняя ту же операцию записи данных из сопроцессора в память, извлекают число из стека. Эта модификация команд поддерживает все внешние типы данных. Команда замены fxch — следующая команда в группе команд пересылки данных. Она меняет местами содержимое вершины стека с содержимым другого регистра стека. В качестве операнда этой команды может использо- использоваться только другой элемент стека. Обменять содержимое вершины стека и ячейки памяти эта команда не позволяет. Для этого потребуется несколько
Глава 2. Оптимизация вычислительных алгоритмов с помощью ассемблера 65_ команд. Сопроцессор может в одной команде выполнить чтение из памяти или запись в память, но не то и другое одновременно. Команды чтения или загрузки выполняют загрузку данных в вершину стека сопроцессора. Основным вариантом такой команды является fid. Для за- загрузки целых чисел используется модификация команды — f iid. Следующей группой команд, которую мы рассмотрим, является группа команд сложения и вычитания. Каждая из этих команд находит сумму или разность регистра st@) и другого операнда. Результат операции всегда по- помещается в регистр сопроцессора. Йалее представлена мнемоника этих команд: fadd ST(O),STA) fadd ST@),STB) fadd STB),ST@) fiadd WORD_INTEGER fiadd SHORT_INTEGER fadd SHORT_REAL fadd LONG_REAL faddp STB),ST@) fsub ST@),STB) fisub WORD_INTEGER fsubp STB),ST@) fsubr STB),ST@) fisubr SHORT_INTEGER fsubrp STB) , ST@) Операндами этих команд могут быть либо регистры стека сопроцессора, ли- либо регистр стека и ячейка памяти. В качестве исходных данных памяти ис- используются слово и короткое слово разрядностью 16 и 32 бита. Фрагмент программного кода (листинг 2.1) демонстрирует нахождение сум- суммы двух вещественных чисел с применением команд сложения математиче- математического сопроцессора. // FIADD_EXM.cpp : Defines the entry point for the console application. #include "stdafx.h" int _tmain(int argc, _TCHAR* argv[])
& Часть I. Основы эффективного программирования на ассемблере float fl,f2,fsum; while (true) { printf("\nEnter float 1: "); scanf("%f", &fl); printf("Enter float 2: "); scanf("%f", &f2); _asm { finit fid DWORD PTR fl fadd DWORD PTR f2 fstp DWORD PTR fsum fwait }; printf("fl + f2 = %6.3f\n", fsum); } return 0; В блоке _asm {...} первая команда fid загружает вещественное число fi из памяти в вершину стека сопроцессора. Команда fadd вычисляет сумму значений вершины стека st@) и ячейки памяти, содержащей значение f2. Результат операции сохраняется в вершине стека сопроцессора. Наконец, команда fstp сохраняет значение суммы в переменной fsum, при этом вер- вершина стека st@) очищается. Окно работающего приложения показано на рис. 2.1. Рис. 2.1. Окно приложения, показывающего сложение двух вещественных чисел с помощью команд сопроцессора В следующем примере мы рассмотрим применение'команд загрузки, сложе- сложения и сохранения для нахождения суммы элементов целочисленного масси-
Глава 2. Оптимизация вычислительных алгоритмов с помощью ассемблера 67_ ва из семи элементов. Подобные задачи очень часто приходится решать на практике. Я приведу исходный текст программы как на C++ .NET, так и с использованием ассемблера. Этот пример, как и предыдущий, показывает технику применения ассемблера для выполнения математических операций над числами. В листинге 2.2 приводится исходный текст консольного при- приложения на C++ .NET без ассемблерных команд. Листинг 2.2. Нахождение суммы злумснтоу целочисленного м о исполк.то манием только опера топ of; О* // FSUM.cpp : Defines the entry point for the console application. #include "stdafx.h" int _tmain(int argc, _TCHAR* argv[]) { int iarray[6] = {1, -7, 0, 5, 3, 9}; int *piarray = iarray; int isum = 0; int sf = sizeof(iarray)/4; for (int cnt = 0;cnt < sf;cnt++) { isum += *piarray; piarray++; } printf("Summa of integers = %d\n", isum); getchar(); return 0; Вычисление суммы элементов выполняется в цикле for по классической схеме с использованием указателя piarray на массив iarray: for (int cnt = 0;cnt < sf;cnt++) { isum += *piarray; piarray++; } Модифицируем исходный текст программы так, чтобы в ней использова- использовались команды ассемблера. Исходный текст такой программы представлен в листинге 2.3.
68 Часть I. Основы эффективного программирования на ассемблере II FSUM.cpp : Defines the entry point for the console application. #include "stdafx.h" int _tmain(int argc, JTCHAR* argv[]) int int int int iarray[6] ; *piarray = isum; = {-3, -7, 0, iarray; sf = sizeof(iarray)/4; asm { mov dec mov finit fild next: add fiadd loop fistp fwait ECX, sf ECX ESI, DWORD DWORD PTR ESI, 4 DWORD PTR next DWORD PTR 5, 3, 9}; PTR piarray [ESI] [ESI] isum printf("Summa of integers = %d\n", isum); getchar(); return 0;, Для нахождения суммы элементов массива воспользуемся следующим про- простым алгоритмом: первоначально загрузим в вершину стека первый элемент массива с помощью команды fild dword ptr [esi], после чего будем при- прибавлять к нему в каждой итерации следующий элемент. Адрес первого эле- элемента массива поместим в регистр esi, а количество итераций, на 1 мень- меньшее размера массива, поместим в регистр есх. После того как сумма найдена, сохраняем ее в переменной isum командой fistp.
Глава 2. Оптимизация вычислительных алгоритмов с помощью ассемблера 69 Ассемблерный код, приведенный в листинге 2.3, более эффективен в плане повышения производительности приложения, чем схема вычисления суммы в листинге 2.2. Окно работающего приложения представлено на рис. 2.2. Рис. 2.2. Окно приложения, выполняющего подсчет суммы элементов целочисленного массива Следующая группа команд, которую мы рассмотрим, — команды умноже- умножения и деления целых и вещественных чисел. Их можно представить в виде списка: WORD_INTEGER SHORT_ SHORT_ INTEGER REAL LONG_REAL fmul f imul fmulp fdiv f idiv fdivp fdivr fidivr fdivrp SHORT_REAL WORD_INTEGER STB) ,ST@) ST@) ,STB) SHORT_INTEGER ST(-2) ,ST@) ST@) ,STB) WORD_INTEGER STB)/ST@) LABEL LABEL LABEL LABEL WORD DWORD DWORD QWORD Как и в случае операций сложения и вычитания, в качестве операндов этих команд используются либо регистры сопроцессора, либо комбинация реги- регистра стека и ячейки памяти. Показать использование команд умножения и деления лучше всего на примере. Программный код этого примера более сложен и демонстрирует способы применения различных команд сопроцес- сопроцессора. Необходимо найти числовое значение величины z вещественного ти- типа, определяемой формулой (х - Y)/(x + Y). Исходный текст консольного приложения C++ с ассемблерным кодом показан в листинге 2.4.
70 Часть I. Основы эффективного программирования на ассемблере Листинг 2.4, Вычисление формулы с помощью ассем команд сопроцессора ! .. . . ...... .... . . .... // FORMULA.cpp : Defines the entry point for the console application. #include "stdafx.h"- int _tmain(int argc, _TCHAR* argv[]) { float X, Y, Z; while (true) { printf("\nEnter X: "); scanf("%f", &X); printf("Enter Y: "); scanf("%f", &Y) ; _asm { finit fid DWORD PTR X fadd DWORD PTR Y fid DWORD PTR X fsub DWORD PTR Y fxch st(l) fdiv st(l), st@) fxch st(l) fstp DWORD PTR Z fwait }; printf("(X - Y)/(X + Y) - %7.3f\n", Z); }; return 0; Проведем анализ примера. Вычисление выражения (х - Y)/(x + Y) вы- выполняется в три этапа. Первый шаг — вычисление знаменателя при помощи команд: fid DWORD PTR X fadd DWORD PTR Y
Глава 2. Оптимизация вычислительных алгоритмов с помощью ассемблера 71 Вершина стека (регистр st{0)) загружается значением переменной х. Далее к этому значению прибавляется значение переменной Y. В результате вы- выполнения этих двух команд в вершине стека будет находиться сумма х и Y. Следующие две команды выполняют вычисление разности х и Y. Для этого в вершину стека загружается значение х, затем производится вычитание значения y: fid DWORD PTR X fsub DWORD PTR Y К этому моменту в регистре st@) находится разность х и y. Так как стек сопроцессора организован в виде циклического буфера, то ранее вычислен- вычисленное значение х + у переместилось в регистр стека stA). Чтобы разделить разность переменных х и у на их сумму, обменяем значения в регистрах st@) и stA), после чего выполним операцию деления содержимого реги- регистра st A) на значение в регистре st (о): fxch st(l) fdiv st(l), st@) После выполнения этих команд в регистре stA) находится вычисленное значение величины z. Чтобы записать значение stA) в переменную z, в памяти выполним команды: fxch st(l) fstp DWORD PTR Z Вид окна работающего приложения показан на рис. 2.3. Рис. 2.3. Окно приложения, выполняющего вычисления по формуле с помощью команд сопроцессора Как и у обычного процессора, у математического сопроцессора имеются команды, выполняющие сравнение двух чисел. Далее приводится мнемони- мнемоника команд сравнения: WORD_INT LABEL WORD SHORT INT LABEL DWORD
72 Часть I. Основы эффективного программирования на ассемблере SHORT_REAL LABEL DWORD LONG_REAL LABEL QWORD fcom fcom ficom fcom fcomp ficomp fcomp fcompp ftst fxam STB) WORD_INT SHORT_REAL SHORT_INT LONG_REAL Результат сравнения сопроцессор отбрасывает, но устанавливает в соответ- соответствии с результатом внутренние флаги состояния. Перед тем, как опросить флаги состояния, программа должна считать слово состояния в память. Са- Самый простой способ — загрузить флаги состояния в регистр ан, а затем, чтобы облегчить себе задачу проверки условия, — в регистр флагов процес- процессора. В операции всегда участвует вершина стека, поэтому в команде надо указать только один регистр или ячейку памяти. После выполнения сравнения в слове состояния процессора содержится результат этой операции. При за- загрузке флагов состояния сопроцессора в регистр флагов процессора бит со помещается на место флага переноса cf, С2 — на место бита четности pf, a сз — на место zf. Для отражения результата сравнения необходимы только два бита состоя- состояния: сз и со. В табл. 2.1 приводится соотношение сравниваемых операндов и битов состояния. Таблица 2.1. Соотношение сравниваемых операндов и битов состояния СЗ СО Результат О О ST > источник 0 1 ST < источник 1 О ST = источник 1 1 ST и источник несравнимы Следующая программа (листинг 2.5) сравнивает два вещественных числа и выводит результат сравнения на экран.
Глава 2. Оптимизация вычислительных алгоритмов с помощью ассемблера 73_ // COMPARE_REAL.cpp : Defines the entry point for the console // application. #include "stdafx.h" int _tmain(int argc, _TCHAR* argv[]) float X, Y; int Flag = 0; X = 0; Y = 0; while (true) printf("\nEnter X: "); scanf("%f", &X) ; printf("Enter Y: "); scanf("%f", asm { finit fldz fid fcomp fstsw fwait sahf jb je mov jmp xly: mov jmp xeqy: mov &Y); DWORD PTR X DWORD PTR Y AX xly xeqy Flag, 2 ex Flag, 0 ex Flaa. 1
74 Часть I. Основы эффективного программирования на ассемблере ех: }; switch (Flag) { case 0: printf("X < Y\n"); break; case 1: printf("X * Y\n"); break; case 2: printf("X > Y\n"); break; default: break; } } return 0; После инициализации сопроцессора командой finit в вершину стека по- помещается переменная х. Команда f comp сравнивает число в вершине стека с переменной в памяти и, в зависимости от результата, устанавливает биты в слове состояния процессора. Биты состояния записываются затем в регистр состояния центрального процессора, где и анализируются. В зависимости от установленных битов выполняется переход на соответствующую ветвь про- программы. Фрагмент кода, выполняющий эти действия, выглядит так: finit fid DWORD PTR X fcorap DWORD PTR Y fstsw AX sahf Окно работающего приложения показано на рис. 2.4. Кроме команды f comp, существуют и другие варианты команды сравнения fcom, в частности — версия f icomp для сравнения целых чисел. Далее пред- представлен исходный текст программы, сравнивающей два целых числа (листинг 2.6).
Глава 2. Оптимизация вычислительных алгоритмов с помощью ассемблера 75 Рис. 2.4. Окно приложения, реализующего алгоритм сравнения вещественных чисел // COMPARE_INTS.срр : Defines the entry point for the console // application. #include "stdafx.h" int _tmain(int argc, _TCHAR* argv[]) int X, Y; int Flag = 0; X = 0; Y = 0; while (true) printf("\nEnter X: "); scanf("%d", &X); printf("Enter Y: ") ; scanf("%d", &Y); _asm { finit fild DWORD PTR X ficomp DWORD PTR Y fstsw AX fwait sahf jb xly
76 Часть I. Основы эффективного программирования на ассемблере j e " xeqy mov Flag, 2 jmp ex xly: mov Flag, 0 jmp ex xeqy: mov Flag, 1 ex: switch (Flag) case 0: printf("X < Y\n"); break; case 1: printf("X = Y\n"); break; case 2: printf("X > Y\n"); break; default: break; return 0; В целом исходный текст программы сравнения целых чисел почти не отли- отличается от текста программы для сравнения вещественных чисел. Единствен- Единственное отличие — вместо команд арифметики вещественных чисел (fid, fcomp) используются команды целочисленной арифметики (f iid, f icomp). Приведем еще один пример, показывающий технику использования команд сопроцессора в ассемблерном коде. Необходимо подсчитать, сколько раз встречается в массиве целых чисел определенное число. Исходный текст консольного приложения C++ .NET представлен в листинге 2.7.
Глава 2. Оптимизация вычислительных алгоритмов с помощью ассемблера 77_ ¦ Л/;с;инг 2. !. Ьр'Л pit^Vd г;иД(..:-:С; a KO;"M4C:;:vss:.j ВХОМД ;;:¦< им ц;".по: О ч;к:::;; // COUNT_NUMBER.cpp : Defines the entry point // for the console application. #include "stdafx.h" int _tmain(int argc, JTCHAR* argv[]) { int iarraytlO] = {-13, -7, .10, -5, 3, -7, -5, 4, -7, -3}; int *piarray = iarray; int num; printf("Array: "); for (int cnt = 0;cnt < sizeof (iarray)/4; cnt+-i-) { printf("%d ", *piarray++); } while (true) { printf("\nEnter number to found: ") ; scanf("%d", &num); cnt = 0/ int sf = sizeof(iarray)/4; _asm { mov ESI, DWORD PTR piarray mov ECX, DWORD PTR sf finit fild DWORD PTR num next_cmp: ficom DWORD PTR fESI] fstsw AX sahf jne skip inc cnt skip: sub ESI, 4
78_ Часть I. Основы эффективного программирования на ассемблере loop next_cmp fwait } printf("\nThe number %d occures = %d times\n", num, cnt); } return 0; Первые команды ассемблерного блока инициализируют регистры процессо- процессора esi и есх адресом последнего массива и его размером соответственно. После этого искомое значение загружается из переменной num в вершину математического сопроцессора командой fild DWORD PTR num Значение в вершине стека сравнивается последовательно с каждым элемен- элементом массива командой ficom DWORD PTR [ESI] которая устанавливает соответствующим образом биты в слове состояния. Извлечение и анализ этих битов выполняется с помощью команд fstsw AX sahf jne skip inc cnt Каждый раз при обнаружении совпадения элементов счетчик cnt инкремен- тируется. Переход к предыдущему элементу массива выполняется командой sub ESI, 4 По завершению цикла loop в счетчике cnt находится количество вхождений целого числа в массив. Если число в массиве не обнаружено, cnt = о. Вид окна работающего приложения представлен на рис. 2.5. Команды сравнения с извлечением из стека обеспечивают удобный способ очистки стека. Математический сопроцессор не имеет команды,' которая бы удобно извлекала операнд из стека, вместо нее можно использовать коман- команды сравнения с извлечением из стека. Эти команды также изменяют и ре- регистр состояния, поэтому их нельзя использовать, если биты состояния имеют значение для дальнейшей работы. Но в большинстве случаев эти команды позволяют быстро извлечь из стека один или два операнда. Так как сопроцессор регистрирует ошибку при переполнении стека, необходимо удалить все операнды из стека по окончании вычислений.
Глава 2. Оптимизация вычислительных алгоритмов с помощью ассемблера 79 Рис. 2.5. Окно приложения, выполняющего подсчет количества вхождений целого числа в массив Существуют две специальные команды сравнения. Это команда сравнения содержимого вершины стека с нулем ftst, с помощью которой можно бы- быстро определить знак содержимого вершины стека, и команда fxam. Команда f хат устанавливает все четыре флага регистра состояния (от сз до со включительно), показывая, какого типа число находится в вершине стека. Со- Сопроцессор может обрабатывать числа, представленные в любой форме, а не только нормализованные числа с плавающей точкой. При помощи команды f xam можно определить формат данных, находящихся в вершине стека. Если при арифметической обработке не делать чего-либо из ряда вон выхо- выходящего и не работать на пределе разрядной сетки сопроцессора, то вряд ли результат команды fxam должен вас заинтересовать. Мы не будем детально рассматривать реакцию сопроцессора на исключительные ситуации, иногда возникающие при вычислениях. Это очень обширная тема, и желающие уг- углубить свои знания могут обратиться к фирменному руководству Intel по процессору 387. Следующей группой функций, на которые мы обратим внимание, является группа степенных и тригонометрических функций. Эти команды позволяют сопроцессору вычислять математические выражения, содержащие логариф- логарифмы, экспоненты и тригонометрические функции. Далее приведен список этих команд: fsqrt fscale fprem frndint fxtract
80 Часть I. Основы эффективного программирования на ассемблере fabs fchs • fsin fcos fsincos fptan fpatan f2xml fyl2x fyl2xpl Наличие команд трансцендентных функций значительно усиливает вычис- вычислительную мощь процессора. Результат вычислений таких функций имеет высокую точность. Необходимо учитывать тот факт, что аргументы триго- тригонометрических функций задаются в радианах. Если, к примеру, мы хотим вычислить синус угла А, заданного в градусах, то для перевода в радианы можно использовать формулу Арад = А х PI / 180, где Ардд — величина угла в радианах, а А — величина угла в градусах. Рассмотрим небольшой пример, в котором будут вычисляться синус и коси- косинус угла. Программа (листинг 2.8), исходный текст которой-приводится да- далее, довольно проста. Листинг >,ё. Программа вычисления сннусч и: носин/с-з у:.па // SinCos.cpp : Defines the entry point for the console application. #include "stdafx.h" int _tmain(int argc, _TCHAR* argv[]) { float angle, angleRad, Sinus, Cosinus; while (true) { printf("\nEnter degrees: "); scanf("%f", &angle); angleRad = angle*3.14/180; _asm { finit
Глава 2. Оптимизация вычислительных алгоритмов с помощью ассемблера 81_ fid DWORD PTR angleRad fid DWORD PTR angleRad fsin fstp DWORD PTR Sinus fcos fstp DWORD PTR Cosinus fwait printf("The angle in degrees = %7.3f\n", angle) printf("Sinus of angle = %7.3f\n", Sinus); printf("Cosinus of angle = %7.3f\n", Cosinus); getchar(); return 0; Команды fsin и fcos вычисляют значения синуса и косинуса угла, находя- находящегося в вершине стека st@) . Команды не имеют операндов и возвращают результат в регистре st @). Рис. 2.6. Окно приложения, выполняющего вычисление синуса-косинуса угла
S2_ Часть I. Основы эффективного программирования на ассемблере Предьщушее значение регистра (угол) теряется после операции вычисления синуса. Вот почему возникает необходимость выполнения двух команд fid в нашей процедуре! Окно работающего приложения показано на рис. 2.6. Среди команд ассемблера для вычисления значений тригонометрических функций есть fsincos. Эта команда вычисляет синус и косинус угла, нахо- находящегося в вершине стека сопроцессора st@). Команда не использует опе- операнды и возвращает результат в регистрах st (о) и st A). При этом в st (о) возвращается значение синуса, а в регистре stA) — косинуса. Модифици- Модифицируем предыдущий пример так, чтобы можно было использовать команду fsincos. Модифицированный вариант программы представлен в листинге 2.9. // SinCosjnod.cpp : Defines the entry point for the console application. #include "stdafx.h" int _tmain(int argc, __TCHAR* argv[]) { float angle, angleRad, Sinus, Cosinus; while (true) { printf("\nEnter degrees: "); scanf("%f", sangle); angleRad = angle*3.14/180; _asm { finit fid DWORD PTR angleRad fsincos fxch st(l) fstp DWORD PTR Sinus fstp DWORD PTR Cosinus fwait } printf("The angle in degrees = %7.3f\n", angle); printf("Sinus of angle = %7.3f\n", Sinus);
Глава 2. Оптимизация вычислительных алгоритмов с помощью ассемблера 83_ printf("Cosinus of angle = %7.3f\n", Cosinus); getchar(); } return 0; Рассмотренные примеры демонстрируют далеко не все возможности языка ассемблера при выполнении математических операций. Примечательным свойством этого языка является возможность относительно легкой оптими- оптимизации кода, написанного на самом ассемблере! На языке C++ .NET, равно как и в других языках высокого уровня, ассемб- ассемблер помогает разработать эффективные решения для оптимизации критиче- критических участков программ, причем в довольно широком диапазоне производи- производительности и размера кода. Далее я приведу несколько примеров, успешно применяемых на практике для улучшения качества программного кода с помощью ассемблера. Поскольку мы рассматриваем математические опера- операции, посмотрим, как можно оптимизировать их работу. Рассмотрим не- несколько способов оптимизации. Способ 1. Можно использовать вместо команд математического сопроцессо- сопроцессора целочисленные инструкции. Операции пересылки данных вещественного типа с успехом могут быть заменены более быстродействующими команда- командами для целых чисел. Например, команды fid QWORD PTR [ESI] fstp QWORD PTR [EDI] можно заменить командами mov EAX,[ESI] mov EBX,[ESI+4] mov [EDI],EAX mov [EDI+4],EBX Способ 2. Проверка равенства нулю вещественного числа может быть вы- выполнена с помощью целочисленных инструкций. Посмотрим, как выглядит последовательность действий для тестирования равенства нулю с использо- использованием команд математического сопроцессора. Далее приведен исходный текст простого консольного приложения C++ .NET (листинг 2.10), где при- применяется такая проверка. // CHANGE_FPU_INT.срр : Defines the entry point for the console // application.
84 Часть I. Основы эффективного программирования на ассемблере #include "stdafx.h" int _tmain(int argc, JTCHAR* argv[]) { float fl = 0; float *pfl = &fl; bool isZero; while (true) printf("\nEnter real value: "); scanf("%f", _asm { mov mov finit fid ftst fstsw sahf jz mov ex: mov fwait pfl); ECX, 1 EBX, DWORD PTR pfl DWORD PTR [EBX] AX ex ECX, 0 DWORD PTR isZero, if (isZero) printf("Entered value equal 0\n"); else printf("Entered value not equal 0\n' } return 0; Эквивалентный вариант с использованием обычных команд ассемблера мог бы выглядеть так (показан только ассемблерный код): _asm { mov ECX, 1 mov EBX, DWORD PTR pfl
Глава 2. Оптимизация вычислительных алгоритмов с помощью ассемблера 85 mov add jz mov ex: mov EAX, EAX, ex ECX, DWORD DWORD EAX 0 PTR PTR [EBX] isZero, EC Дальнейшую оптимизацию алгоритма сравнения вещественного числа с ну- нулем можно провести, исключив ветвления из программного кода. Для этого необходимо найти эквивалент команды условного перехода jz ex.* Можно воспользоваться командой cmov с соответствующим условием. Фрагмент ас- ассемблерного кода после этих изменений будет выглядеть так: asm { mov mov mov add cmovz mov cmovnz mov ECX, EBX, EAX, EAX, EAX, ECX, EAX, 1 DWORD DWORD EAX ECX 0 ECX DWORD PTR PTR pfl PTR [EBX] isZero, EAX Необходимо учитывать, что последние два фрагмента исходного кода рабо- работают с операндами, имеющими размерность двойного слова. Если вещест- вещественное число имеет двойную точность (double precision, или QWORD), то необходимо протестировать только биты 32—64. Если эти биты равны 0, то и число равно 0. Окно работающего приложения для всех трех модификаций программного кода представлено на рис. 2.7. Способ 3. Оптимизировать математические вычисления можно и с использо- использованием команды загрузки адреса lea. Например, команда lea EAX, 3[-100][EDX+ECX] заменяет целую группу команд: mov EAX, ECX add EAX, EDX
86 Часть I. Основы эффективного программирования на ассемблере add EAX, 100 sub EAX, 3 Рис. 2.7. Окно приложения, выполняющего проверку вещественного операнда на О Исходный текст небольшого консольного приложения показан в листин- листинге 2.11. // LEAEX.cpp : Defines the entry point for the console application. #include "stdafx.h" int _tmain(int argc, JTCHAR* argv[]) int il, i2, ires; while (true) printf("\nEnter il -> ECX: "); scanf("%d", &il); printf("\nEnter i2 -> EDX: "); scanf("%d", &i2); _asra { mov EDX, DWORD PTR il mov ECX, DWORD PTR i2
Глава 2. Оптимизация вычислительных алгоритмов с помощью ассемблера 87_ lea EAX, 3[-100][EDX+ECX] mov DWORD PTR ires, EAX printf("Calculated result = %d\n", ires) return 0; Исходный текст программы довольно прост и нет необходимости давать какие-либо разъяснения. Вид окна работающего приложения показан на рис. 2.8. .Щ, <L?&\ 11 iAi,' LlSi :Lii:..S!iISil:i*..::i!;JilMllZiLs Рис. 2.8. Окно приложения, демонстрирующего использование команды загрузки адреса для выполнения математических операций Мы рассмотрели далеко не полный набор математических функций ассемб- ассемблера. Главная цель — убедить читателя в полной мере использовать матема- математический сопроцессор в программах на ассемблере. При желании можно модифицировать существующие или написать свои собственные математи- математические алгоритмы, которых нет в языках высокого уровня. Преимущества ассемблера проявляются и при обработке строк и массивов данных. Для выполнения таких операций была разработана целая группа команд, в терминологии Intel именуемая командами строковых примитивов. Под обработкой строк мы будем понимать выполнение следующих операций: ? сравнение двух строк; П копирование строки-отправителя в строку-получатель; ? считывание строк из устройства или файла; О запись строки в устройство или файл; ? определение размера строки; П нахождение подстроки в заданной строке; ? объединение двух строк (конкатенация).
88_ Часть I. Основы эффективного программирования на ассемблере Операции над строками широко используются в языках высокого уровня. Ассемблерная реализация таких операций позволяет существенно повысить быстродействие программ на языках высокого уровня, особенно если требу- требуется обработать большое число строк и массивов. Рассмотрим вначале ос- основные команды языка ассемблера для обработки строк. Строка символов или чисел, с которыми программа работает как с группой, является обычным типом данных. Программа пересылает строку из одного места в другое, сравнивает ее с другими строками, а также ищет в ней за- заданное значение. Обычным типом данных является строка символов. Программа представляет каждое слово, предложение либо другую структуру последовательностью символов в памяти. Функции редактирования, напри- например, в большой степени используют операции поиска и пересылки. Строко- Строковые команды процессора выполняют эти операции с минимальными про- программными затратами, а также при минимальном времени исполнения. Сначала давайте обсудим принципы работы со строками. Программа может выполнять строковые операции над байтами, словами и двойными словами. Строковые команды не применяют способы адресации, используемые ос- остальными командами обработки. Они адресуют операнды комбинациями регистров esi или edi. Операнды источника используют регистр esi, а операнды результата — ре- регистр edi. Все строковые команды корректируют адрес после выполнения операции. Строка может состоять из нескольких элементов, но команды обработки строк могут обрабатывать только один элемент в каждый момент времени. Автоматический инкремент (увеличение) или декремент (умень- (уменьшение) адреса операнда позволяет быстро обрабатывать строковые данные. Флаг направления (Direct Flag) в регистре состояния определяет направле- направление обработки строк. Если он равен 1, то адрес уменьшается, а если он сброшен в 0, то адрес увет личивается. Сама величина инкремента или декремента адреса определяется размером операнда. Например, для символьных строк, в которых размер операндов равен 1 байту, команды обработки строк изменяют адрес на 1 после каждой операции. Если обрабатывается массив целых чисел, в кото- котором каждый операнд занимает 4 байта, то строковые команды изменяют ад- адрес на 4. После выполнения операции указатель адреса в регистрах esi или edi ссылается на следующий элемент строки. Рассмотрим представление строк в разных языках программирования. Наи- Наиболее часто используются строки с завершающим нулем (null-terminated strings). Они используются в языке Сив операционных системах Windows. Вот как выглядит такая строка на языке ассемблера String О DB "NULL-TERMINATED STRING", О
Глава 2. Оптимизация вычислительных алгоритмов с помощью ассемблера 89_ Мы будем рассматривать в основном строки с завершающим нулем. Можно выделить пять основных команд для работы со строками. Часто эти коман- команды называют командами строковых примитивов. К ним относятся: О movs — команда для перемещения строки данных из одного участка па- памяти в другой; П lods — команда загрузки строки, адрес которой указан в регистре esi, в регистр-аккумулятор еах (ах, ad; П stos — команда сохранения содержимого регистра еах (ах, ad в памяти по адресу, указанному в регистре edi; ? cmps — команда сравнения строк, расположенных по адресам, содержа- содержащимся в регистрах esi и edi; О seas — команда сканирования строк, которая сравнивает содержимое регистра еах (ах, al) с содержимым памяти, определяемым регистром EDI. Каждая команда обработки строк имеет три допустимых формата. Напри- Например, команда movs может иметь одно из представлений: movsb, movsw, movsd. Команда movsb может использоваться только для работы с однобайтовыми операндами, movsw — для работы со словами, a movsd — для работы с двой- двойными словами. Суффиксы ь, w и d определяют шаг инкремента и декре-. мента для индексных регистров esi и edi. Если команда используется в об- общем формате, то размерность операндов должна быть определена явно. Перед выполнением команд строковых примитивов необходимо загрузить в регистры esi и/или edi адреса обрабатываемых ячеек памяти. Для выполнения повторяющихся операций со строками практически всегда используется префикс повторения rep. Это позволяет выполнить строковую операцию количество раз, определяемое содержимым регистра есх. Следующая программа демонстрирует копирование одной строки в другую, причем обе строки имеют тип cstring. С помощью мастера приложений C++ .NET разработаем приложение на основе диалогового окна. На главной форме приложения разместим два элемента Edit control, с которыми свяжем переменные строки-источника csrc и строки-приемника cDst, имеющие тип cstring. В поле редактирования csrc вводится строка, кото- которую необходимо скопировать в cDst и отобразить на экране. Кроме этого, добавим на форму два элемента static Text и кнопку Button. При нажатии на кнопку содержимое csrc копируется в строку cDst. Для большей наглядности в примере используются промежуточные переменные si и s2 типа cstring. Исходный текст обработчика нажатия кнопки показан в листинге 2.12.
90 Часть I. Основы эффективного программирования на ассемблере void CCP_STRINGDlg::OnBnClickedButtonl() { // TODO: Add your control notification handler code here UpdateData(TRUE); CString si, s2; LPCTSTR lpsl, lps 2; si = cSrc; s2 = cDst; lpsl = sl.GetBufferC2); int Isrc = sl.GetLengthO ; asm next: I lea lea mov eld lodsb stosb loop ESI, EDI, ECX, next DWORD DWORD Isrc PTR PTR lpsl lps 2 s2 = (CString)lps2; cDst = s2; UpdateData(FALSE); Проанализируем код обработчика. Содержимое cSrc помещается в строку si. Далее строка si копируется в s2. Наконец, содержимое строки s2 ото-
Глава 2. Оптимизация вычислительных алгоритмов с помощью ассемблера 91_ бражается в поле редактирования, соответствующее переменной cDst. Для работы со строками типа cstring удобно ссылаться на них как на строки с завершающим нулем, поэтому нужно знать адрес буфера строки и ее размер. Следующие операторы позволяют получить эти параметры: CString si, s2; LPCTSTR lpsi, Ips2; si = cSrc; s2 = cDst; lpsi = sl.GetBufferC2); int Is re = sl.GetLengthO ; Размер буфера строк выбран равным 32 из соображений удобства. Далее вы- выполняется копирование строки с указателем lpsi в строку, определяемую указателем ips2, с помощью ассемблерных команд: asm { lea lea mov eld next: lodsb stosb loop ESI, EDI, ECX, next DWORD DWORD lsrc PTR lpsi PTR Ips2 Перед началом операции копирования необходимо установить флаг направ- направления так, чтобы адреса источника и приемника увеличивались после каж- каждой итерации. Для этого нужно установить флаг в 0 командой cid. Размер строки lsrc помещается в регистр есх. Операция копирования выполняется двумя командами lodsb и stosb. Команда lodsb загружает байт из ячейки по адресу esi (строка lpsi) в ак- аккумулятор al, а команда stosb записывает полученный байт из аккумулято- аккумулятора в ячейку памяти по адресу, содержащемуся в edi (строка ips2). После операции чтения-записи содержимое регистров esi и edi автомати- автоматически увеличивается на 1. Величина инкремента в этом случае определяется типом строковой команды. В программе копируются байты, поэтому и адре- адреса будут увеличиваться на 1.
92_ Часть I. Основы эффективного программирования на ассемблере Для того чтобы этот фрагмент кода отработал правильно, объем памяти, вы- выделенный для строки-приемника (lps2), должен быть, по крайней мере, не меньше размера строки-источника. Окно работающего приложения с результатом копирования изображено на рис. 2.9. Рис. 2.9. Окно приложения, выполняющего операцию копирования одной строки в другую Может показаться удобным использовать комбинацию команд lods и stos для перемещения данных из одного места в другое, но для этой цели суще- существует команда пересылки строки movs. Она считывает данные по адресу памяти, находящемуся в регистре esi, и помещает их по адресу, указывае- указываемому регистром edi. При этом содержимое регистров esi и edi изменяется так, чтобы указывать на следующие элементы строк. Команда movs не за- загружает регистр-аккумулятор во время пересылки. В команду movs передаются адреса операндов. Только movs и еще одна стро- строковая команда, cmps, работают с двумя операндами памяти. Все остальные команды требуют, чтобы один или оба операнда находились в одном из ре- регистров микропроцессора. Команда movs, так же как и команды lods и stos, работает как с байтами, так и со словами. Поскольку строковые команды имеют дело с жестко заданными адресами, для определения типов служат только операнды, написанные программи- программистом. Команда должна иметь оба операнда одинакового типа. Программист также может указать тип пересылки частью кода операции, т. е. командой movsb в случае байтовых строк или команда movsw — для строк, состоящих из слов. Если в программе используется основная форма — команда movs, то ассемблер проверяет переменные на правильность сегментной адресации и на совпадение типов. Команда movs с префиксом rep представляет собой эффективную команду пересылки блока памяти. Имея счетчик символов в регистре есх и указы- указывающий направление пересылки флаг направления df, команда rep movs пересылает данные из одного места памяти в другое очень быстро.
Глава 2. Оптимизация вычислительных алгоритмов с помощью ассемблера 93_ Когда регистр есх достигает нуля, последняя итерация сканирования сбра- сбрасывает флаг нуля, показывая, что соответствия нет. Упрощенный фрагмент предыдущей программы, в котором вместо двух команд lodsb и stosb используется команда копирования строк movsb, приведен в листинге 2.13. asm { lea ESI, DWORD PTR lpsl lea EDI, DWORD PTR Ips2 mov ECX, Isre eld next: movsb loop next Еще больше можно упростить исходный текст программы, если использо- использовать в ассемблерном блоке команду movsb с префиксом повторения rep. Префикс rep использует в качестве параметра содержимое регистра есх: _asm { lea ESI, DWORD PTR lpsl lea EDI, DWORD PTR Ips2 mov ECX, Isre eld rep movsb } Операции копирования можно выполнять также и для массивов целых или вещественных чисел. В следующей консольной программе (листинг 2.14) содержимое целочисленного, массива sarray копируется в массив darray с помощью ассемблерных команд. Л и и > и н; ? 14. К; j , 1 и u с в ._.< н и о V- а с с ¦/• о а ц и г-хл*. чум: г- ¦ i ¦: и ¦; г ? о л > ¦> з >.;; /. а н и о м о ¦::;:.<¦: >л 6 -. ¦¦¦ v ;:.;;¦ // COPY_INT_ARRAYS.ерр : Defines the entry point for the console // application.
94 Часть I. Основы эффективного программирования на ассемблере #include "stdafx.h" int _tmain(int argc, _TCHAR* argv[]) { int sarray[6] = {245, 11, -34, 56, 7, 19}; int darray[8] - {0, 0, 14, 45, 56, 7, 21, -56}; int lenarray = sizeof(sarray) / 4; printf("sarray: "); for (int cnt = 0;cnt < sizeof(sarray)/4;cnt++) printf("%d\t", sarray[cnt]); printf("\ndarray: "); for (int cnt = 0;cnt < sizeof(darray)/4;cnt++) printf("%d\t", darray[cnt]); _asm { eld mov ECX, DWORD PTR lenarray lea ESI, DWORD PTR sarray lea EDI, DWORD PTR darray rep movsd }; printf("\ndarray after copy: "); for (int cnt = 0;cnt < sizeof(darray)/4;cnt++) printf("%d\t", darray[cnt]); getchar(); return 0; Исходный текст программы несложен для анализа. Следует обратить внима- внимание на применение команды rep movsd для копирования двойных слов. Ок- Окно приложения показано на рис. 2.10. Команда movs может использоваться для еще одной весьма полезной опера- операции над двумя строками. Эта операция называется конкатенацией. При ее выполнении к строке-приемнику добавляются символы строки-источника. В этом случае программист должен сам позаботиться о достаточном размере буфера приемника. Очень ча'сто используется прием, когда несколько по- последних элементов строки-приемника заполняются пробелами, и на их ме- место помещаются элементы строки-источника. Буфер приемника должен
Глава 2. Оптимизация вычислительных алгоритмов с помощью ассемблера 95 иметь размер, как минимум, равный сумме размеров сцепляемых строк. Да- Далее приводится исходный текст консольного приложения C++ .NET, де- демонстрирующий именно такой подход (листинг 2.15). .,. Рис. 2.10. Окно приложения, выполняющего копирование элементов массивов Лис!и:-;г 2.15. ;\OHr-.:j,¦ ::н;-цн>- строк с помощью "ссемипорл г; прогоп^ш // STRINGS_CONCAT.срр : Defines the entry point for the console // application. #include "stdafx.h" int _tmain(int argc, _TCHAR* argv[]) char sl[] = "STRING1 "; char s2[] = "STRING2"; printf("String-destination: %s\n", si); printf("String-source: %s\n", s2) ; int Is2 = strlen(s2); _asm { lea ESI, DWORD PTR s2 lea EDI, DWORD PTR si eld mov AL, ' ' again: scasb j e next jmp again
96 Часть I. Основы эффективного программирования на ассемблере next: dec EDI mov BYTE PTR [EDI], •+' inc EDI mov ECX, Is2 rep movsb }; printf("Result of concatenation : %s\n", si); getchar () ; return 0; Ассемблерные команды lea ESI, DWORD PTR s2 lea EDI, DWORD PTR si eld mov AL, ' ' загружают адреса строк si и s2 в регистры esi и edi, а также устанавливают флаг направления для инкремента адреса. В регистр al помешается символ пробела для определения адреса в буфере строки si, начиная с которого помещаются символы строки s2. Команда mov BYTE PTR [EDI], '+' помещает знак плюс между двумя частями строки. Копирование строки s2 на место пробелов в строке si выполняется командой rep movsb. При этом в регистре есх находится размер строки s2. Окно работающего приложения показано на рис. 2.11. Рис. 2.11. Окно приложения, выполняющего конкатенацию двух строк Конкатенация массивов целых или вещественных чисел немного отличается от аналогичной операции с символьными строками, хотя во многом они
Глава 2. Оптимизация вычислительных алгоритмов с помощью ассемблера 97_ схожи. Принимающий массив должен иметь необходимое пространство для добавления новых элементов из массива-источника. Необходимое смещение в массиве-приемнике должно пересчитываться с учетом размерности в бай- байтах элемента массива. Исходный текст консольной программы, выполняют щей конкатенацию двух массивов целых чисел, представлен далее в лис- листинге 2.16. // CONCAT_INT_AARAYS.срр : Defines the entry point for the console // application. #include "stdafx.h" int _tmain(int argc, _TCHAR* argv[]) { int il[] = {23, 44, 8, 0, 0, 0, 0}; int i2[] = {-56, 7, -3, 7}; int ilen = sizeof(i2)/4; printf("Source array i2:\t "); for (int cnf = 0; cnt < sizeof(i2)/4;cnt++) printf("%d\t", i2[cnt]); printf("\nDest array i2:\t\t "); for (int cnt = 0; cnt < sizeof(il)/4;cnt++) printf("%d\t", il[cnt]); _asm { lea ESI, DWORD PTR i2 lea EDI, DWORD PTR il eld mov EAX, 0 again: scasd j e next jmp again next: sub EDI,4 mov ECX, ilen
j?8 Часть I. Основы эффективного программирования на ассемблере rep movsd printf{"\nConcatenated arrays:\t ") ; for (int cnt = 0; cnt < sizeof(il)/4;cnt++) printf("%d\t", il[cnt]); getchar(); return 0; В этом фрагменте кода элементы массива-источника i2 записываются в массив-приемник il, начиная с четвертого элемента. В регистры esi и edi помещаются адреса первых элементов этих массивов, а в регистр есх — ко- количество записываемых элементов, равное^размеру массива i2. Окно приложения показано на рис. 2.12. Рис. 2.12. Окно приложения, выполняющего конкатенацию массивов Другой распространенной операцией над строками и массивами является сравнение. Для сравнения элементов строк и массивов используется коман- команда cmps и ее модификации. Следующий фрагмент программного кода на ас- ассемблере MASM (листинг 2.17) сравнивает две строки символов. SRC LSRC DST LDST FLAG eld lea DB " EQU DB " EQU DD 0 ESI, STRING $-SRC STRING $-DST SRC 1 ' 1"
Глава 2. Оптимизация вычислительных алгоритмов с помощью ассемблера 99_ lea EDI, DST mov ECX, LSRC mov EDX, LDST cmp ECX, EDX je next_check jmp continue next_check: repe CMPSB je equal mov EAX, FLAG jmp continue equal: mov FLAG, 1 continue: В этом фрагменте кода используется команда cmpsb, т. к. сравнение выпол- выполняется побайтно, с префиксом повторения гере. Если строки одинаковы, то переменной flag присваивается значение 1, а если не равны — то 0. В этом примере строки не равны, поэтому переменная flag будет сброшена в 0. Тот же результат мы получим, если строки содержат одинаковое число элемен- элементов, но отличаются хотя бы одним. Если размер строк разный, то flag так- также будет сброшен в 0. Предьщущий фрагмент кода легко модифицировать для работы с целыми числами. Фрагмент программного кода для сравнения массивов целых чисел приведен в листинге 2.18. ISRC DD 3, 16, 89, 11 LISRC EQU ($-ISRC)/4 IDST DD 3, 16, 89, 11, 9 LIDST EQU ($-IDST)/4 FLAG DD eld lea ESI, ISRC lea EDI, IDST mov ECX, LISRC mov EDX, LIDST
100 Часть I. Основы эффективного программирования на ассемблере cmp ECX, EDX je next_check jmp continue next_check: repe cmpsd je equal mov EAX, FLAG jmp continue equal: mov FLAG, 1 continue: Различия в программных кодах для обработки массивов целых чисел и бай- байтов связаны, прежде всего, с размерностью операндов. Поскольку целые числа занимают в памяти 4 байта, то вместо cmpsb необходимо использовать команду cmpsd для сравнения двойных слов. В регистр есх по-прежнему за- заносим размер исходного массива, но теперь эта величина выражена количе- количеством двойных слов. Вот почему мы делим полученные значения на 4: ISRC DD 3, 16, 89, 11 LISRC EQU ($-ISRC)/4 IDST DD 3, 16, 89, 11, 9 LIDST EQU ($-IDST)/4 Еще один полезный пример — заполнение области памяти определенным символом или числом. Чтобы заполнить, например, символьную строку пробелами, можно написать код, приведенный в листинге 2.19. DB "Эта строка будет заполнена пробелами' SRC LSRC eld mov mov lea rep DB "Эта EQU $-SRC AL, ' ' ECX, LSRC EDI, SRC stosb
Глава 2. Оптимизация вычислительных алгоритмов с помощью ассемблера 101_ Чтобы заполнить массив целых чисел нулями, необходимо использовать фрагмент кода, представленного в листинге 2.20. ISRC DD 3, 16, 89, 11, -99, 4 LISRC EQU ($-ISRC)/4 eld lea EDI, ISRC mov ECX, LISRC mov EAX, 0 rep stosd Строковые команды ассемблера очень полезны для оптимизации программ, написанных на Visual C++ .NET. Команды копирования строк, конкатена- конкатенации, поиска элементов, сравнения строк и заполнения области памяти оп- определенными значениями есть в любом языке высокого уровня. Ассемблер- Ассемблерный вариант реализации таких команд, как правило, требует намного меньше программного кода и выполняется быстрее. Рассмотрим еще один пример операции со строками. Очень часто требуется преобразовывать символы нижнего регистра клавиатуры в символы верхне- верхнего. В этом фрагменте кода использование строковых команд может неоправ- неоправданно усложнить программу, поэтому будем использовать обычные операто- операторы. Полностью исходный текст консольного приложения на C++ .NET выглядит так, как показано в листинге 2.21. // CONVERT_TO_UPPER.ерр : Defines the entry point for the console // application. #include "stdafx.h" int _tmain(int argc, _TCHAR* argv[]) { char sl[] = "string must be converted to upper"; int lsl = strlen(sl);
102 Часть I. Основы эффективного программирования на ассемблере printf("Before: %s\n", si); _asm { lea ESI,- DWORD PTR si mov ECX, DWORD PTR lsl next: mov cmp jb cmp ja and mov next_addr: inc loop AL, BYTE PTR [ESI] AL, 'a' next addr AL, fz' next addr AL, Odfh BYTE PTR [ESI], AL ESI next printf("After: %s\n", si); getchar(); return 0; Преобразование символов к верхнему регистру выполняется в ассемблерном блоке. Перед началом преобразования загружаем в регистр esi адрес строки si, а в регистр есх — ее размер lsl. Поскольку мы имеем дело с литерами, то анализ выполняется для символов 'a'-'z1, не затрагивая остальные. Алгоритм преобразования реализован в.следующем фрагменте профаммного кода: next: mov AL, BYTE PTR [ESI] cmp AL, 'a' jb next_addr cmp AL, 'z' j a next_addr and AL, Odfh mov BYTE PTR [ESI], AL next_addr: inc ESI loop next
Глава 2. Оптимизация вычислительных алгоритмов с помощью ассемблера 103_ Окно работающего приложения изображено на рис. 2.13. Рис. 2.13. Окно приложения, преобразующего символы нижнего регистра строки в символы верхнего регистра Чтобы у читателя не сложилось впечатление, будто операции со строками можно эффективно выполнять только используя строковые команды, я при- приведу пример программы, где нет ни одной строковой команды. Операции над строками можно успешно выполнять и при помощи обычных команд ассемблера. Исходный текст консольного приложения для сравнения двух строк (листинг 2.22) демонстрирует такой подход. // CMP_STRINGS_WITHOUT_PRIMITIVES.срр : Defines the entry point for the // console application. #include "stdafx.h" int _tmain(int argc, _TCHAR* argv[]) { char sl[] = "string 1"; char s2[] = "stRing 1"; bool result; printf("String 1: %s\n", si); printf("String 2: %s\n", s2); _asm { lea ESI, DWORD PTR si //" адрес строки si lea EDI, DWORD PTR s2 // адрес строки s2 again: mov AL, BYTE PTR [ESI]
104 Часть I. Основы эффективного программирования на ассемблере mov DL, BYTE PTR [EDI] push EAX push EDX xor AL, DL pop EDX pop EAX jz streq jmp strnot_eq streq: test AL, DL jz succ inc ESI inc EDI jmp again strnot_eq: mov EAX, 0 jmp quit succ: mov EAX, 1 quit: mov DWORD PTR result, EAX }; 'if (result) printf("Equal\n") ; else printf("Not equal\n"); getchar () ; return 0; В ассемблерном блоке адрес строки-источника помещается в регистр esi. a адрес строки-приемника — в регистр edi. Элементы строк помещаются в регистры al и dl, после чего сравнивается их содержимое: mov AL, BYTE PTR [ESI] mov DL, BYTE PTR [EDI] push EAX push EDX xor AL, DL
Глава 2. Оптимизация вычислительных алгоритмов с помощью ассемблера 105 pop EDX pop EAX jz streq jmp strnot_eq Если символы не равны, то происходит выход из процедуры с возвратом 0 в основную программу. Если процедура обнаруживает равенство символов, то они проверяются на равенство 0 (команда перехода jz streq): streq: test AL, DL j z succ inc ESI inc EDI jmp again Если элементы равны 0, то очевидно, что достигнут конец строки, и срав- сравнение прошло успешно, т. е. строки равны. В этом случае в переменной result возвращается значение 1. Если же элементы не равны 0 (хотя и рав- равны между собой), то выполняется переход на следующие адреса в строках, и цикл сравнения повторяется. С помощью команд push EAX push EDX xor AL, DL pop EDX pop EAX достигается дополнительный эффект оптимизации. Дело в том, что процес- процессоры Pentium (включая последние модели), содержат кэш команд и данных размером по 8 или 16 килобайт каждый, известные под названием "кэш первого уровня" (level one cache). С точки зрения производительности про- программы, имеет чрезвычайно важное значение, чтобы критические участки кода размещались в кэше команд. Очень желательно уменьшить размер кода — это позволит выполнять условные ветвления и циклические вычис- вычисления большей вложенности в кэше команд, что значительно увеличивает производительность работы приложения. Применение коротких инструкций позволяет разместить большую часть ко- кода в кэше команд. В нашем случае однобайтовые команды push reg и pop
106 Часть I. Основы эффективного программирования на ассемблере reg уменьшают размер программного кода, что для вычислений с большой вложенностью оказывается весьма актуальным. В нашем случае строки не равны, поэтому в окно работающего приложения (рис. 2.14) будет выведено соответствующее сообщение. Ш=ш Рис. 2.14. Окно приложения, выполняющего операцию сравнения двух строк Как видим, при манипуляциях со строками можно обходиться и без специа- специализированных команд, однако код получается несколько громоздким за счет дополнительных операций инкремента-декремента адресов и дополнитель- дополнительного анализа условий равенства символов и конца строк. Самая высокая скорость выполнения строковых операций достигается обычно при копиро- копировании одной строки в другую или при перемещении элементов строки из одной области памяти в другую. Это особенно заметно при перемещении больших объемов данных. .Меньший выигрыш в производительности по сравнению с обычными командами дают команды поиска и сканирования. На скорость выполнения строковых операций влияет и размерность операндов. Мы рассмотрели только небольшую часть тех возможностей, которые пре- предоставляет язык ассемблера в плане оптимизации обработки данных. В по- последующих главах мы будем использовать рассмотренный здесь материал и продемонстрируем дополнительные возможности улучшения качества про- программных продуктов.
Глава 3 Разработка и использование подпрограмм на ассемблере Довольно часто в программах, особенно больших, приходится несколько раз решать одну и ту же подзадачу и, следовательно, многократно выписывать одинаковую группу команд, выполняющих эту подзадачу. Чтобы избежать повторения такой группы команд, ее обычно разрабатывают один раз и оформляют соответствующим образом, а затем в нужных местах программы просто передают управление на эти команды, которые, отработав, возвра- возвращают управление обратно. Такая группа команд, которая решает некоторую подзадачу и которая организована таким образом, что ее можно использо- использовать любое число раз и в любых местах программного кода, называется под- подпрограммой или процедурой. По отношению к подпрограмме остальную часть программы принято называть основной профаммой. В этой главе мы будем рассматривать принципы построения процедур на ассемблере. Эти процедуры помещаются в файл с расширением asm и компилируются в от- отдельные файлы объектных модулей, имеющие расширение obj. Для исполь- использования процедур, находящихся в объектном модуле, достаточно включить файл с расширением obj в проект на C++ и вызвать в нужном месте проце- процедуру в соответствии с соглашением о вызовах, принятым в C++ .NET. Ассемблерной подпрофамме соответствует функция в C++, а обращение к подпрофамме соответствует вызову функции. Реализация подпрофамм на языке ассемблера более сложный процесс, чем описание функций в C++ .NET. Далее в этой главе термины "подпрофамма", "процедура" и "функция" будут использоваться как синонимы. В C++ многие аспекты работы с подпрограммами скрыты от разработчика, а их реализация возложена на компилятор, в то время как в ассемблере много приходится делать самому профаммисту. Писать подпрофаммы на ассемблере сложнее, чем на C++, однако использование ассемблера дает полный контроль над профаммным кодом и позволяет достичь большей степени оптимизации приложения в целом. Мы будем рассматривать разра- разработку процедур в контексте соглашений, принятых для макроассемблера
108 Часть /. Основы эффективного программирования на ассемблере MASM 6.14 фирмы Microsoft, хотя основные принципы верны для любого другого ассемблера на платформе Intel. Как объявить процедуру на ассемблере? Ее описание выглядит так: <имя процедуры> ргос <параметр> <тело процедуры> <имя процедуры> endp • Перед телом процедуры (ее командами) ставится директива ргос (procedure), а за ним — директива endp (end of procedure). Например, фрагмент кода с объявлением процедуры AsmSub мог бы выглядеть так: AsmSub ргос ret AsmSub endp Процедура должна заканчиваться командой ret. В одном файле ASM можно размещать несколько процедур. Вот пример компоновки процедур с имена- именами Asmsubi и Asmsub2 в одном модуле: .code AsmSubl ret AsmSubl AsmSub2 ret AsmSub2 end proc endp proc endp Точкой входа в процедуру считается директива ргос. Следует обратить вни- внимание, что в директиве ргос после имени не ставится двоеточие, однако это имя считается меткой и указывает на первую команду процедуры. Имя про- процедуры можно указать в команде перехода, и тогда будет осуществлен пере- переход на первую команду процедуры. У директивы ргос есть параметр — это либо near (близкий), либо far (дальний). Параметр может и отсутствовать, тогда считается, что он равен near (поэтому параметр near обычно не указывается). При использовании
Глава 3. Разработка и использование подпрограмм на ассемблере 109 параметра near или при отсутствии параметра процедура называется "близкой", при параметре far — "дальней". К близкой процедуре можно об- обращаться только из того сегмента команд, где она описана, а к дальней про- процедуре — из любых сегментов команд (в том числе и из того, где она описа- описана). Этим и отличаются близкие и дальние процедуры. Для 32-разрядных приложений, которые рассматриваются в этой книге, все вызовы процедур считаются близкими. Следует отметить, что в ассемблере имена и метки, описанные в процедуре, не являются локальными, поэтому они должны быть уникальными и не совпадать с другими именами, используемыми в программе. В ассемблере можно описать одну процедуру внутри другой, но никаких преимуществ это не дает, из-за чего вложенность процедур обычно не используется. . Проанализируем, как выполняется вызов процедур и возврат из них. При программировании на C++ для запуска процедуры достаточно указать ее имя й фактические параметры. Сама работа процедуры и возврат управле- управления в основную программу скрыты от программиста компилятором. Но ес- если писать процедуру на ассемблере, то все переходы между основной про- программой и процедурой приходится реализовывать нам самим. Рассмотрим, как это делается. Здесь две проблемы: как из основной программы заставить работать про- процедуру и как вернуться из процедуры в основную программу. Первая про- проблема решается просто: достаточно выполнить команду перехода на первую команду процедуры, т. е. указать в команде перехода имя процедуры. Слож- Сложнее со второй проблемой. Дело в том, что обращаться к процедуре можно из разных мест основной программы, а потому и возвращаться из процедуры надо в разные места. Сама процедура не знает, куда ей надо вернуть управ- управление, зато это знает основная программа. Поэтому при обращении к про- процедуре основная программа обязана сообщить ей так называемый адрес воз- возврата — адрес той команды основной программы, на которую процедура обязана сделать переход по окончании своей работы. Обычно это адрес команды, следующей за командой обращения к процедуре. Именно этот адрес основная программа и сообщает процедуре, именно по нему процеду- процедура и выполняет возврат в основную программу. Поскольку при разных об- обращениях к процедуре ей указывают разные адреса возврата, то она и воз- возвращает управление в разные места основной программы. Как сообщать адрес возврата? Это можно сделать по-разному. Во-первых, его можно передать через регистр: основная программа записывает в неко- некоторый регистр адрес возврата, а процедура извлекает его оттуда и делает по нему переход. Во-вторых, это можно сделать через стек: прежде чем обра- обратиться к процедуре, основная программа записывает адрес возврата в стек, а процедура затем считывает его отсюда и использует для перехода. Обще-
110 Часть I. Основы эффективного программирования на ассемблере принято передавать адрес возврата через стек, поэтому в дальнейшем мы будем рассматривать только этот способ передачи адреса возврата. Передачу адреса возврата через стек и возврат по этому адресу можно реали- реализовать с помощью тех команд, которые мы уже знаем. Однако в реальных программах процедуры используются очень часто, поэтому в систему команд процессора включены специальные команды, которые упрощают реализацию переходов между основной программой и процедурами. Это команда call и известная нам ret. Основные варианты этих команд следующие: call <имя процедуры> ret Команда call записывает адрес следующей за ней команды в стек и затем осуществляет переход на первую команду указанной процедуры. Команда ret считывает из вершины стека адрес и выполняет переход по нему. Рассмотрим следующий пример. Пусть в основной программе требуется вы- вывести на экран целое число, вычисленное по формуле ii - 12 - 100, где ii и 12 — целые числа. Для вычислений по этой формуле разработаем две функции на ассемблере и запишем их в файл с расширением asm. Исход- Исходный текст, включенный в этот файл, представлен в листинге 3.1. asmsub proc mov ЕАХ, il sub EAX, i2 call sublOO ret asmsub endp sublOO proc sub EAX, 100 ret sublOO endp В начале процедуры asmsub вычисляется разность ii - i2, и промежуточ- промежуточный результат помещается в регистр еах. Команда call subioo записывает в стек адрес следующей за ней команды и передает управление на начало процедуры subioo — на команду sub еах, юо. Эта процедура возвращает в регистре еах окончательное значение, равное и - i2 ~ 100. После этого
Глава 3. Разработка и использование подпрограмм на ассемблере . 1JJ_ команда ret извлекает из стека находящийся там адрес и делает переход по нему. Тем самым возобновляется работа процедуры asmsub с команды, сле- следующей за командой call subioo. Существует несколько вариантов команды call. Мы рассмотрели основной вариант, когда в качестве ее операнда указывается имя процедуры. Но мож- можно в качестве операнда использовать регистр. В этом случае адрес вызывае- вызываемой процедуры помещается в регистр. Предыдущий пример можно видоиз- видоизменить так, как показано в листинге 3.2. asmsub mov sub push lea call pop ret asmsub sublOO sub ret sublOO proc EAX, EAX, EBX EBX, EBX EBX endp proc EAX, endp il 12 sublOO 100 Наиболее важными для понимания принципов работы процедур в этом примере являются следующие четыре строки: push EBX lea EBX, sublOO call EBX pop EBX Первая команда сохраняет регистр евх в стеке, перед тем как его модифи- модифицировать. Это очень важный момент. Дело в том, что в ПК не так уж и много регистров, и в то же время чуть ли не в каждой команде используется тот или иной регистр. Поэтому с большой вероятностью основной програм- программе и процедуре могут потребоваться для работы одни и те же регистры, что
112 Часть I. Основы эффективного программирования на ассемблере усложнит их применение. Можно разработать приложение так, чтобы ос- основная программа и процедура использовали разные регистры, хотя сделать это довольно сложно — количество регистров процессора ограничено. По- Поэтому обычно код подпрограмм не делает никаких предположений об ис- использовании регистров основной программой, а просто сохраняет все ис- исходные значения регистров. Регистр евх — один из наиболее часто используемых основной программой, и поэтому важно его сохранять и возвращать вызывающей процедуре неиз- неизменным. Это выполняется с помощью команды push евх / pop евх. После сохранения регистра евх в него зафужается адрес процедуры subioo. Наконец, команда call евх вызывает процедуру, т. е. сохраняет адрес воз- возврата в стеке и передает управление процедуре. Рассмотрим еще один модифицированный вариант фрагмента кода. Для вы- вызова процедур можно использовать и обычную команду jmp (листинг 3.3). asmsub mov sub lea push jmp ex: ret asmsub sublOO sub ret sublOO proc EAX, EAX, EDX, EDX il i2 ex sublOO endp proc EAX, endp 100 Применение команды jmp основано на том, что в стек предварительно по- помещается адрес следующей за jmp команды вызывающей профаммы. Про- Процедура subioo вычитает из регистра еах значение юо, затем передает управ- управление в вызвавшую ее процедуру asmsub с помощью команды ret. Команда ret "не знает", что в стеке находится адрес следующей за call команды.
Глава 3. Разработка и использование подпрограмм на ассемблере 113 После выполнения ret в программный счетчик помещается адрес метки ех, предварительно сохраненный в стеке с помощью команд lea EDX, ex push EDX В этих примерах мы использовали для передачи параметров и возврата ре- результата регистр еах. Это один из простейших вариантов. В общем случае проблема передачи параметров и возврата результата в ассемблере решается не столь просто, поэтому следует ее рассмотреть более подробно. Передавать фактические параметры процедуре можно по-разному. Про- Простейший способ, как в наших предыдущих примерах, — передавать пара- параметры через регистры: основная программа записывает фактические пара- параметры в регистры, а процедура извлекает их оттуда и использует в своей работе. Аналогично можно поступить и с результатом, если он имеется: процедура записывает свой результат в регистр, а основная программа затем извлекает его оттуда. Какие регистры можно использовать для передачи па- параметров и возврата результата? Программист может выбирать те или иные регистры, исходя из своих соображений, но и здесь есть некоторые правила. Наиболее часто для передачи параметров используются регистры еах, евх, есх, edx, немного реже — евр, esi, edi. Регистр евр обычно используется вместе с регистром указателя стека esp для доступа к параметрам, находя- находящимся в стеке, и об этом мы поговорим далее. Регистры esi и edi удобно использовать для операций с массивами данных в качестве индексных, хотя ничто не мешает применять их по своему усмотрению. Рассмотрим такой пример. Необходимо найти большее из двух целых чисел и вычислить абсолютную величину этого максимума. Разработаем две про- процедуры на ассемблере (назовем их maxint и maxabs) для определения мак- максимума и его абсолютного значения. Процедура maxint принимает два целочисленных параметра (обозначим их il И 12) И МОЖет быть Записана В ВИДе maxint (il, i2). Процедура maxabs принимает в качестве параметра значение целого числа (обозначим его intval) И МОЖет быть объявлена как maxabs (intval). Договоримся первый параметр il передавать в регистре еах. а второй ±2 — в регистре евх. Процедуры будут возвращать результат в регистре еах. Ре- Результат выполнения процедуры maxint является входным параметром для процедуры maxabs. Исходный текст фрагмента программы, в которой при- применяются процедуры, представлен в листинге 3.4. ;основная программа
114 Часть I. Основы эффективного программирования на ассемблере mov EAX, il mov EBX, i2 call maxint /максимум находится в регистре ЕАХ mov intval, EAX call maxabs ;абсолютное значение максимума ;здесь объявлены процедуры ; процедура maxint(il, i2) maxint proc cmp EAX,EBX jge ex mov EAX,EBX ex: ret maxint endp ; процедура maxabs(intval) maxabs proc mov EAX, intval cmp EAX, 0 jge quit neg EAX quit: ret maxabs endp При передаче параметров через регистры необходимо учитывать еще один важный аспект. Регистры, которые процедура пытается использовать, могут
Глава 3. Разработка и использование подпрограмм на ассемблере 115 быть задействованы другими частями программы, поэтому разрушение со- содержимого регистров процедурой может привести к краху приложения. Же- Желательно сохранять содержимое регистров перед входом в процедуру, если нет полной уверенности в том, что информация в них не используется дру- другими процедурами или программой. Регистры обычно сохраняют в стеке. Так, если в предыдущем примере основная программа использует регистр евх, то следует внести соответствующие изменения в исходный текст (листинг 3.5). Дополнительные команды вьщелены жирным текстом. ; основная программа mov EAX, il push ЕВХ mov EBX, i2 call maxint pop EBX ; максимум находится в регистре ЕАХ mov intval, EAX call maxabs ; здесь объявлены процедуры ; процедура maxint(il, i2) maxint proc maxint endp ; процедура maxabs(intval) maxabs proc maxabs endp
116 Часть I. Основы эффективного программирования на ассемблере Часто поступают иначе: разрешают и основной программе, и процедуре пользоваться одними и теми же регистрами, но при этом требуют от про- процедуры, чтобы она сохраняла те значения регистров, которые использовала основная программа. Достичь этого просто: в начале своей работы процеду- процедура должна сохранить в стеке значения тех регистров, которые ей потребуют- потребуются для работы, после чего она может использовать эти регистры как угодно, а перед выходом она должна восстановить прежние значения этих регист- регистров, считав их из стека. Такое сохранение регистров настоятельно рекомендуется делать в любой процедуре, даже если явно видно, что основная программа не пользуется теми же регистрами, что и процедура. Дело в том, что исходный текст про- программы в дальнейшем может измениться (а это происходит очень часто), и может оказаться так, что после этих изменений основной программе потре- потребуются эти регистры. Поэтому лучше сразу предусмотреть в процедуре сохранение регистров, вос- воспользовавшись двумя специальными командами — pusha и рора, которые позволяют сохранять в стеке и восстанавливать из стека значения регистров общего назначения. Отметим, что сохранять значение регистра, через который процедура воз- возвращает результат, не нужно, поскольку в изменении этого регистра и за- заключается цель работы процедуры. Передавать параметры через регистры удобно, и используется этот метод очень часто. Он эффективен, когда параметров немного; если же параметров много, то на них попросту не хватит регистров. В таком случае используют другой способ передачи параметров — через стек: основная программа запи- записывает фактические параметры (их значения или адреса) в стек, а процедура затем их оттуда извлекает. Пусть некоторая процедура (назовем ее myproc) имеет п параметров и объ- объявляется как myproc (xi,x2,... ,хп). Будем считать, что перед обращением к процедуре основная программа записывает параметры в стек в определен- определенном порядке. Выбор расположения параметров в стеке невелик и ограничи- ограничивается двумя вариантами. В первом варианте параметры записываются в стек слева направо: сначала записывается 1-й параметр, затем 2-й и т. д. Для 32-разрядных приложений каждый параметр имеет размер двойного слова, тогда команды основной программы, реализующие обращение к процедуре, будут следующими: push xl push x2 push xn call myproc
Глава 3. Разработка и использование подпрограмм на ассемблере 1J7_ По второму варианту параметры записываются в стек справа налево: сначала записывается n-й параметр, затем (п - n-й и т. д. В этом случае перед вы- вызовом процедуры необходимо выполнить команды: push xn push xl call myproc Каким образом процедура получает доступ к параметрам? Общепринятым считается способ, при котором доступ к параметрам осуществляется с ис- использованием регистра евр. Для этого необходимо поместить в него адрес вершины стека (содержимое регистра esp), а затем использовать выражения вида [Евр+i] для доступа к параметрам процедуры. Желательно сохранить регистр евр, поскольку он может использоваться в основной программе. Поэтому вначале сохраняем прежнее значение этого регистра и только затем пересылаем в него значение регистра esp. Проиллюстрируем это на примере. Модифицируем предыдущий пример так, чтобы для передачи параметров использовался стек. Предположим, что па- параметры в процедуру maxint передаются справа налево, т. е. первым поме- помещается в стек переменная i2, затем и. Фрагменты исходного текста основ- основной программы и процедур на ассемблере с передачей параметров через стек показаны в листинге 3.6. ; основная программа push i2 push il call maxint ; максимум находится в регистре ЕАХ mov intval, EAX push intval call maxabs ; абсолютное значение максимума
118 Часть L Основы эффективного программирования на ассемблере ; здесь объявлены процедуры ; процедура maxint(il, i2) maxint proc push EBP mov EBP, ESP ; загружаем параметр Ив регистр ЕАХ mov ЕАХ, DWORD PTR [EBP+8] ; сохраняем регистр ЕВХ push ЕВХ ; загружаем параметр i2 в регистр ЕВХ mov ЕВХ, DWORD PTR [EBP+12] ex: cmp jge mov pop ret maxint EAX,EBX ex EAX,EBX EBP 8 endp ; процедура maxabs(intval) maxabs proc push EBP mov EBP, ESP ; загрузить параметр в регистр ЕАХ push EBP mov EBP, ESP mov EAX, DWORD PTR [EBP+8] ; intval
Глава 3. Разработка и использование подпрограмм на ассемблере 119 сшр jge neg quit: pop ret maxabs EAX, 0 quit EAX EBP 4 endp После завершения процедура обязана выполнить некоторые действия. Рас- Рассмотрим их. Необходимо отметить, что к моменту выхода из процедуры стек должен быть в том же состоянии, в каком он был до вызова процедуры. К концу работы процедуры в вершине стека будет находиться старое значе- значение регистра ев?. Считываем его и восстанавливаем евр с помощью коман- команды pop евр. Теперь в вершине стека окажется адрес возврата. Казалось бы, можно выйти из процедуры по команде ret, однако это не так — нужно еще очистить стек от параметров, которые уже не нужны. Очистить стек может как вызывающая программа, так и процедура. Конечно, это может сделать основная программа, для чего в ней после команды call my sub нужно вы- выполнить команду add sp, n, где п — количество байт для очистки. Однако лучше, если очистку стека будет делать сама процедура. Дело в том, что обращений к процедуре много, поэтому в основной программе команду add придется выписывать многократно, а процедура — одна, поэтому в ней такую команду надо выписать только раз. Это полезное правило для опти- оптимизации программ: если какое-то действие может быть выполнено и в ос- основной программе, и в процедуре, то лучше, если это будет сделано в про- процедуре. В этом случае требуется меньше команд. Итак, процедура должна сначала очистить стек от параметров и только затем передать управление по адресу возврата. Чтобы упростить реализацию этой пары действий, в систему команд введен расширенный вариант команды ret — с непосредственным операндом, который трактуется как число без знака: ret n По этой команде из стека сначала извлекается адрес возврата, затем стек очищается на указанное операндом п число байтов, и далее выполняется переход по адресу возврата. Сделаем несколько замечаний. Во-первых, команда ret — это на самом деле команда ret :. т. е. возврат без очистки стека. Во-вторых, операнд команды указывает, на сколько байт надо очищать стек. В-третьих, в операнде не
120 Часть I. Основы эффективного программирования на ассемблере должен учитываться адрес возврата — команда ret считывает его до очистки стека. После такого возврата из процедуры стек будет в таком же состоянии, как и до ее вызова, т. е. до выполнения команд записи параметров в стек. Тем са- самым в стеке уничтожаются все следы обращения к процедуре, а это как раз то, что нужно. Такова общая схема передачи параметров через стек. Еще раз напомним, что этот способ передачи параметров универсален, его можно использовать при любом числе параметров. Однако этот способ более сложный, чем пе- передача параметров через регистры, поэтому желательно передавать парамет- параметры через регистры, так будет проще и короче. Что же касается результата процедуры, то он крайне редко передается через стек и обычно передается через регистр. Общепринято, что результат выполнения процедуры возвра- возвращается в регистре еах. Во многих процедурах не возникает проблемы с хранением локальных дан- данных (величин, нужных только на время выполнения процедуры) — для них достаточно и регистров. Однако если в процедуре много локальных данных, тогда возникает вопрос: где отводить для них место? Можно выделить место в сегменте данных, кода или в стеке самой процедуры. Макроассемблер MASM позволяет использовать все варианты. Для хране- хранения данных в сегменте данных необходимо его проинициализировать. Это делается с помощью директивы .data. Тип данных можно указать как байт (db), слово (dw), двойное слово (dd). Если в сегменте данных представлена последовательность элементов, то ее размер можно определить с помощью оператора $. Приведу пример использования сегмента данных для обработки строки символов. Пусть вызывающая процедура или программа должна получить 7-й элемент из строки, находящейся в вызываемой процедуре. В качестве параметра вызываемой процедуре передается позиция элемента в строке (в данном случае 6, поскольку первый элемент имеет индекс 0). Разработанная на макроассемблере MASM процедура (назовем ее findchar) возвращает в регистре еах символ или 0, если заданный номер позиции превышает раз- размер строки. Исходный текст процедуры представлен в листинге 3.7. .data si DB 'STRING1!!!' Is EQU $-sl .code
Глава 3. Разработка и использование подпрограмм на ассемблере 121 findchar push mov mov cmp jbe mov jmp next: lea add xor mov ex: pop ret findchar end proc EBP EBP, EDX, EDX, next EAX, ex ESI, ESI, EAX, AL, EBP endp ESP DWORD PTR [EBP+8] Is 0 si EDX EAX 3YTE PTR [ESI] Номер позиции элемента передается в стеке по адресу [евр+8]. Он сохраня- сохраняется в регистре еох и сравнивается с размером строки lsi. Если размер строки меньше запрашиваемой позиции, выходим из процедуры, присвоив еах значение 0. Если мы попадаем в диапазон размера строки, тогда в регистр al помещает- помещается нужный элемент. Адрес этого элемента находится как сумма начального смещения и номера позиции с помощью команд lea ESI, si add ESI, EDX В нашем случае стек должна очистить вызывающая программа или процедура. Для работы с локальными данными можно использовать и сегмент кода процедуры. Часто это бывает очень удобно, поскольку не требуется инициализация сег- сегмента данных и увеличивается быстродействие программы. Очень легко мо- модифицировать исходный текст из предыдущего примера для работы с ло- локальными данными непосредственно в сегменте кода (листинг 3.8). .code
122 Часть /. Основы эффективного программирования на ассемблере findchar proc jmp si lsl strt: push mov mov cmp jbe mov jmp next: lea add xor mov ex: pop ret strt DB EQU EBP EBP, EDX, EDX, next EAX, ex ESI, ESI, EAX, AL, EBP 1STRING1!!!!' $-sl ESP DWORD PTR [EBP+8] lsl 0 Si EDX EAX BYTE PTR [ESI] findchar endp end Здесь применен несложный трюк с использованием команды jmp strt для перехода на основную ветвь процедуры. Локальные данные помещаются в память в сегменте кода. Выделение места в стеке можно сделать обычным способом. Для этого нужно запомнить в стеке текущее значение регистра евр и затем установить его в вершину стека, после чего уменьшить значение указателя стека esp на число требуемых байтов. Например, если процедуре my sub тре- требуется три двойных слова, тогда последовательность действий определяется командами: mysub proc push EBP mov EBP, ESP sub ESP, 12 mysub endp
Глава 3. Разработка и использование подпрограмм на ассемблере 123 После этого доступ к локальным данным осуществляется с помощью выра- выражений вида [EBP-i], где i— позиция элемента. При завершении работы процедуры нужно выполнить такие действия: mov ESP, EBP pop EBP ret Резервирование памяти в области стека используется в языках высокого уровня при вызове процедур (функций). Для ассемблерных процедур ис- использование подобной техники дает меньший эффект чем, например, ис- использование сегмента данных. В большинстве случаев, особенно при разработке больших программ, воз- возникает ситуация, когда требуется обрабатывать одни и те же объемы данных несколькими процедурами или даже отдельными программами. Очень удоб- удобно было бы сделать эти данные доступными для нескольких процедур. Мо- Может возникнуть вопрос: зачем применять для взаимодействия процедур ка- какие-то специфические приемы, когда данные можно передавать от одной процедуры другой через использование параметров. Только что рассмотрен- рассмотренные примеры как раз и продемонстрировали такую методику. Однако в слу- случае использования параметров как средства взаимодействия между процеду- процедурами возникают определенные проблемы. Вот только некоторые из них: ? при относительно большом количестве процедур обработки одних и тех же данных производительность работы процедуры снижается. Предполо- Предположим, в процедуре имеется массив чисел, который обрабатывается не- несколькими процедурами. Каждый раз при обращении к процедуре другие программы вынуждены вычислять каким-то образом адреса элементов массива. Если используется стек, а в подавляющем большинстве случаев так оно и есть, возникает необходимость в обращении к стеку для полу- получения местоположения элементов массива в памяти. При относительно редком обращении к процедуре таких проблем может и не возникнуть, но с усложнением структуры программы и алгоритмов обработки быст- быстродействие приложения может упасть; ? при обработке одних и тех же данных разными процедурами структури- структурируемое^ программы, использующей ассемблерные процедуры, ухудшается. Применение общих или, как их еще называют, глобальных переменных по- позволяет работать с ними при минимальном использовании стека, что эко- экономит процессорное время. К тому же фиксированные привязки обших пе- переменных на этапе компоновки ускоряют доступ к ним.
124 Часть I. Основы эффективного программирования на ассемблере В дальнейшем мы будем употреблять термины "общая переменная" и "гло- "глобальная переменная" как синонимы. Для работы с общими переменными в языке ассемблера используются директивы public и extern. Директива public объявляет переменную или процедуру доступной для других моду- модулей, директива extern указывает на то, что используемая переменная или процедура является внешней по отношению к выполняемой процедуре. Обе директивы применяются для компоновки основной программы или про- процедуры из нескольких объектных модулей и очень удобны для построения больших программ. Глобальные переменные объявляются так: ? в объектном модуле, где находится такая переменная, необходимо указать на возможность доступа к ней с помощью директивы public; П в объектных модулях, из которых происходит обращение к общей пере- переменной, необходимо объявить ее с директивой extern. Пример, приведенный далее, демонстрирует технику применения общих данных для работы двух процедур. Первая (назовем ее sub2) выполняет вы- вычитание трех целых чисел. Первые два числа являются входными парамет- параметрами для процедуры, а третья переменная (назовем ее add2res) является внешней по отношению к процедуре sub2 и находится в другом объектном модуле. Исходный текст процедуры sub2 приведен в листинге 3.9. extern add2res: DWORD .code sub2 push mov mov sub sub pop ret sub2 end proc EBP EBP, EAX, EAX, EAX, EBP endp ESP DWORD PTR [EBP+8] DWORD PTR [EBP+12] add2res Хочу обратить внимание на строку extern add2res: DWORD
Глава 3. Разработка и использование подпрограмм на ассемблере 125 Директива extern объявляет переменную add2res как внешнюю размером в двойное слово, что соответствует целочисленной переменной. Результат выполнения процедуры помещается, как обычно, в регистр еах. Но откуда мы будем брать переменную add2res? Значение этой переменной является результатом выполнения второй процедуры а2, вычисляющей сум- сумму двух целых чисел, являющихся для нее входными параметрами. Пере- Переменная add2res определена как двойное слово в сегменте данных и содер- содержит сумму двух целых. Поскольку add2res должна быть доступна для процедуры sub2 из другого объектного модуля, она должна быть объявлена с директивой public. Исходный текст процедуры а2 представлен в листин- листинге 3.10. public • data add2res .code a2 push mov mov add mov pop ret a2 end add^res DD 0 proc EBP EBP, ESP EAX, DWORD PTR [EBP+8] EAX, DWORD PTR [EBP+12] add!res, EAX EBP endp Для правильной работы основная программа должна вначале вызвать про- процедуру а2 с соответствующими параметрами, а потом — процедуру sub2. Приведенный пример является простейшим и демонстрирует ключевые ас- аспекты использования общих переменных. Если нужно передать в процедуру строку или массив чисел с использованием общих переменных, задача не- несколько усложняется. Рассмотрим следующий пример. Пусть в основной программе необходимо отобразить определенный символ строки на экране. Разработаем две про- процедуры, взаимодействующие с основной программой. Первая процедура (на-
126 Часть I. Основы эффективного программирования на ассемблере зовем ее rets) содержит строку символов с завершающим нулем, и единст- единственное ее назначение — передать каким-то образом строку другой процеду- процедуре (назовем ее fchar). Процедура fchar использует эту строку для поиска элемента с порядковым номером, который передается ей в качестве пара- параметра из основной программы. Это было краткое описание наших процедур. Теперь более подробно. Вначале рассмотрим процедуру rets. Исходный текст представлен в листинге 3.11. public .data si asi .code rets lea mov ret rets end asi DB ' DB 0 DD 0 proc EAX, asi, endp STRING TO SEND1 si EAX Строка si определяется как строка с завершающим нулем. Как известно, строку можно передать ее адресом. Поскольку адрес является 32-разрядным, то удобно сохранить его в переменной asi размером в двойное слово с по- помощью команд lea EAX, si mov asi, EAX Кроме того, необходимо объявить переменную asi директивой public. Ис- Исходный текст процедуры fchar представлен в листинге 3.12. extern asi:DWORD public fchar .code
Глава 3. Разработка и использование подпрограмм на ассемблере 127 fchar push mov mov mov add xor mov pop ret fchar end proc EBP EBP, EDX, ESI, ESI, EAX, EBP endp ESP DWORD PTR [EBP+8] DWORD PTR asl EDX EAX BYTE PTR [ESI] Для доступа к строке в процедуре fchar необходимо объявить переменную, содержащую адрес строки, директивой extern: extern asl:DWORD Далее выполняется поиск элемента с указанным порядковым номером, и результат возвращается, как обычно, в регистре еах. В языке ассемблера помимо общих переменных можно использовать и об- общие (глобальные) для нескольких модулей процедуры. Как и в случае с об- общими переменными, объявление таких процедур выполняется так: ? в объектном модуле, где находится общая процедура, необходимо указать на возможность доступа к ней с помощью директивы public; ? в объектных модулях, из которых происходит обращение к общей проце- процедуре, необходимо объявить ее с директивой extern. Рассмотрим пример. Пусть основная программа выводит на экран абсолют- абсолютное значение разности двух целых чисел. Вычисление разности двух целых выполняется с помощью процедуры subcom, а вычисление абсолютного значения этой разности — с помощью процедуры abs. Процедура subcom используется основной программой и объявлена как public. Процедура abs вызывается внутри subcom и объявлена как extern. Исходный текст про- процедуры subcom представлен в листинге 3.13. .model flat, С public subcom
128 Часть I. Основы эффективного программирования на ассемблере extern abs:proc .code subcom proc push EBP mov EBP, ESP mov EAX, DWORD PTR [EBP+8] sub EAX, DWORD PTR [EBP+12] push EAX call abs add ESP, 4 pop EBP ret subcom endp end Исходный текст процедуры subcom несложен. Хочется обратить внимание на строки push EAX call abs add ESP, 4 Разность двух чисел передается процедуре abs через стек с использованием регистра еах. Очистка стека после выполнения процедуры выполняется вы- вызывающей процедурой, в данном случае subcom. Исходный текст процедуры abs представлен в листинге 3.14. П и с Т и н г 3.14. П р С ц i.; /.; у | .к i .. i::..: j public .code abs abs proc push mov mov cmp jge neg EBP EBP, EAX, EAX, ESP DWORD PTR [EBP+8] 0 no_change .EAX
Глава 3. Разработка и использование подпрограмм на ассемблере 129 no_change: pop EBP ret abs endp end Процедура abs объявлена как public, что позволяет использовать ее в дру- других модулях. На этом рассмотрение применения процедур, написанных языке ассемблера MASM 6.14, можно закончить. Как видно из приведенных примеров, ас- ассемблер обладает весьма широкими возможностями в плане разработки от- отдельных модулей, которые мы будем использовать в материалах последую- последующих глав.
Глава 4 Оптимизация логических структур C++ с помощью ассемблера Программисты, пишущие на языках высокого уровня, применяют ассемб- ассемблерные вставки и целые, отдельно скомпилированные модули для улучше- улучшения размеров и быстродействия своих приложений. Рассмотрим наиболее часто используемые конструкции языков высокого уровня в ассемблерной интерпретации. Такие конструкции, как if ... else, do ... while и дру- другие, обладают определенной избыточностью, поэтому применение их ас- ассемблерных аналогов позволяет добиться повышения производительности приложения. Следует сказать, что все языки высокого уровня имеют, как правило, встроенный ассемблер, однако более подробное его рассмотрение отложим до части III. Анализ программ, написанных на языках высокого уровня, позволяет найти их слабые места, прежде всего, в нерациональном использовании инструк- инструкций выбора и циклических вычислений. Значительно снижают производи- производительность программ обработка больших массивов данных, строк и матема- математические вычисления. Ни один компилятор, ни в одном языке высокого уровня, как бы он ни оптимизировал программу по быстродействию или по размеру исполняемого модуля, не в состоянии устранить избыточность и неоптимальность кода. Это касается даже компилятора фирмы Intel с не- неплохими, на мой взгляд, характеристиками оптимизации на уровне команд процессора. Наиболее легко поддаются оптимизации на ассемблере циклические вычис- вычисления, такие КОНСТРУКЦИИ, как if ... else, while, do ... while, switch ... case. Как правило, оптимизация инструкций выбора и циклов основана на использовании команд сравнения и условных переходов в зави- зависимости от результата сравнения. В общем виде это можно представить так: cmp operandl, operand2 Jcond labell
Глава 4. Оптимизация логических структур C++ с помощью ассемблера 131_ «операторы 1> jmp Iabel2 labell: <операторы 2> Iabel2: Здесь operandi и operand2 — переменные и/или выражения, a jcond — оператор условного перехода (je, ji, jge, jz или другой). Любые, скаль угодно сложные конструкции языка высокого уровня можно представить в виде комбинаций операторов условных переходов и операто- операторов сравнения, причем несколькими способами. Далее мы рассмотрим ва- варианты реализации конструкций языка высокого уровня в контексте прак- практического применения. Начнем с инструкции выбора if. 4.1. Инструкция if Одиночная инструкция if предназначена для выполнения команды или блока команд в зависимости от того, истинно или нет заданное условие. В общем виде инструкция выглядит так: if (условие) операторы В C++ такая инструкция может быть представлена следующим образом: if (условие) { <операторы> } Более сложный вариант инструкции, if ... else, позволяет выборочно выполнить одно из двух действий в зависимости от условия. Далее показан синтаксис данной инструкции в языке C++: if (условие) { <операторы 1> } else { <операторы 2>
132 Часть I. Основы эффективного программирования на ассемблере В языке ассемблера нет таких конструкций, однако они довольно легко реа- реализуются с помощью определенных последовательностей команд. Рассмотрим, например, алгоритм проверки двух операндов на равенство и, в зависимости от результата, переход на ту или иную ветвь программы. На Visual C++ .NET выражение будет выглядеть так: if (operandl = operand2) else Язык ассемблера позволяет представить конструкцию if ... else довольно просто: cmp operandl, operand2 je YES <команды 1> jmp EXIT YES: <команды 2> EXIT: Возможен и другой вариант: cmp operandl, operand2 jne NO <команды 2> EXIT: NO: <команды 1> jmp EXIT Разработаем фрагмент программного кода, в котором анализируется условие сравнения двух переменных целочисленного типа х и y. В зависимости от результата сравнения переменная х принимает значение у в случае, если х больше y, и остается неизменной, если х меньше или равно у.
Глава 4. Оптимизация логических структур C++ с помощью ассемблера 133_ В C++ фрагмент кода выглядит так: if (X > Y) X = Y; А вот реализация этого фрагмента на ассемблере: mov cmp jge mov EXIT: EAX, EAX, EXIT DWORD DWORD DWORD PTR X PTR Y PTR X , EAX Поскольку мы работаем с 32-разрядными операндами, все переменные и регистры описаны соответствующим образом. Команда сравнения cmp не выполняется для операндов, одновременно находящихся в оперативной па- памяти, поэтому- один из операндов (в данном случае y) помещается в регистр еах. Результат вычислений помещается в переменную х. В следующем фрагменте кода вычисляется сумма двух целых чисел х и y при условии, что оба они находятся в диапазоне от 1 до 100. В Visual С-*~*- этот фрагмент выглядит так: if ((X <= 100 && X >= 1) && (Y <= 100 && Y >= 1)) X = X - Y Программный код на ассемблере представлен в листинге 4.1. cmp jge jmp DWORD PTR X, check_xl00 EXIT check xlOO: cmp jle jmp check_yl: crop jge jmp DWORD PTR X, check yl EXIT DWORD PTR Y, check_yl00 EXIT check_yl00: cmp DWORD PTR Y, 1 100 1 100
134 Часть I. Основы эффективного программирования на ассемблере jg EXIT mov EAX, DWORD PTR Y add DWORD PTR X, EAX EXIT: Как видно из алгоритма, для получения суммы чисел х и y необходимо про- проанализировать как минимум четыре условия: ? х больше или равно i; О х меньше или равно юо; О y больше или равно i; П y меньше или равно юо. Только при одновременном выполнении всех четырех условий переменной х может быть присвоено значение суммы х + y. Для решения такой задачи необходимо представить условие (X <= 100 && X >= 1) && (Y <= 100 && Y >= 1) в виде более простых конструкций. Это выражение распадается на четыре: X <= 100, X >= 1, Y <= 100, Y >= 1 Задача упростилась. Каждое из полученных четырех условий легко проверя- проверяется с помощью команды стр. Например, проверка х <= юо и последую- последующий переход выполняются так: cmp DWORD PTR X, 100 jle check_yl Остальные проверки можно выполнить с помощью аналогичных комбина- комбинаций команд. Надо заметить, что ассемблерный аналог конструкций на языках высокого уровня может быть достаточно завуалирован и с первого взгляда неочеви- неочевиден, что видно из следующего примера. Разработаем программный код для вычисления модуля (абсолютного значе- значения) целого числа х. Один из вариантов решения такой задачи — использо- использование КОНСТРУКЦИИ if ... else. Вариант использования конструкции if ... else на Visual C++: if (X >= 0) AbsX = X else AbsX = -X где AbsX — переменная, в которой хранится модуль х.
Глава 4. Оптюмпация логических структур C++ с помощью ассемблера 135_ Ассемблерная реализация такой конструкции представлена далее: cmp DWORD PTR X, О D1 jmp NOT_X: neg EXIT: mov mov NOT_X EXIT DWORD EAX, DWORD PTR X DWORD PTR X PTR AbsX, EAX И в ветви if. и в ветви else выполняется оператор присваивания. По смыс- смыслу можно объединить эти два присваивания, поместив их в конце фра'гмента кода: mov EAX. DWORD PTR X mov DWORD PTR AbsX, EAX Ветвь else представлена на ассемблере командой: NOT_X: neg DWORD PTR X Результат сохраняется в переменной AbsX. Решим простую задачу — определим большее из двух целочисленных значе- значений и присвоим значение максимума третьей переменной. Консольное при- приложение в C++ .NET имеет всего несколько строк программного кода и его исходный текст приведен в листинге 4.2. ; Лнс"---- ¦¦- 2 Определение б о.ль л: его из дяух чисел и вывод // IF_ELSE_SETCC.cpp : Defines the entry point // for the console application. #include "stdafx.h" int ._tmain(int argc, JTCHAR* argv[]) { int il, i2, ires; while (true) { printfCW'); printf("Enter first number (il): ");
136 Часть I. Основы эффективного программирования на ассемблере scanf("%d", &il); printf("Enter second number (i2): "); scanf<"%d", &12); if (il >= i2)ires = il; else ires = i2; printf("Maximum = %d\n", ires); } return 0; Как видно из листинга, операцию сравнения выполняют операторы if (il >= i2)ires = il; else ires = i2; Попробуем оптимизировать фрагмент кода с оператором if ... else. Удобно воспользоваться для этого встроенным ассемблером Visual C++ .NET. Ассемблерный аналог оператора условия имеет вид: _asm { mov EAX, il mov EBX, i2 cmp EAX, EBX jge set_ires xchg EAX, EBX set_ires: mov ires, EAX } Приведенный фрагмент кода несложен и не нуждается в дополнительном анализе. Еще более эффективный код можно создать, если избавиться от ветвлений и переходов в программе, или, по крайней мере, минимизировать их количество. Процессоры Pentium IT и выше включают несколько команд, позволяющих эффективно управлять ветвлениями программы. К таким командам относятся setcc (установка по условию — set conditionally) и ко- команды cmov и fcmov. Комбинируя эти команды, можно добиться ощутимых результатов в повышении производительности приложения. Ассемблерный аналог оператора if ... else с использованием команд setge И cmovl ВЫГЛЯДИТ ТЭК: xor EBX, EBX mov EAX, il
Глава 4. Оптимизация логических структур C++ с помощью ассемблера 137 mov EDX, i2 cmp EAX, EDX setge BL mov ire3, EAX cmp BL, 1 onovl EAX, EDX mov ires, EAX } Вначале обнуляем регистр евх — он будет использоваться в качестве инди- индикатора "больше или меньше". В регистр еах помещается первое число (и), а в edx — второе (i2). Если содержимое еах больше или равно содержимому edx, то команда setge bl помещает в младшую часть регистра евх единицу, иначе в евх остается 0. Если bl = о, содержимое edx помещается в регистр еах. Перед последней командой в регистре еах находится максимум, кото- который и помещается в переменную ires. Как видно из этого фрагмента кода, ветвлений и переходов нет. Перед применением команды cmov необходимо проверить, поддерживается ли она данным типом процессора. Проверку МОЖНО ВЫПОЛНИТЬ С ПОМОЩЬЮ КОМаНДЫ cpuid. Полный текст консольного приложения приводится в листинге 4.3. ] Л и с т i -¦ и;' 4. 'Л. М о / \ и ф и ¦. \ а \ -. о в з н н ь: и в ?-. р и г> i\ т п pcv с avnt ь i с о п\:\:-, / i s о >у <:., w // IF_ELSE_SETCC.cpp : Defines the entry point // for the console application. #include "stdafx.h" int ._traain(int argc, JTCHAR* argv[]) { int il, i2, ires; while (true) { printf("\n"); printf("Enter first number (il): "); scanf("%d", sil); printf("Enter second number (i2) : "); scanf("%d", &i2); _asm { xor ЕВХ, ЕВХ
138 Часть I. Основы эффективного программирования на ассемблере mov EAX, il mov EDX, i2 cmp EAX, EDX setge BL mov ires, EAX cmp BL, 1 cmovl EAX, EDX mov ires, EAX printf("Maximum = %d\n", ires); return 0; Окно приложения показано на рис. 4.1. Рис. 4.1. Окно приложения, демонстрирующего применение оптимизированного кода для нахождения максимума двух целых чисел 4.2. Цикл while Этот цикл используется в тех случаях, когда число повторений цикла зара- заранее неизвестно. Цикл while — это цикл с предварительным условием, и его выполнение или невыполнение зависит от начальных условий. Синтаксис этого выражения можно представить в общем виде следующим образом: while (условие) <операторы цикла>
Глава 4. Оптимизация логических структур C++ с помощью ассемблера 139_ Выход из цикла осуществляется, если условие окажется ложным. Так как истинность условия определяется в начале каждой итерации, вполне может оказаться, что тело цикла не выполнится ни разу. В C++ цикл while имеет вид: while (условие) { ^операторы цикла> } В следующем фрагменте программного кода продемонстрируем применение цикла while. Пусть имеется массив из 10 целых чисел. Требуется найти ко- количество элементов массива, предшествующих первому встретившемуся ну- нулевому элементу (если таковой будет найден). Фрагмент программы должен вернуть число элементов, предшествующих первому нулевому, или о, если нулевой элемент не найден. Такой фрагмент кода можно применить для по- поиска и выделения строк с завершающим нулем. Подобная задача легко ре- решается при помощи цикла for, но мы решим ее, используя цикл while. Будем использовать следующие переменые и обозначения: О XI — массив целых чисел; ? ixi — текущий индекс массива; ? sxi — размер массива; О counter — счетчик элементов. Этот фрагмент кода выполняется следующим образом: ? после инициализации переменных программа в начале каждой итерации цикла while проверяет неравенство элемента массива xi нулю. Если эле- элемент равен о, происходит немедленный выход из цикла; О если условие верно, т. е. элемент массива не равен о, выполняется тело цикла. При этом инкрементируются счетчик counter и индекс массива ixi. Если обнаружен последний элемент массива, происходит выход из цикла (инструкция if); О в любом случае счетчик counter содержит количество элементов, пред- предшествующих первому нулевому, или о, если нулевой элемент вообще не обнаружен. Программный код в Visual C++ для этой задачи представлен в листинге 4.4. ; Листинг 4.4. Фрагмент кода с использованием циклз int Х1[10] = {12, 90, -6, 30, 22, 10, 22, 89, -0, 47};
140 Часть I. Основы эффективного программирования на ассемблере int Counter = 0; int 1X1 = 0; int SX1 = sizeof (Xl) / 4; while (X1[IX1] != 0) { Counter++; if AX1 — SX1) break; IX1++; }; if (Counter == SX1+1) Counter = 0; Вариант решения задачи на ассемблере (листинг 4.5) выглядит на первый взгляд более сложным, чем в предыдущих примерах. .686 .model flat, stdcall .data XI SX1 DD 2, -23, 5, 9, -1, 0, 9, 3 DD $-Xl Counter DD 0 .code start: push mov mov mov shr mov AGAIN: mov cmp je inc dec jz EBX ECX, 0 EBX, offset XI EDX, DWORD PTR SX1 EDX, 2 ESI, EDX EAX, DWORD PTR [EBX] EAX, 0 RUNOUT ECX EDX RUNOUT
Глава 4. Оптимизация логических структур C++ с помощью асдемблера 141 add jmp RUNOUT: cmp jne xor SET_CNT: mov pop end start EBX, 4 AGAIN ECX, ESI SET CNT ECX, ECX DWORD PTR Counter, ECX EBX Необходимо сделать несколько важных замечаний. Первое касается исполь- использования регистров. При работе с внешними программами и модулями на языках высокого уровня желательно сохранить регистры евх, евр, esi и edi в стеке. Что касается остальных регистров (еах, есх и edx), to можно ис- использовать их по своему усмотрению. Второе замечание касается работы с массивами данных и строками на ас- ассемблере. В операционной системе Windows для доступа к таким данным всегда используются 32-разрядные переменные, которые хранят адреса мас- массивов или строк. Для доступа к элементам массива xi можно использовать регистр евх, поместив в него адрес первого элемента массива: mov EBX, offset XI Для работы с массивом необходимо знать его размер, который мы сохраним в регистре edx: mov EDX, DWORD PTR SX1 Счетчик ненулевых элементов сохраним в регистре есх. Поскольку каждый элемент массива занимает в памяти 4 байта (двойное слово), то для доступа к последующему элементу мы используем команду add EBX, 4 В примере присутствуют две структуры высокого уровня — цикл while и оператор условия if. Цикл while реализован с помощью трех операторов: mov ЕАХ, DWORD PTR [EBX] Otip ЕАХ, О je RUNOUT, а условие if — операторами: cmp ECX, EDX je RUNOUT
142 Часть I. Основы эффективного программирования на ассемблере Если нулевой элемент вообще не найден, то в счетчик ненулевых элементов по условию задачи записываем о: cmp ECX, EDX jne SET_CNT xor ECX, ECX Столь подробный анализ ассемблерного варианта программы сделан для того, чтобы читатель понял, что однозначного решения оптимизации логи- логических структур в языках высокого уровня не существует! "Строительным" кирпичиком такой оптимизации во многих случаях является пара команд ассемблера: cmp operandl, operand2 Jcond label В принципе, на ассемблере можно реализовать сколь угодно сложные логи- логические выражения и ветвления. Все ограничивается только фантазией и опытом разработчика. Цикл while можно реализовать на ассемблере, используя команды строко- строковых примитивов. Эти команды широко используются при циклической об- обработке массивов и строк и зачастую упрощают алгоритм задачи. Вариант цикла while с использованием команды scasd представлен в листинге 4.6. .686 . model .data XI SX1 1X1 flat, DD DD DD Counter DD .code start: mov xor mov shr eld xor next: scasd EDI, ECX, EDX, EDX, EAX, stdcall 2, -23, 5, 9, -1, 0, 9, 3 $-Xl 1 0 offset XI ECX DWORD PTR SX1 2 EAX
Глава 4. Оптимизация логических структур C++ с помощью ассемблера 143 je ex inc ECX dec EDX jz ex jmp next ex: crap ECX, 10 jne write_cnt mov Counter, 0 jmp quit write_cnt: mov Counter, ECX quit: end start 4.3. Цикл do... while Операторы цикла do ... while организуют выполнение цикла, состоящего из любого числа операторов с заранее неизвестным числом повторений. Те- Тело цикла в любом случае будет выполнено хотя бы один раз. Выход из цик- цикла происходит, когда становится истинным некоторое логическое условие. В языке C++ цикл do ... while имеет вид: do <операторы> > while <условие> Напишем программный код для вычисления суммы первых четырех элемен- элементов массива целых чисел. Пусть размерность массива равна 7. Для решения задачи используются переменные и обозначения: ? XI — массив целых чисел, состоящий из семи элементов; О ixi — индекс текущего элемента массива; ? sumxi — текущее значение суммы. Фрагмент программного кода на C++ довольно прост и представлен в лис- листинге 4.7.
144 Часть I. Основы эффективного программирования на ассемблере iiir.:r;!rii -';.?. О Г.-. Ц ».' С •: ' к;ПШ i;;j ¦'( -. >} М'ЛОГ.О'-.* iKi к СПИТСЯ -Г;уУМа nCj/HS>:X .; -:t'T: .if:-:; *: нк^^;:м : ;.ч ь::;;;г-;Ь;,:1 int Х1[7] = {2, -4, 5, 1, -1, 9, 3}; int 1X1 = 0; int sumXl = 0; do { sumXl = sumXl + X1[IX1]; IX1++; } while AX1 <= 3); Ассемблерный вариант цикла do ... while представлен в листинге 4.8. .686 .model .data XI SX1 1X1 CNT SUMX1 .code start push mov mov mov shr cmp jl NEXT: add flat DD 2, -23, 5, 9, -1, 9, 3 DD $-Xl DD 1 EQU 3 DD 0 EBX EBX, EAX, EDX, EDX, EDX, EXIT EAX, offset XI 0 DWORD PTR SX1 2 CNT [EBX]
Глава 4. (Утилизация логических структур C++ с помощью ассемблера 145 стр ЗЯ inc add jmp EXIT: mov pop end start DWORD EXIT DWORD EBX, 4 MEXT DWORD EBX PTR PTR PTR 1X1, CNT 1X1 SUMX1, EAX Вначале инициализируем все необходимые переменные. Для доступа к эле- элементам массива помещаем его адрес в регистр евх: mov EBX, offset XI Начальное значение суммы, равное о, помещаем в регистр еах: mov EAX, О Условие ассемблерного цикла do ... while проверяется командой: cmp DWORD PTR 1X1, CNT где ixi — текущий индекс массива. Поскольку целочисленное значение элемента массива занимает в памяти двойное слово, то, как и в предыдущем примере, для доступа к последую- последующему элементу мы должны увеличивать значение адреса на 4: add EBX, 4 Результат помещается в переменную sumxi для дальнейшего использования. 4.4. Цикл for Оператор цикла for организует выполнение оператора или группы операто- операторов определенное число раз. В обшем виде цикл можно представить так: for выражение-инициализатор; условие; выражение-модификатор) <операторы> Дойдя до цикла, программа сразу выполняет выражение-инициализатор, которое устанавливает начальное значение счетчика цикла. Затем анализи- анализируется условие, которое называется еще условием прекращения цикла. Пока оно истинно, цикл будет выполняться. Каждый раз после прохождения тела
146 Часть I. Основы эффективного программирования на ассемблере цикла выражение-модификатор изменяет счетчик цикла. Если проверка ус- условного выражения дает false, to происходит выход из цикла, и выполня- выполняются операторы, непосредственно следующие за for. В Visual C++ цикл for имеет единое представление независимо от направ- направления изменения модификатора: for (инициализатор; условие; модификатор) Чаще всего цикл for используется для математических вычислений итера- итерационного типа с постоянным приращением на каждой итерации с заранее известным числом повторений или для поиска элементов массивов или строк. Продемонстрируем применение цикла for. Пусть требуется найти сумму первых 7 элементов массива, состоящего из 10 целых чисел. В C++ .NET фрагмент программного кода мог бы выглядеть так, как пред- представлено в листинге 4.9. int il[10] = {3, -5, 2, 1, -9, 1, -3, -7, -11, 15}; int isum = 0; for (int cnt = 0; cnt < 7; cnt++) { isum = isum + il[cnt]; Цикл for на ассемблере удобно реализовать с помощью команды loop. В этом случае переменная цикла помещается в регистр есх, а текущее зна- значение суммы помещается в регистр еах. В регистр esi загружается адрес массива целых чисел ii. Перед переходом на следующую итерацию значение адреса в ESI увеличива- увеличивается на 4. По завершению цикла значение суммы помещается в переменную isum. Фрагмент кода приведен в листинге 4.10. .686 .model flat .data
Глава 4. Оптимизация логических структур C++ с помощью ассемблера 147 il isum .code start: mov xor lea next: add add loop mov end start DD 3, -5, 2, 7, -9, DD 0 ECX, 7 EAX, EAX ESI, DWORD PTR il EAX, [ESI] ESI, 4 next DWORD PTR isum, EAX 4.5. Условный оператор switch Условный оператор switch позволяет осуществить выбор одной из ветвей вычислительного процесса, исходя из значения управляющего выражения. Значение управляющего выражения сравнивается с целой либо символьной константой из списка. Если будет найдено совпадение, то выполнятся ассо- ассоциированные с совпавшей константой операторы. Структуру этого оператора можно представить в виде: switch (выражение) { case константа 1: <операторы> break; case константа 2: <операторы> break; default: } Чтобы реализовать оператор switch на ассемблере, можно использовать сравнение для каждого случая с переходом на соответствующую метку в программс'(листинг 4.11).
148 Часть I. Основы эффективного программирования на ассемблере Листинг 4.11, Фрд|мимч кидо на ассемблере, реализующей опера гор svitch I mov emp je emp je emp je EAX, DWORD PTR N EAX, VALUE_1 BRANCH_1 EAX, VALUE_2 BRANCH_2 EAX, VALUE_3 BRANCH 3 emp EAX, VALUE_N ie BRANCH N BRANCH_1: <операторы> BRANCH_2: <операторы> BRANCH_N: <операторы> Часто бывает удобно вместо условных переходов сразу вызывать подпро- подпрограммы-обработчики условия (листинг 4.12). mov EAX, DWORD PTR N emp EAX, VALUE_1 jne BRANCH_1 call PROC_1 jmp EXIT BRANCH_1: emp EAX, VALUE_2 jne BRANCH 2
Глава 4. Оптимизация логических структур C++ с помощью ассемблера 149_ call PR0C_2 jmp EXIT BRANCH_2: cmp EAX, VALUE_3 jne BRANCH_3 call PR0C_3 jmp EXIT BRANCH_3: cmp EAX, VALUE_4 jne EXIT call PR0C_4 EXIT: cmp EAX, VALUE_2 je BRANCH_2 cmp EAX, VALUE_3 je BRANCH_3 cmp EAX, VALUE_N ie BRANCH N Рассмотрим программу на C++, в которой, в зависимости от выбора одного из трех вариантов, выполняется либо сложение, либо вычитание, либо ум- умножение двух целых чисел. Исходный текст консольного приложения кода представлен в листинге 4.13. р !¦ Нистин!" а.1л. П::-ого";!v:?.:-;,*л «<,:¦/; г-..i О ¦•¦*¦. r.'V/<:ih;:.7pv'pvio:uvv.'i пр1-'Кл(:!к;.ниг II SWITCH_EXM.срр : Defines the.entry point for the console application. #include "stdafx.h" int _tmain(int argc, _TCHAR* argv[]) { int il, i2, isw, ires;
150 Часть I. Основы эффективного программирования на ассемблере while (true) { printf("\nEnter first number (il): ") ; scanf("%d", &il); printf("Enter second number (i2): "); scanf("%d", &i2); printf ("Choice: 1 - il+i2, 2 - il-i2, 3 - il*i2\n"); scanf("%d", &isw); switch (isw) ( case 1: ires = il+i2; break; case 2: ires = il-i2; break; case 3: ires = il*i2; break; default: break; } printf("Result = %d\n", ires); } return 0; Для этого примера неплохо было бы написать эффективный ассемблерный код. Если исключить ветвления switch ... case, это позволяет существен- существенно увеличить производительность программы. Система команд процессоров Pentium II и выше, как уже упоминалось ранее, включает команды cmov и fcmov, с помощью которых можно реализовать алгоритм switch ... case. Модифицируем предыдущий листинг так, чтобы можно было использовать команду cmov, для чего будем использовать встроенный ассемблер C++ .NET. Исходный текст программы показан в листинге 4.14. ¦ П и с т и н г 4.14. Иск р. > о -: о;! и о п о т п л о н и н -:¦¦ -„¦ х - -:¦:¦¦. . . - ¦„ ¦: ¦ * с пом о щ ь; о : в с т р о о н к о г с, ,1 г. с с 'J n л р р .:< F. п ;:¦ о г с a v м:::- н г-. С ¦ ¦ •+ // SWITCH_EXM.срр : Defines the entry point for the console application. #include "stdafx.h"
Глава 4. Оптимизация логических структур C++ с помощью ассемблера 151_ int _tmain(int argc, JTCHAR* argv[]) { int il, i2, isw, ires; int iadd, isub, im; while (true) { printf("\n"); printf("Enter first number (il): "); scanf("%d", &il); printf("Enter second number (i2): "); scanf ("%d", &i2); printf ("Choice: 1 - il+i2, 2 - il-i2, 3 - il*i2\n"); scanf("%d", &isw); iadd = il+i2; isub = il-i2; im = il*i2; _asm { xor EDX, EDX mov EAX, isw cmp EAX, 1 cmove EDX, iadd cmp EAX, 2 cmove EDX, isub cmp EAX, 3 cmove EDX, im mov ires, EDX } printf("Result » %d\n", ires); } ' return 0; Как видно из листинга 4.14, оператор switch представлен своим ассемблер- ассемблерным аналогом, т. е. группой команд в блоке asm {...}. Для реализации ассемблерного варианта необходимо включить в исходный текст объявления вспомогательных переменных: int iadd, isub, im;
152 Часть I. Основы эффективного программирования на ассемблере Непосредственно перед ассемблерным блоком включим следующие опера- операторы: iadd = il+i2; isub = il-i2; im = il*i2; Эти операторы необходимы в команде cmove. Эта команда, в зависимости от флагов, установленных предыдущей командой, выполняет пересылку содер- содержимого регистра или ячейки памяти в другой регистр. Перед применением команды cmov необходимо проверить, поддерживается ли она процессором. Это делается с помощью команды cpuid. Окно работающего приложения показано на рис. 4.2. Рис. 4.2. Окно приложения, показывающего применение ассемблерных команд для замены оператора switch На этом мы закончим рассмотрение логических структур C++ .NET и их оптимизацию с помощью ассемблера. Все примеры из этой главы могут быть легко модифицированы читателями для использования в собственных разработках.
ЧАСТЬ II С ЯЗЫКАМИ ВЫСОКОГО УРОВНЯ
Глава 5 Интерфейс модулей на ассемблере с программами на C++ Среда программирования Visual C++ .NET 2003 является довольно мошной и обладает встроенными средствами разработки на ассемблере. Основным преимуществом встраиваемого ассемблерного кода является простота напи- написания, т. к. не надо создавать дополнительный код для загрузки, отсутству- отсутствуют проблемы именования и передачи параметров. С другой стороны, встроенный ассемблер обладает целым рядом недостат- недостатков. Ограничения, накладываемые компилятором, влияют на программный код ассемблерных вставок и функций, особенно когда речь идет об оптими- оптимизации приложения. Проблемы со встраиваемым ассемблерным кодом могут возникнуть и при компиляции программы для различных платформ. Использование отдельно скомпилированных ассемблерных модулей в C++ .NET предоставляет разработчику более широкие возможности для оп- оптимизации приложений. Кроме того, отдельно скомпилированные модули можно применять неоднократно в различных приложениях, что в случае со встроенным ассемблером весьма проблематично. Материал этой главы посвящен разработке интерфейса отдельно скомпи- скомпилированных ассемблерных модулей с программами на C++. 5.1. Общие принципы построения интерфейсов с языками высокого уровня Рассмотрим наиболее общие-вопросы, касающиеся построения интерфейсов при вызове процедур на ассемблере из программ, написанных на C++ .NET. Для иллюстрации материала используются многочисленные примеры, которые являются оригинальными разработками и нигде более не встреча- встречаются. Везде, где необходимо, даются подробные комментарии. Программные модули на ассемблере разработаны с использованием компи- компилятора MASM 6.14 фирмы Microsoft. Для разработки модулей я буду исполь-
156 Часть II. Интерфейс с языками высокого уровня зовать упрощенный синтаксис языка ассемблера. Это означает, что везде в исходных текстах будут использоваться директивы .data и .code. Я не буду описывать здесь все опции компилятора MASM, поскольку будут использо- использоваться только некоторые из них, и разъяснения будут приводиться по ходу текста. Скомпилированные модули на ассемблере имеют расширение obj, а команд- командная строка для компилятора MASM выглядит так: ml /с /Fo <имя_файла.obj> <имя_файла.asm> Полученный OBJ-файл необходимо скомпоновать с основной программой на языке C++. При разработке интерфейса ассемблерного модуля и основ- основной программы необходимо учитывать следующее: П правила согласования имен идентификаторов (переменных и функций), помещенных в объектные файлы. Компилятор языка высокого уровня может изменять или нет оригинальные имена в объектном модуле, поэтому важно знать, происходит ли такое изменение и каким именно образом; П модель памяти, используемую ассемблерным модулем (tiny, small, compact, medium, huge, large ИЛИ flat); ? параметры вызова функции на ассемблере. Параметры вызова — это до- довольно обширное понятие и включает следующие аспекты, которые дол- должен принимать во внимание программист: • нужно ли сохранять регистры в функции, и если да, то какие именно; • порядок передачи параметров вызываемой функции; • метод передачи параметров в функцию (с использованием регистров, стека, разделяемой памяти); • способ передачи параметров в вызываемую функцию (по значению или по ссылке); • если передача параметров функции осуществляется через стек, то как должен восстанавливаться указатель стека — вызывающей или вызы- вызываемой программой или функцией; • метод возвращения значения в вызывающую программу (через стек, регистры или общую область памяти). Рассмотрим принципы построения интерфейсов более подробно. Начнем с согласования имен идентификаторов. Компилятор C++ не изменяет регист- регистра букв, и имена идентификаторов по этой причине считаются чувствитель- чувствительными к регистру. Кроме того, компилятор C++ перед всеми внешними именами помещает префикс в виде символа подчеркивания. Необходимо учитывать и модели памяти, используемые внешними функ- функциями. Для 32-разрядных приложений используется только одна модель па-
Глава 5. Интерфейс модулей на ассемблере с программами на C++ 157 мяти — fiat. Что касается параметров вызова внешних функций, то, не- несмотря на сложность и некоторую запутанность описания во многих литера- литературных источниках, в действительности все оказывается намного проще. Для программиста, желающего разобраться с этими нагромождениями ди- директив и соглашений, выделим основные моменты процесса вызова внеш- внешних функций из программы на C++. ? Для 32-разрядных приложений параметры в вызываемую функцию пере- передаются одним из двух способов: либо по значению, либо по ссылке. При передаче параметра по значению в функцию передается непосредственно сам 32-разрядный операнд, а при передаче по ссылке — адрес (тоже 32- разрядное значение) этого операнда. ? Параметры передаются через стек, через регистры или через общую об- область памяти. Передача параметров через общую область памяти для 32- разрядных приложений довольно сложна в реализации и применяется обычно в системном программировании и при разработке драйверов уст- устройств. Это отдельная тема и мы ее рассмотрим более подробно в главе 7. Все варианты передачи параметров в функцию через стек или регистры представлены в табл. 5.1. Таблица 5.1. Варианты передачи параметров Директива Порядок передачи параметров Освобождение стека Передача пара метров через регистры fastcall Слева направо cdecl Справа налево stdcall Справа налево Процедура Вызывающая программа Процедура ЕСХ, EDX, стек справа-налево Нет Нет Хотелось бы сделать некоторые пояснения, касающиеся табл. 5.1. Порядок передачи параметров для каждой директивы указывает компилятору, каким образом параметры передаются в вызываемую функцию. Для директив _cdeci и _stdcaii параметры передаются через стек, а при использовании директивы _fastcaii — через регистры (первые два параметра) и стек (остальные параметры). Перед возвращением в основную профамму необходимо восстановить указа- указатель стека. Это касается всех директив. Что касается применения тех или иных способов вызова внешних функций, то здесь не существует однознач- однозначных рецептов. Если вы работаете с API-функциями Windows, то для них стан- стандартным способом вызова является _stdcaii. Директиву _cdeci лучше ис- использовать для вызова процедур и функций из профамм, написанных в C++.
158 - Часть II. Интерфейс с языками высокого уровня Наиболее быстрым способом передачи параметров в Visual C++ .NET явля- является _fastcaii. Для передачи первых двух параметров стек не используется, поэтому можно получить выигрыш в производительности. Все функции возвращают результат либо в регистре еах, либо в вершине стека математического сопроцессора st (о). Для иллюстрации хочу привести в качестве примера несложную функцию на ассемблере, вычисляющую сумму двух целых чисел. Программа на C++ передает функции два целочисленных параметра, а в качестве результата получает их сумму. Назовем эту функцию Addmts, первый параметр обо- обозначим как XI, а второй как Х2. В этом случае функция вычисления суммы могла бы выглядеть как Addmts (xi, X2). Исходный текст функции для варианта передачи параметров _stdcaii при- приведен в листинге 5.1. ; — .686 addints.asm .model flat public _ .data .code _AddInts@8 push mov mov add pop ret _AddInts@8 end Addlnts@8 proc EBP EBP, ESP EAX, DWORD PTR [EBP+8] EAX, DWORD PTR [EBP+12] EBP 8 endp Для корректного вызова функции необходимо в начале ее имени поместить символ подчеркивания, а в конце добавить суффикс @п, где п — число байт, требуемое для передачи параметров. В данном случае п равно 8. Такая фор- форма именования функции отвечает требованиям компилятора C++ .NET. Функция получает параметры в стеке, а результат возвращает в регистре еах.
Глава 5. Интерфейс модулей на ассемблере с программами на C++ 159 Поскольку функция Addmts должна быть доступна из внешних программ- программных модулей, необходимо объявить ее с директивой public. Первые две строки тела процедуры push EBP mov EBP, ESP необходимы для доступа к параметрам в стеке с помощью регистра евр. Са- Сами параметры находятся в стеке по адресам [евр+8] и [евр+12]. Но где из них первый, а где второй? Чтобы ответить на этот вопрос, необходимо ука- указать в вызывающей программе порядок передачи аргументов. Директива _stdcaii (см. табл. 5.1) указывает на то, что параметры xi и Х2 процедуры Addints передаются через стек справа налево, т. е. первым, в стек помещается Х2, затем xi. Поскольку стек растет от больших адресов памяти к меньшим, то Х2 будет размещаться по большему адресу, a xi — по мень- меньшему. После вызова процедуры Addints и сохранения регистра евр в стеке распо- расположение параметров xi и Х2 в стеке будет выглядеть так, как изображено на рис. 5.1. ЕВР+12(Х2) ЕВР+8 (Х1) ЕВР+4 (адрес возврата) ЕВР ЕВР<— ESP Рис. 5.1. Расположение параметров в стеке Для нахождения суммы чисел используются команды: mov ЕАХ, DWORD PTR [ЕВР+8] add ЕАХ, DWORD PTR [ЕВР+12] После выполнения этих команд результат сложения находится в регистре еах. Возвращая управление вызывающей программе, функция Addints в со- соответствии с директивой _stdcaii восстанавливает стек. Перед выполнени- выполнением команды ret там находятся два двойных слова, т. е. 8 байт. Для очистки стека в команде ret необходимо указать параметр 8. В качестве альтернати- альтернативы вместо ret 8 можно воспользоваться последовательностью команд: add ret ESP, 8
160 Часть II. Интерфейс с языками высокого уровня Сохраним исходный текст программы в файле Addlnts.asm и откомпилиру- откомпилируем его: ml /с Addlnts.asm Опция /с указывает компилятору на необходимость только компиляции ис- исходного модуля. В случае успешного выполнения операции получим файл объектного модуля Addints.obj, который будет использован в основной программе. Программа на C++. NET, вызывающая функцию Addlnts, должна содер- содержать ее объявление в разделе описаний переменных и функций: extern "С" int _stdcall Addlnts(int il, int i2); Имеет смысл рассмотреть более подробно директивы и спецификаторы в описании функции Addlnts, поскольку понимание их очень важно для пра- правильного построения интерфейсов с любыми внешними функциями. Директива extern указывает н^ то, что функция является внешней по от- отношению к модулю, где она используется. Спецификатор "с11 запрещает компилятору C++ декорировать (модифици- (модифицировать) имя внешнего идентификатора. Декорированное имя функции, ис- используемой в C++, содержит следующую информацию: ? имя функции; ? класс, членом которого является функция; . ? пространство имен (область видимости) функции; ? типы параметров, принимаемых функцией; ? соглашение о вызовах; ? тип возвращаемого значения. Декорированные имена имеют определенный смысл только для компилято- компилятора и компоновщика (линкера) C++ .NET. Примеры недекорированных и декорированных имен представлены в табл. 5.2. Таблица 5.2. Пример недекорированных и декорированных имен Недекорированное имя Декорированное имя int a(char) ?a@@YAHD@Z int i=3; return i; }; void stdcallb::с(float){}; ?c@b@@AAGXM@Z
Глава 5. Интерфейс модулей на ассемблере с программами на C++ 161 В подавляющем большинстве случаев декорированное имя функции знать не нужно. Необходимость в этом возникает, например, при вызове функ- функций, написанных на C++, из ассемблерных программ. В описании функции применяется соглашение о вызовах _stdcaii. В об- общем случае соглашение о вызовах в среде программирования C++ .NET можно установить, используя страницу свойств разрабатываемого проекта. Компилятор может работать со следующими опциями: ? /Gd — установлена по умолчанию, определяет cdeci соглашение для всех функций, исключая функции-члены и функции, явно обявленные как _stdcall ИЛИ _fastcali; ? /Gr — определяет соглашение _fastcaii для всех функций, исключая функции-члены и функции, явно объявленные как cdeci или stdcaii. Все функции fastcaii должны иметь прототипы; ? /Gz — определяет соглашение stdcaii для всех функций, исключая функции с переменным числом аргументов и функции, явно объявлен- объявленные как cdeci или fastcaii. Все функции, использующие соглаше- соглашение stdcaii, должны иметь прототипы. Для установки этих опциий компилятора в Visual C++ .NET, необходимо: 1. Открыть страницу свойств проекта (Property Pages dialog box). 2. Выбрать папку C/C++. 3. Выбрать страницу Advanced. 4. Изменить свойство Calling Convention. Вернемся к нашей программе. Перед компиляцией программы на C++ не- необходимо включить в проект файл объектного модуля с вызываемой функ- функцией. Проще всего скопировать файл в рабочий каталог проекта. Фрагмент программы на C++, выполняющей вычисления с использованием внешней функции Addints, мог бы выглядеть так: int Intl = 74; int Int2 - -56; int ires; ires = Addints(Intl, Int2); И еще одно замечание. Компоновщик Visual C++ работает с файлами объ- объектных модулей в формате COFF (Common Object File Format). При компи- компиляции исходного модуля с помощью MASM можно получить объектный файл как в формате COFF, так и в формате OMF (Object Module Format).
162 Часть II. Интерфейс с языками высокого уровня Поэтому в процессе сборки проекта в C++ .NET компоновщик может вы- выдать предупреждение: Warning: converting object format from OMF to COFF В принципе, это не так важно, поскольку компилятор C++ преобразует формат OMF в COFF в любом случае. Можно сразу задать опцию /cof f в компиляторе MASM для получения COFF-файла: ml /с /coff Addlnts.asm Сейчас мы рассмотрим, как работает наш интерфейс, если использовать со- соглашение о вызовах_сс!ес1 (листинг 5.2). Отличие этого метода передачи па- параметров от _stdcaii в том, что вызывающая программа должна сама восста- восстанавливать стек. Параметры передаются справа налево, как и в _stdcaii. • _ _ — .686 addints.asm .model flat public .data .code _AddInts push mov mov 'add pop ret _AddInts end _AddInts proc EBP EBP, ESP EAX, DWORD PTR [EBP+8] EAX, DWORD PTR [EBP+12] EBP endp Как видно из исходного текста, для работы с директивой _cdeci необходи- необходимо добавить символ подчеркивания в начало имени. Команда ret выхода из функции используется здесь без параметров. Что касается исходного текста программы на Visual C++, то изменения здесь минимальны и касаются лишь раздела деклараций, где мы должны заменить директиву _stdcaii директивой _cdeci: extern "С" int _cdecl Addlnts(int il, int i2); И, наконец, рассмотрим довольно широко применяемый регистровый метод передачи параметров в вызываемую функцию, представленный директивой
Глава 5. Интерфейс модулей на ассемблере с программами на C++ 163_ _fastcaii. Аргументы передаются через регистры есх и edx слева направо. Если имеется больше 3-х аргументов, то, начиная с 4-го, остальные переда- передаются через стек. Рассмотрим функцию (назовем ее AddSubFc), выполняю- выполняющую операцию и - 12 - 13 + 14, где и, 12, 13 и 14 — целые числа. Ис- Исходный текст функции представлен в листинге 5.3. ; addsubf с. asm .686 .model flat public @AddSubFc@16 .code @AddSubFc@16 proc push EBP mov EBP, ESP sub ECX, EDX sub ECX, DWORD PTR [EBP+8] add ECX, DWORD PTR [EBP+12} mov EAX, ECX pop EBP ret 8 @AddSubFc@16 endp end Если число параметров не превышает двух, то метод _fastcaii позволяет существенно ускорить работу приложения в целом, т. к. не требуется ини- инициализация стека и его восстановление, как при других методах передачи параметров. Однако злоупотреблять им не стоит, потому что интенсивное использование регистров процессора в функциях будет препятствовать оп- оптимизации программы компилятором. Как известно, для оптимизации про- программ многие компиляторы языков высокого уровня используют регистры процессора. Обратите внимание на суффикс @1б в идентификаторе имени функции. Он указывает общее количество байт, занимаемых параметрами (два двойных слова в регистрах есх и edx и два двойных слова в стеке). В основной программе на Visual C++ для вызова этой процедуры необходи- необходимо указать директиву _fastcaii: extern "С" int fastcall Addlnts(int il, int i2, int i3, int i4);
164 Часть II. Интерфейс с языками высокого уровня На практике очень часто приходится иметь дело с несколькими ассемблер- ассемблерными модулями, поэтому рассмотрим более сложный вариант интерфейса программы на C++ .NET и процедур на ассемблере. Предположим, основ- основная программа должна вычислять сумму двух целых чисел, уменьшенную на число 20. Применим две отдельно скомпилированные ассемблерные функ- функции для вычислений. Пусть первая функция выполняет суммиро;вание (назовем ее AddTwo) и принимает в качестве параметров два целых числа inti и int2. Вторая функция (назовем ее Sub20) вычитает из результата, по- полученного AddTwo, число 20. Сделаем так, чтобы результат вычислений воз- возвращался В ОСНОВНУЮ Программу функцией AddTwo. Исходный текст функции AddTwo сохраним в файле AddTwo.asm, а исход- исходный текст функции sub20 — в файле Sub20.asm. Кроме того, положим, что функция AddTwo обрабатывает параметры в соот- соответствии с директивой _stdcaii, а функция sub20 — в соответствии с дирек- директивой _cdeci. Исходный текст функции AddTwo представлен в листинге 5.4. ; AddTwo.asm .686 .model flat public _AddTwo@8 extern _Sub20:proc .code _AddTwo@8 proc push EBP mov EBP, ESP mov EAX, DWORD PTR [EBP+8] add EAX, DWORD PTR [EBP+12] push EAX call _Sub20 add ESP, 4 pop EBP ret 8 _AddTwo@8 endp end Посмотрим, чем модифицированный вариант функции AddTwo отличается от исходного. Первое отличие — наличие строки extern _Sub2Q:proc
Глава 5. Интерфейс модулей на ассемблере с программами на C++ 165_ в разделе директив. Эта строка вынуждает компилятор MASM рассматривать функцию sub20 как внешнюю по отношению к модулю, содержащему AddTwo. Кроме того, при сборке приложения компилятор Visual C++ .NET предполагает, что передача параметров в функцию sub20 выполняется в со- соответствии с директивой _cdeci (поскольку идентификатор функции начи- начинается с символа подчеркивания). Второе отличие — присутствие команд push ЕАХ call _Sub20 add ESP, 4 К моменту выполнения этих строк кода в регистре еах находится .сумма двух целых чисел. Это значение является параметром для функции sub20 и помещается в стек командой push еах. После выполнения функции sub20 результат находится в регистре еах. Поскольку при вызове _cdeci стек должна восстанавливать вызывающая программа, то присутствие оператора add esp, 4 становится понятным. ' Проанализируем исходный текст функции sub20 (листинг 5.5). ; Sub20.asm .686 .model flat public _Sub20 .code _Sub20 proc push EBP mov EBP, ESP mov EAX, DWORD PTR [EBP+8] sub EAX, 20 pop EBP ret _Sub20 endp end В качестве параметра функция sub20 принимает значение суммы двух целых чисел. Результат возвращается в регистре еах, стек при выходе из функции не восстанавливается, это должна сделать вызывающая программа, в данном случае, функция AddTwo. Директива public _Sub20 делает функцию sub20 доступной из других модулей.
166 Часть II. Интерфейс с языками высокого уровня После компиляции файлов AddTwo.asm и Sub20.asm полученные объектные модули AddTwo.obj и Sub20.obj необходимо включить в проект Visual C++ .NET. С помощью мастера приложений Visual Studio .NET 2003 разработаем консольное приложение Windows, в котором будут использоваться функции AddTwo и Sub20. Исходный текст главного файла проекта представлен в лис- листинге 5.6. // This is the main project file for VC++ application project // generated using an Application Wizard. #include "stdafx.h" extern "C" int _stdcal-l AddTwo (int il, int i2) ; extern "C" int _cdecl Sub20(int i3); #using <mscorlib.dll> using namespace System; int _tmain() { // TODO: Please replace the sample code below with your own. String *SIntl, *SInt2, *SIres; int Intl, Int2,ires; Console::Write("Enter Intl: "); Slntl = Console::ReadLine(); Console::Write("Enter Int2: "); SInt2 = Console::ReadLine (); Intl = Convert::ToInt32 (Slntl); Int2 = Convert::ToInt32 (SInt2); ires = AddTwo(Intl, Int2); Sires = Convert::ToString(ires); Console::Write("Result:[Intl + Int2 - 20] = "); Console::WriteLine(ires); Console::WriteLine("Press any key to exit...");
Глава 5. Интерфейс модулей на ассемблере с программами на C++ 167 Console::ReadLine (); return 0; В раздел объявлений, как видно из исходного текста, обязательно должны быть включены строки extern "С" int _stdcall AddTwo(int il, int i2); extern "C" int _cdecl Sub20(int i3); В остальном программный код понятен и в пояснениях не нуждается. Вид окна работающего приложения показан на рис. 5.2. Рис. 5.2. Окно приложения, иллюстрирующего применение двух отдельных ассемблерных процедур из двух объектных файлов Компоновку и отладку приложения можно упростить, если вместо двух от- отдельных файлов с исходными текстами функций использовать один. Помес- Поместим исходный текст ассемблерных функций AddTwo и Sub20 в файл AddSub.asm. Немного изменим исходный текст функции sub20, заменив ко- команду sub еах, 20 на sub еах, юо. Назовем модифицированную функцию Subioo. Исходный текст функций из файла AddSub.asm приводится в лис- листинге 5.7. . AddSub.asm .686 .model flat public _AddTwo@8 ;_stdcall convention public _Subl00 ;_cdecl convention
168 Часть II. Интерфейс с языками высокого уровня .code _AddTwo@8 proc push EBP mov EBP, ESP mov EAX, DWORD PTR [EBP+8] add EAX, DWORD PTR [EBP+12] push EAX call _SubiOO add ESP,4 pop EBP ret 8 _AddTwo@8 endp _SublOO proc push EBP mov EBP, ESP mov EAX, DWORD PTR [EBP+8] sub EAX, 100 pop EBP ret _Subl00 endp end Как видно из листинга, в секции деклараций исчезла строка extern _Sub20:proc поскольку обе процедуры находятся в одном модуле. Если компилятор MASM не обнаружил ошибок, то получим файл объектного модуля AddSub.obj. В исходном файле проекта C++ заменим строку extern "С" int _cdecl Sub20(int i3); на extern "С" int _cdecl Subl00(int i3); Кроме того, удалим из проекта файлы AddTwo.obj и Sub20.obj, а вместо них включим в проект AddSub.obj. Окно работающего приложения показано на рис. 5.3. Из предыдущих примеров видно, что при выполнении функций, написан- написанных в соответствии с соглашениями _stdcaii или _cdeci, необходимо каж- каждый раз сохранять и восстанавливать стек. При интенсивных вычислениях, когда подобные функции вызываются много раз, такие операции могут ска-
Глава 5. Интерфейс модулей на ассемблере с программами на C++ 169 зываться на производительности приложения. Если при разработке функций использовать соглашение _fastcaii, это поможет повысить производитель- производительность приложения. Использовать этот метод следует осторожно, поскольку компилятор C++ .NET интенсивно использует регистры. Рис. 5.3. Окно приложения, иллюстрирующего применение двух отдельных ассемблерных процедур из одного объектного файла В следующем примере рассмотрим вычисление результата математической операции для целых чисел inti, mt2, int3, int4 no формуле inti-int2- Int3+int4-ioo и вывод результата на экран. Разработаем в C++ .NET при- приложение на основе диалогового окна. На главной форме приложения помес- поместим ПЯТЬ ЭЛемеНТОВ редактирования (Edit Control), КНОПКУ (Button) И ПЯТЬ элементов статического текста (static Text). Поставим в соответствие эле- элементам редактирования целочисленные переменные ii-i4, соответствую- соответствующие числам inti-int4, а также переменную ires, в которую будет выведен результат. Вывод результата на экран выполняется при нажатии на кнопку. Сами вычисления выполняются с помощью двух ассемблерных функций — AddSubFc (_fastcall) И SublOO (cdecl), ИСХОДНЫЙ ТвКСТ КОТОрЫХ ПРИВО- ПРИВОДИТСЯ в листинге 5.8. Л -ф, 5.8. Ф у и к ц и и. ж с п о л ь j у ю i .ц и б г о г л з lj с н и я t ?* n ¦: с о .1.1 и с u .686 .model flat public @AddSubFc@16, _SublOO .code @AddSubFc@16 proc push EBP
170 Часть II, Интерфейс с языками высокого уровня mov sub sub add mov push call add pop ret EBP, ECX, ECX, ECX, EAX, EAX ESP EDX ;ECX -> Intl-Int2 DWORD PTR [EBP+8] ;ECX -> ECX-Int3 DWORD PTR [EBP+12] ;ECX -> ECX+Int4 ECX _SublOO ESP, EBP 8 4 0AddSubFc@16 endp _SublOO push mov mov sub pop ret _SublOO end proc EBP EBP, EAX, EAX, EBP endp ESP DWORD PTR [EBP+8] 100 Первый параметр функции AddSubFc (т. е. inti) передается в регистре есх, второй, int2 — в регистре edx, третий параметр, int3, находится в стеке по адресу [ЕВР+8], и, наконец, четвертый — по адресу [евр+12]. Перед вызовом функции subioo результат промежуточных вычислений из регистра есх по- помещается в регистр еах командой mov еах, есх. Последовательность команд push EAX call _Subl00 i add ESP,4 является стандартной для вызова процедуры с соглашением _cdeci, прини- принимающей один параметр. Сохраним исходный текст функций в файле AddSubFc.asm и откомпилируем его. Если нет ошибок, получим файл AddSubFc.obj, который включим в проект на C++ .NET. В исходном файле проекта C++ в раздел деклараций добавим строки extern "С" int _fastcall AddSubFc(int il, int i2, int i3, int i4); extern "C" int _cdecl Subl00(int i3); Обработчик нажатия кнопки в вызывающей программе на C++ приводится в листинге 5.9.
Глава 5. Интерфейс модулей на ассемблере с программами на C++ 171 Листинг 5.9. Обработчик кажзгик кнопки void CCdecl_FastCall_MFCDlg::OnBnClickedButtonl() { // TODO: Add your control notification handler code here UpdateData(TRUE); ires = AddSubFc(il, i2, i3, i4); UpdateData(FALSE); Окно приложения после успешной сборки проекта и запуска на выполне- выполнение изображено на рис. 5.4. \7Т Рис. 5.4. Окно приложения, демонстрирующего использование функций с соглашениями _cdecl и _fastcall Последний пример этой главы показывает, как можно получить результат выполнения математической операции через стек математического сопро- сопроцессора. Функция (назовем ее nfp) получает в качестве параметра вещест- вещественное число и инвертирует его. Результат возвращается в вершине стека st @). Исходный текст функции nfp показан в листинге 5.10. .686 .model flat public _nfp@4 nfp.asm
172 Часть II. Интерфейс с языками высокого уровня .code _nfp@4.proc push EBP mov EBP, ESP finit fid DWORD PTR [EBP+8] fchs fwait pop EBP ret 4 _nfp@4 endp end Параметр, представляющий собой вещественное число, помещается в вер- вершину стека сопроцессора командой fid dword ptr [евр+8]. Изменение знака выполняется командой fchs, а результат остается в вершине стека со- сопроцессора. Консольное приложение C++ .NET использует результат выполнения функции nfp для отображения преобразованного числа на экране. Исход- Исходный текст приложения представлен в листинге 5.11. // This is the main project file for VC++ application project // generated using an Application Wizard. #include "stdafx.h" extern "C" float _stdcall nfp(float fl); #using <mscorlib.dll> using namespace System; int _tmain() { // TODO: Please replace the sample code below with your own. String *sl; float fl; Console::Write("Enter float value: "); si = Console::ReadLine();
Глава 5. Интерфейс модулей на ассемблере с программами на C++ 173_ fl = Convert::ToSingle(si); fl = nfp(fl); ' si = Convert::ToString(fl); Console::Write("Changed value: "); Console::WriteLine(si); Console::Write("Press any key to exit..."); Console::ReadLine(); return 0; Вызываемая функция объявлена следующим образом: extern "С" float _stdcall nfp{float fl); Результат выполнения функции nfp сохраняется в переменной fl fl - nfp(fl); В этом случае число, находящееся в вершине стека сопроцессора, копирует- копируется в переменную fl, регистр еах при этом не используется. Вид окна рабо- работающего приложения показан на рис. 5.5. Рис. 5.5. Окно приложения, выполняющего вывод инвертированного вещественного числа на экран В этой главе основное внимание было уделено интерфейсам внешних моду- модулей на ассемблере с программами на C++ .NET. На этом этапе мы не рас- рассматривали работу с различными типами параметров. Я сознательно отнес эту тему к следующей главе, в которой будут подробно рассмотрены спосо- способы передачи параметров и анализ возвращаемых функциями результатов для различных типов данных.
Глава 6 Особенности разработки и применения подпрограмм на ассемблере В предыдущей главе были рассмотрены общие принципы построения ин- интерфейсов ассемблерных модулей с основной программой на C++ .NET, основные стандарты и соглашения, используемые в программах. Остано- Остановимся более подробно на применении параметров при вызове ассемблерных функций. Существуют два основных способа, с помощью которых функция получает данные для дальнейшей обработки — по значению и по ссылке. Вначале проанализируем вариант передачи параметров по значению. В этом случае вызываемая функция получает копию переменной, и при вы- выходе из функции эта копия теряется. В то же время переменная в вызываю- вызывающей программе не меняет значение. Рассмотрим простой пример консоль- консольного приложения, в котором вызываемая функция выполняет умножение целочисленного параметра на 5 и возвращает результат основной программе 'в регистре еах. Условимся, что основная программа и вызываемая функция в примерах этой и последующих главах используют соглашение stdcaii. Ассемблерная функция, выполняющая умножение числа, представлена в листинге 6.1. nm"TV:;r u ¦). Оу;н-Г(;ИЗ. ОЫПОЛнЯК^ЦЛЯ ум НО А'Г,-НИ С ЦОЛСГО ЧИСЛЛ !¦<¦! fj. .686 .model flat public _mul5@4 .code __mul5@4 proc push EBP raov EBP, ESP mov EAX, DWORD PTR [EBP+8]
Глава 6. Особенности разработки и применения подпрограмм на ассемблере 175 mov ECX, 5 mil ECX pop EBP ret _mul5@4 endp end Исходный текст функции несложен и в дополнительных пояснениях не ну- нуждается. Основная программа на Visual C++ .NET разработана как консоль- консольное приложение, и ее исходный текст представлен в листинге 6.2. // This is the main project file for VC++ application project // generated using an Application Wizard. #include "stdafx.h" #using <mscorlib.dll> extern "C" int _stdcall mul5(int il); using namespace System; int _tmain() s { // TODO: Please replace the sample code below with your own, int il, i5; String *sil, *Si5; Console::Write("Enter integer value: "); Sil - Console::ReadLine(); il = Convert::ToInt32 (Sil); i5 Sil = Convert::ToString(il); Si5 = Convert::ToString (i5); Console::Write("Entered integer = ") Console::WriteLine (Sil);
176 Часть II. Интерфейс с языками высокого уровня Console: ".Write("Multiplying il x 5 Console::WriteLine (Si5); Console::ReadLine(); return 0; После вызова процедуры muis с помощью оператора i5 = mui5(ii) значе- значение переменной il не меняется. Это видно на рис. 6.1, где изображено окно работающего приложения. Рис 6.1. Окно приложения, выполняющего умножение целого числа на константу Использование параметров-значений при вызове функций делает очень не- неудобной обработку массивов числовых и символьных данных. Для обработки таких данных при вызове функций обычно используются указатели, а передача параметров таким способом называется передачей по ссылке. Рассмотрим более подробно применение параметров-указателей для обра- обработки строк и массивов в C++ .NET с использованием ассемблерных функ- функций. Как известно, указателем называется переменная, содержащая адрес ячейки памяти другой переменной. Адресом строки или массива является адрес первого элемента. Адреса последующих элементов вычисляются путем прибавления величины, равной размерности элемента строки или массива. Для символьных строк ascii, которые мы будем рассматривать, адрес сле- следующего элемента на 1 больше адреса предыдущего. Для целочисленного массива, например, адрес последующего элемента на 4 больше адреса пре- предыдущего. Еще одно важное замечание. Во всех примерах этой главы при манипуляци- манипуляциях со строками будут использоваться строки с завершающим нулем.
Глава 6. Особенности разработки и применения подпрограмм на ассемблере 177 Следующий пример показывает, как можно передать строку символов в ос- основную программу из ассемблерного модуля и отобразить ее в окне прило- приложения. Исходный текст ассемблерной функции представлен в листинге 6.3. ; strshow.asm .686 .model flat public _strshow@0 .data TESTSTR DB "THIS STRING GOES FROM ASM PROCEDURE !", 0 .code _strshow@0 proc mov EAX, offset TESTSTR ret _strshow@0 endp end Функция очень проста. Она не принимает параметров и возвращает адрес строки teststr в регистре еах. Исходный текст консольного приложения C++ .NET, использующего вызов функции strshow, также не вызывает за- затруднений при анализе (листинг 6.4). // This is the main project file for VC++ application project // generated using an Application Wizard. #include "stdafx.h" extern "C" char* _stdcall strshow(void); #using <mscorlib.dll> using namespace System; int _tmain () { Console::Write (strshow());
178 Часть II. Интерфейс с языками высокого уровня Console::ReadLine() ; return 0; Вывод строки в окно приложения выполняется с помощью оператора Console: :Write. Метод Write класса System: :Console В Качестве аргумента принимает указатель на буфер строки, возвращаемый функцией strshow. Вид окна работающего приложения показан на рис. 6.2. Рис 6.2. Окно приложения, отображающего строку из ассемблерной функции Еще один вариант передачи строки или массива в основную программу — скопировать строку в буфер памяти основной программы. Исходный текст ассемблерной функции, выполняющей эти действия, показан в листинге 6.5. ПО'¦¦: н.> про; |,-..:?;м copystr.asm .686 .model flat public _copystr@4 .data TESTSTR DB "TEST STRING IS COPIED FROM ASM PROCEDURE !", 0 LENSTR EQU $-TESTSTR .code _copystr@4 proc push ESI push EDI push EBP
Глава 6. Особенности разработки и применения подпрограмм на ассемблере 179 mov eld. mov mov mov rep pop pop pop ret _copystr@4 end EBP, ECX, ESI, EDI, movsb EBP EDI ESI 4 endp ESP LENSTR offset TESTSTR DWORD PTR [EBP+16] В качестве параметра функция получает адрес буфера памяти вызывающей программы, куда нужно скопировать строку. Предполагается, что буфер имеет достаточный размер для помещения всей строки. В регистр есх по- помещается размер строки в байтах. Регистр esi содержит адрес строки- источника teststr, а регистр edi — адрес строки-получателя в основной программе. Копирование выполняется командой rep movsb. Консольное приложение C++ .NET, отображающее копию строки на экране, может быть представлено следующим программным кодом (лис- (листинг 6.6). // This is the main project file for VC++ application project // generated using an Application Wizard. #include "stdafx.h" extern "C" void _stdcall copystr(char* si); #using <mscorlib.dll> using namespace System; int _tmain() { // TODO: Please replace the sample code below with your own. char buf[64]; copystr(buf);
180 Часть II. Интерфейс с языками высокого уровня Console::WriteLine(buf); Console::ReadLine(); return 0; Окно работающего приложения показано на рис. 6.3. Рис. 6.3. Окно приложения, отображающего копию строки на экране При сборке приложения в Visual C++ .NET, как обычно, необходимо вклю- включить файл объектного модуля на ассемблере в состав проекта. Рассмотрим еще несколько примеров, демонстрирующих технику передачи параметров и обработки данных в ассемблерных функциях. Очень часто возникает необходимость передать в основную программу не целую строку, а лишь ее часть (подстроку), начиная с определенной пози- позиции. Следующий пример показывает, как это можно сделать. Пусть в ассемблерном модуле находится строка символов, и необходимо передать в вызывающую программу подстроку, начиная с определенной по- позиции. В этом случае ассемблерная функция получает в качестве параметра величину начального смещения от начала строки, а возвращает адрес пер- первого элемента выделенной подстроки. Исходный текст функции на ассемблере (назовем ее strpart) приведен в листинге 6.7. 6.7. Фун^и^.я, ыо-лф;иц;;ющпн подстроку п strpart. asm .686 .model flat public _strpart@8 .data .code
Глава 6. Особенности разработки и применения подпрограмм на ассемблере 181 _strpart@8 push mov mov mov add pop ret _strpart@8 end proc EBP EBP, ECX, EAX, EAX, EBP 8 ESP DWORD PTR [EBP+12] DWORD PTR [EBP+8] ECX endp В качестве шаблона для основной программы на C++ .NET выберем клас- классический вариант процедурно-ориентированного Windows-приложения. По- После генерации каркаса мастером приложений сделаем некоторые изменения и дополнения в исходном тексте и добавим в меню пункт Return Part of String. Свяжем с ним идентификатор iDPartstr. При выборе этого пункта меню в окне работающего приложения будет отображаться исходная строка, подстрока и смещение в исходной строке. В разделе объявлений основной программы winMain сделаем ссылку на внешнюю процедуру: extern "С" char* _stdcall strpart(char *ps, int off); В качестве параметров функция strpart принимает адрес исходной строки (ps) и смещение от ее начала (off). В приложениии используется еще несколько переменных: char src[] = "STRING1 STRING2 STRING3 STRING4 STRING5"; char *dst; int off, ioff; char buf[4]; где: ? строка src — исходная строка для обработки; ? строка dst — строка-получатель; ? целочисленная переменная off определяет смещение от начала исходной строки; ? строка buf и целое ioff используются функцией sprintf для формати- форматирования вывода. В функции обратного вызова wndProc напишем обработчик выбора пункта меню iD_Partstr (листинг 6.8).
182 Часть II. Интерфейс с языками высокого уровня case ID_PartStr: hdc = GetDC(hWnd); GetClientRect(hWnd, Srect); off = 10; dst = strpart(src, off); ioff = sprintf(buf,"%d",off); TextOut(hdc,(rect.right - rect.left)/4, (rect.bottom - rect.top)/4, "Source:", 7); TextCait(hdc, (rect.right - rect.left)/3, (rect.bottom - rect.top)/4, src, strlen(src)); TextOut(hdc, (rect.right - rect.left)/4, (rect.bottom - rect.top)/3, "Dest:", 5); TextOut{hdc, {rect.right - rect.left)/3, (rect.bottom - rect.top)/3, dst, strlen(dst)); TextOut(hdc, (rect.right - rect.left)/4, (rect.bottom - rect.top)/3 + 30, "Offset:", 7); TextOut(hdc, (rect.right - rect.left)/3, {rect.bottom - rect.top)/3 + 30, buf, ioff); ReleaseDC(hWnd, hdc); break; Для вывода текста в клиентскую область окна используется функция тех tout. В качестве первого параметра она получает дескриптор контекста устройства отображения для рисования на экране дисплея. Дескриптор кон- контекста возвращается функцией GetDC. Чтобы выводимый текст попадал в рабочую область окна, желательно полу- получить КООрДИНаТЫ ЭТОЙ Области С ПОМОЩЬЮ ФУНКЦИИ GetClientRect. Для форматирования вывода целочисленной переменной off на экран ис- используется функция sprintf. Прототип этой функции описан в файле stdio.h, поэтому в раздел деклараций функции winMain необходимо вклю- включить запись: #include <stdio.h> Окно приложения будет лучше смотреться, если заменить стандартный бе- белый цвет фона на серый: wcex.hbrBackground = (HBRUSH)GetStockObj ect(GRAY_BRUSH); Полный исходный текст приложения приведен в листинге 6.9.
Глава 6. Особенности разработки и применения подпрограмм на ассемблере 183 ; Листинг Й.«. Программа на О*-, отображающая подстроку #include "stdafx.h" #include "Return Part of String in C.NET.h" #define MAX_LOADSTRING 100 #include <stdio.h> HINSTANCE hlnst; TCHAR szTitle[MAX_LOADSTRING]; TCHAR szWindowClass[MAX_LOADSTRING]; // Ссылки на функции, определенные в этом модуле ATOM MyRegisterClass(HINSTANCE hinstance); BCOL Initlnstance(HINSTANCE, int); LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); LRESULT CALLBACK About(HWND, UINT, WPARAM, LPARAM); extern "C" char* _stdcall strpart(char *ps, int off); int APIENTRY _tWinMain(HINSTANCE hinstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow) { MSG msg; HACCEL hAccelTable; LoadString(hinstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING); LoadString(hinstance, IDC_RETURNPARTOFSTRINGINCNET, szWindowClass, MAX_LOADSTRING); MyRegisterClass(hinstance); if ( UnitInstance(hinstance, nCmdShow)) { return FALSE; hAccelTable = LoadAccelerators(hinstance, (LPCTSTR)IDC RETURNPARTOFSTRINGINCNET);
184 Часть II. Интерфейс с языками высокого уровня while (GetMessage(&msg, NULL, 0, О)) { if (ITranslateAccelerator(msg.hwnd, hAccelTable, &msg)) { TranslateMessage(&msg); DispatchMessage(&msg); } } return (int)msg.wParam; } ATOM MyRegisterClass(HINSTANCE hlnstance) { WNDCLASSEX wcex; wcex.cbSize = sizeof(WNDCLASSEX); wcex.style = CS_HREDRAW | CS_VREDRAW; wcex.lpfnWndProc = (WNDPROC)WndProc; wcex.cbClsExtra = 0; wcex.cbWndExtra = 0; wcex.hlnstance = hlnstance; wcex.hlcon = Loadlcon(hlnstance, (LPCTSTR)IDI_RETURNPARTOFSTRINGINCNET); wcex.hCursor = LoadCursor(NULL, IDC_ARROW); wcex.hbrBackground = (HBRUSH)GetStockObject(GRAY_BRUSH); wcex.lps zMenuName = (LPCTSTR)IDC_RETURNPARTOFSTRINGINCNET; wcex.lpszClassName = szWindowClass; wcex.hlconSm = Loadlcon(wcex.hlnstance, (LPCTSTR)IDI_SMALL); return RegisterClassEx(Swcex); } BOOL Initlnstance(HINSTANCE hlnstance, int nCmdShow) { HWND hWnd; hlnst = hlnstance; hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hlnstance, NULL);
Глава 6. Особенности разработки и применения подпрограмм на ассемблере 185 if (IhWnd) { return FALSE; } ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd); return TRUE; LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { int wmld, wmEvent; PAINTSTRUCT ps; HDC hdc; RECT rect; char src[] » "STRING1 STRING2 STRING3 STRING4 STRING5"; char *dst; int off, ioff; char buf[4]; switch (message) { case WM_COMMAND: wmld = LOWORD(wParam) ; wmEvent = HIWORD(wParam); switch'(wmld) { case IDM_ABOUT: DialogBox(hlnst, (LPCTSTR)IDD_ABOUTBOX, hWnd, (DLGPROC)About); break; case IDM_EXIT: DestroyWindow(hWnd); break; case ID_PartStr: hdc = GetDC(hWnd); GetClientRect(hWnd, &rect);
186 Часть II. Интерфейс с языками высокого уровня off = 10; dst = strpart(src, off); ioff = sprintf(buf,"Id",off); TextOut(hdc, (rect.right - rect.left)/4, (rect.bottom - rect.top)/4, "Source:", 7); TextOut(hdc, (rect.right - rect.left)/3, (rect.bottom - rect.top)/4, src, strlen(src)); TextOut(hdc, (rect.right - rect.left)/4, (rect.bottom - rect.top)/3, "Dest:", 5); TextOut(hdc, (rect.right - rect.left)/3, (rect.bottom - rect.top)/3, dst, strlen(dst)); TextOut(hdc, (rect.right - rect.left)/4, (rect.bottom - rect.top)/3 + 30, "Offset:", 7); TextOut(hdc, (rect.right - rect.left)/3, (rect.bottom - rect.top)/3 + 30, buf, ioff); ReleaseDC(hWnd, hdc); break; default: return DefWindowProc(hWnd, message, wParam, lParam); } break; case WMJPAINT: hdc = BeginPaint(hWnd, &ps); EndPaint(hWnd, &ps); break; case WM_DESTROY: PostQuitMessage@); break; default: return DefWindowProc(hWnd, message, wParam, lParam); } return 0; LRESULT CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) { switch (message)
Глава 6. Особенности разработки и применения подпрограмм на ассемблере 137 { case WM_INITDIALOG: return TRUE; case WM_COMMAND: if (LOWORD(wParam) == IDOK I| LOWORD(wParam) == IDCANCEL) { EndDialog(hDlg, LOWORD(wParam)); return TRUE; } break; } return FALSE; } На рис. 6.4 изображен вид окна работающего приложения. Рис. 6.4. Окно приложения, выполняющего отображение части строки из программы на C++ .NET Следующий пример демонстрирует, как можно найти элемент строки. Ас- Ассемблерная функция передает в вызывающую программу порядковый номер первого встретившегося элемента строки, если таковой найден, или 0 в слу- случае неудачи. Параметрами функции являются адрес строки и символ для поиска. Исходный текст ассемблерной функции (назовем ее charpos) представлен в листинге 6.10.
188 ' Лист И! ir 5.10. Фум кциу поиска си; Часть II. Интерфейс с языками высокого уровня у; зол а о с! роке ; ; charpos. asm- .686 .model flat public _charpos@8 .data .code _charpos@8 proc push EBX push EBP mov EBP, ESP mov EBX, DWORD PTR [EBP+12] xor EAX, EAX mov AL, BYTE PTR [EBP+16] mov ECX, 1 next check: emp je emp jne jmp quit: mov pop pop ret inc_cnt inc inc jmp not_found: xor ECX, ECX jmp quit _charpos@8 endp end AL, [EBX] quit BYTE PTR [EBX], 0 inc_cnt not found EAX, ECX EBP EBX ECX EBX next check Функция charpos в качестве параметров принимает адрес строки по адресу, находящемуся в [евр+12] и символ для поиска в [евр+16]. Первый элемент
Глава 6. Особенности разработки и применения подпрограмм на ассемблере 189 строки имеет номер 1, поэтому счетчик позиции инициализируется этим значением: mov ECX, 1 Адрес строки помещается в регистр евх, а символ для поиска — в регистр al. Далее символ по адресу в регистре евх сравнивается с содержимым al и, в зависимости от результата сравнения, выполняется переход на нужную ветвь: стар je cmp jne imp AL, [ЕВХ] quit BYTE PTR [ЕВХ], О inc_cnt not found Если искомый символ найден, в счетчик есх записывается его порядковый номер, если же последним элементом строки является ноль, т. е. обнаружен конец строки, содержимое есх становится равным о. Если символ не найден и строка еще не закончилась, регистры евх и есх инкрементируются, после чего цикл поиска повторяется с метки nextcheck: jmp next_check Процедура возвращает значение, как обычно, в регистре еах и освобождает стек командой ret 8. В качестве шаблона для C++ приложения выберем диалоговое окно. Размес- Разместим на главной форме приложения три элемента редактирования Edit control, три элемента staticText и кнопку Button. Свяжем с элементами редактиро- редактирования Source и Character переменные src и csrc типа cstring, а с элементом редактирования Number — переменную iPos целочисленного типа. Напишем обработчик события для нажатия кнопки Button (листинг 6.11). void GetNumberOfCharinStringforCNETDlg::OnBnClickedButtonl() { // TODO: Add your control notification handler code here CString si; CString cl; char *pcl; UpdateData(TRUE); si = src; cl = cSrc;
190 Часть II. Интерфейс с языками высокого уровня pel - cl.GetBuffer(8); ( iPos = charpos(sl.GetBufferA6), *pcl); UpdateData(FALSE); Если символ найден, в поле редактирования Number будет выведен его по- ряковый номер, иначе выводится 0. На рис. 6.5 представлено окно работающего приложения. fiet Number Of Chat In siflni for OiEi (Ц * .¦ ... i... .. Рис. 6.5. Окно приложения, выполняющего поиск позиции символа в строке Следующий пример демонстрирует поиск максимального элемента в масси- массиве вещественных чисел и отображение его на экране. Размер массива поло- положим равным 9. Разработаем классическое процедурно-ориентированное приложение в Visual C++ .NET. В таком приложении обычно присутствуют два взаимосвязанных фрагмента кода: главная процедура winMain, регистри- регистрирующая класс окна и выполняющая все функции по инициализации экзем- экземпляра окна приложения, и функция обратного вызова (оконная процедура). Поиск максимального элемента в массиве вещественных чисел выполняет ассемблерная функция maxreal (листинг 6.12). Л и ст и и г В 12, О у и кц и у<.. о ы п о л н я ю ща ^ л о иск ч а к си м а ль н о го эу\ с ?,i e и т о а миссий с ооще стае иных чк .686 .model flat public _maxreal@8 .data MAXREAL DD 0 .code maxreal.asm
Глава 6. Особенности разработки и применения подпрограмм на ассемблере 191 _jnaxreal@8 proc. push push mov mov mov mov finit fid NEXT_CMP: add fcom fstsw sahf jnc fid EBX EBP EBP,ESP EBX, DWORD PTR [EBP+12] EDX, DWORD PTR [EBP+16] ECX, 1 DWORD PTR [EBX] EBX, 4 DWORD PTR [EBX] AX CHECK_INDEX DWORD PTR [EBX] CHECK_INDEX:' cmp je inc jmp FIN: fwait fstp mov pop pop ret ECX, EDX FIN ECX NEXT_CMP DWORD PTR MAXREAL EAX, offset MAXREAL EBP EBX 8 _maxreal@8 endp end В функции используются команды математического сопроцессора. Для из- извлечения параметров используется регистр евр. Адрес массива передается в [евр+12], а размер массива— в [евр+16]. Текущее значение максимума функция сохраняет в локальной переменной maxreal. После обработки массива адрес максимального элемента передается в ре- регистр еах: fstp DWORD PTR MAXREAL mov EAX, Offset MAXREAL
192 Часть II. Интерфейс с языками высокого уровня Воспользуемся мастером приложений и разработаем обычное 32-разрядное Windows-приложение, использующее результат выполнения ассемблерной функции. Исходный текст процедуры winMain и функции обратного вызова представлен в листинге 6.13. #include "stdafx.h" #include "Find Max Value in Array of Reals.h" #define MAX_LOADSTRING 100 // Глобальные переменные HINSTANCE hlnst; TCHAR s zT i 11e[MAX_LOADSTRING]; TCHAR szWindowClass[MAX_LOADSTRING]; // Объявление функций этого модуля ATOM MyRegisterClass(HINSTANCE hlnstance); BOOL InitInstance(HINSTANCE, int); LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); LRESULT CALLBACK About(HWND, UINT, WPARAM, LPARAM); extern "C" float* _stdcall maxreal(float *px, int sx); int APIENTRY _tWinMain(HINSTANCE hlnstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow) { MSG msg; HACCEL hAccelTable; LoadString(hlnstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING) LoadString(hlnstance, IDC_FINDMAXVALUEINARRAYOFREALS, szWindowClass, MAX_LOADSTRING); MyRegisterClass(hlnstance);
Глава 6. Особенности разработки и применения подпрограмм на ассемблере 193 // Инициализвция приложения if (!Initlnstance (hlnstance, ndndShow)) { return FALSE; } hAccelTable = LoadAccelerators(hlnstance, (LPCTSTR)IDC_FINDMAXVALUEINARRAYOFREALS); // Цикл обработки сообщений while (GetMessage(&msg, NULL, 0, 0)) { if (!TranslateAccelerator(msg.hwnd, hAccelTable,- &msg)) { TranslateMessage(&msg); DispatchMessage(&msg); } } return (int) msg.wParam; } //Функция регистрации класса окна ATOM MyRegisterClass(HINSTANCE hlnstance) { WNDCLASSEX wcex; wcex.cbSize = sizeof(WNDCLASSEX); wcex.style = CS_HREDRAW | CS_VREDRAW; wcex.lpfnWndProc = (WNDPROC)WndProc; wcex.cbClsExtra = 0; wcex.cbWndExtra = 0; wcex.hlnstance = hlnstance; wcex.hlcon = Loadlcon(hlnstance, (LPCTSTR)IDI_FINDMAXVALUEINARRAYOFREALS); wcex.hCursor = LoadCursor(NULL, IDC_ARROW); wcex.hbrBackground = (HBRUSH)GetStockObject(GRAY_BRUSH);
194 Часть II. Интерфейс с языками высокого уровня wcex.lpszMenuName = (LPCTSTR)IDC_FINDMAXVALUEINARRAYOFREALS; wcex.lpszClassName = szWindowClass; wcex.hlconSm = Loadlcon(wcex.hlnstance, (LPCTSTR)IDI_SMALL); return RegisterClassEx(Swcex); BOOL Initlnstance(HINSTANCE hlnstance, int nCmdShow) { HWND hWnd; hlnst = hlnstance; hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hlnstance, NULL) if UhWnd) { return FALSE; ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd); return TRUE; } LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { int wmld, wmEvent; PAINTSTRUCT ps; HDC hdc; char buf[16]; float xarray[9] » {12.43, 93.54, -23.1, 23.59, 16.09, 10.67, -54.7, 11.49, 98.06); float *xres; int cnt; switch (message) { case WM_COMMAND: wmld = LOWORD(wParam);
Глава 6. Особенности разработки и применения подпрограмм на ассемблере 195 winEvent = HIWORD(wParam) ; switch (wmld) { case IDM_ABOUT: DialogBox(hlnst, (LPCTSTR)IDD_ABOUTBOX, hWnd, (DLGPROC)About); break; case IDM_EXIT: DestroyWindow(hWnd); break; default: return DefWindowProc(hWnd, message, wParam, lParam); break; case WM_PAINT: hdc = BeginPaintr(hWnd, &ps) ; // Здесь находится код нашего обработчика TextOut(hdc, 30, 80, "ARRAY: ", 7); for (cnt = 0; cnt < 9; cnt++) { gcvt(xarray[cnt], 6, buf); TextOut(hdc, 100 + cnt*50, 80, buf, 5); } TextOut(hdc, 30, 100, "MAXIMUM: ", 9); xres = maxreal(xarray, 9); gcvt(*xres, 5, buf); TextOut(hdc, 220, 100, (LPCTSTR)buf, 5); EndPaint(hWnd, &ps); break; case WM_DESTROY: PostQuitMessage@); break; default: return DefWindowProc(hWnd, message, wParam, lParam)
196 Часть И. Интерфейс с языками высокого уровня return 0; LRESULT CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { case WM_INITDIALOG: return TRUE; case WM_COMMAND: if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL) { EndDialog(hDlg, LOWORD(wParam)); return TRUE; } break; } return FALSE; Программа выводит в рабочую область окна приложения две строки. Одна из них отображает все элементы массива, а другая, расположенная ниже, максимальный элемент. Вывод на эран выполняет обработчик сообщения ,WM_PAINT С ПОМОЩЬЮ фуНКЦИИ TextOut. В функции обратного вызова WndProc определим следующие переменные: char buf[16]; float xarray[9] = {12.43, 93.54, -23.1, 23.59, 16.09, ГО.67, -54.7, 11.49, 98.06}; float *xres; int cnt; Строка buf используется для хранения результата преобразования вещест- вещественного числа в текст, массив вещественных чисел хаггау определяет после- последовательность из 9 элементов. Кроме этого, определен указатель веществен- вещественного типа xres и счетчик цикла cnt для вывода всех 9 элементов на экран. Вывод строк на экран в обработчике wmpaint определяется программным кодом (листинг 6.14).
Глава 6. Особенности разработки и применения подпрограмм на ассемблере 197 ¦¦ Листинг 6.14. Зыоод строк bi TextOut(hdc, 30, 80, "ARRAY: ", 7); for (cnt = 0; cnt < 9; cnt++) { gcvt(xarray[cnt], 6, buf); TextOut(hdc, 100 + cnt*50, 80, buf, 5); } TextOut(hdc, 30, 100, "MAXIMUM: ", 9); xres = maxreal(xarray, 9); gcvt(*xres, 5, buf); TextOut(hdc, 220, 100, (LPCTSTR)buf, 5); Первая строка листинга — это функция TextOut, принимающая в качестве параметров контекст устройства (hdc), горизонтальную и вертикальную ко- координаты в клиентской области окна, указатель на строку текста и количе- количество элементов для вывода. Для вывода чисел на экран необходимо преобразовать массив в последова- последовательность строк. Преобразование вещественного числа в последовательность символов можно выполнить при помощи функции gcvt, принимающей в качестве параметров вещественное число, количество знаков для вывода и указатель на символьный буфер для хранения результата преобразования. Цикл for мы используем для вывода на экран всех 9 элементов массива. Аналогично выполняется и вывод максимального элемента массива на эк- экран. Но перед этим нужно вызвать функцию maxreal: xres = maxreal(xarray, 9) ; Поскольку xres — указатель, то для правильной работы функции gcvt ее вызов выглядит так: gcvt(*xres, 5, buf); После выполнения этого оператора максимум помещается в переменную buf и отображается на экране функцией тех tout. И последнее. Необходимо включить объявление ассемблерной функции в исходный текст программы: extern "С" float* _stdcall maxreal(float *px, int sx); Окно работающего приложения показано на рис. 6.6. Важнейшими элементами языка C++ .NET являются такие типы данных, как структуры и объединения. Структуры представляют собой массивы или
198 Часть II. Интерфейс с языками высокого уровня векторы, состоящие из тесно связанных элементов, но, в отличие от масси- массивов или векторов, могут содержать данные различных типов. Рис. 6.6. Окно приложения, выполняющего поиск максимума в массиве вещественных чисел Объединения являются близкими к структурам типами данных. С помощью объединений можно сохранять данные различных типов в одном отрезке системной памяти. Структуры и объединения положены в основу большин- большинства программ табличных вычислений и баз данных. Применение ассемб- ассемблерных функций при работе с такими типами данных помогает ускорить как работу приложений, так и уменьшить загрузку операционной системы в це- целом. Сейчас я покажу, как можно работать со структурами и объединения- объединениями, используя функции из отдельно скомпилированных ассемблерных мо- модулей. Начну со структур. Структура объявляется при помощи ключевого слова struct. В следующем примере будет использоваться структура intstr, в которой определены три поля целых чисел: struct intstr { int il; int i2; ¦¦¦« int i3; При объявлении структуры никакая переменная не создается, определяется только вид данных, которые эта структура содержит. Чтобы объявить пере- переменную (назовем ее ist) типа intstr, необходимо записать строку: struct intstr ist
Глава 6. Особенности разработки и применения подпрограмм на ассемблере 199 Переменная ist является экземпляром структуры intstr. Для манипуляций с элементами структуры удобно использовать указатель на структуру. Рас- Рассмотрим пример, в котором элемент ±2 структуры intstr будет инвертиро- инвертироваться в ассемблерной процедуре. Исходный текст процедуры приведен в листинге 6.15. negint.asm .686 .model flat public _negint@4 .code _negint@4 proc push EBP mov EBP, ESP mov EAX, DWORD PTR [EBP+8] neg EAX pop EBP ret 4 _negint@4 endp end Процедура принимает один параметр — целое число — и возвращает в реги- регистре еах его инверсию. Исходный текст консольного приложения, исполь- использующего эту процедуру, приведен в листинге 6.16. \ ш I Листинг tf \о. Консольное приложение, демонстрирующее о о рас от к у ] э«;шентоо структуры в процедуре ну ассемблере // This is the main project file for VC++ application project // generated using an Application Wizard. #include "stdafx.h" fusing <mscorlib.'dll> extern "C" int _stdcall negint(int il); using namespace System;
200 Часть //. Интерфейс с языками высокого уровня int _tmain() { // TODO: Please replace the sample code below with your own. struct intstr { int il; int i2; int i3; struct intstr ist, *pist; pist = &ist; String *s; Console::Write("Enter 12: "); s = Console::ReadLine(); pist->i2 = Convert::ToInt32(s); pist->i2 .= negint(pist->i2); s = Convert::ToString(pist->i2); Console::WriteLine("New value of i2 = {0}", s); Console::WriteLine("Press any key to exit..."); Console::ReadLine(); return 0; Структура intstr содержит три целочисленных элемента il - i3. В строках struct intstr ist, *pist; pist = &ist; объявляются переменная ist и указатель pist на структуру. Указателю pist присваивается адрес экземпляра ist структуры. Для доступа к элементу структуры, например, к il, можно использовать одно из выражений ist.il pist->il Инвертирование элемента i2 выполняется с помощью оператора pist->i2 = negint(pist->i2);
Глава 6. Особенности разработки и применения подпрограмм на ассемблере 201 При использовании внешнего ассемблерного модуля необходимо объявить функцию, находящуюся в нем, как внешнюю: extern "С" int _stdcall negint(int il) ; Вид окна приложения показан на рис. 6.7. Рис. 6.7. Окно приложения, отображающего работу с элементами структуры Рассмотрим применение ассемблерных функций для манипуляций с эле ментами объединений. Будем использовать объединение union test_union { int i ; char с; состоящее из двух элементов. Объявление объединения не создает никаких переменных, поэтому для создания экземпляра переменной-объединения нужно выполнить, например, оператор union test_union tu, *ptu; Компилятор выделяет для объединения столько памяти, чтобы в ней помес- поместился самый большой элемент. В нашем случае будет выделено 4 байта. Объединение дает возможность интерпретировать один и тот же набор би- битов несколькими способами. Рассмотрим пример, в котором ассемблерная функция в одном случае обрабатывает символ, а в другом — целое число. Исходный текст ассемблерной функции (назовем ее иех), приведен в лис- листинге 6.17. о 17. Функции, ывпкзщля элементы ооьединенин uex.asm- .686
202 Часть II. Интерфейс с языками высокого уровня .model flat public .code _uex@8 push mov mov cmp je cmp je xor jrop int_exec mov neg jmp _uex@8 proc EBP EBP, EDX, EDX, int_ EDX, chai EAX, ex : EAX, EAX ex char_exec: xor mov cmp jb cmp ja sub ex: pop ret _uex@8 end EAX, AL, AL, ex AL, ex AL, EBP 8 ESP DWORD PTR [EBP+12] 0 exec 1 :_exec EAX DWORD PTR [EBP+8] EAX BYTE PTR [EBP+8] 97 122 32 endp ;загрузка параметра-индикатора в EDX ;1-й параметр — целое? ;если да, инвертировать число ;1-й параметр — символ? ;если да, перевести в верхний регистр ;2-й параметр вне диапазона, /записываем в ЕАХ 0 и выходим из функции ;помещаем в ЕАХ первый параметр как ;целое и инвертируем его ;обнуление ЕАХ /помещаем символ в регистр AL ;и анализируем его ;если символ алфавитный, /преобразуем его к верхнему ;регистру Функция uex в качестве параметров принимает два целых числа. Второй па- параметр, находящийся по адресу [евр+12], может принимать либо 1, либо 0. Он служит индикатором для определения типа первого параметра, переда- передаваемого в [ЕВР+8]. Если второй параметр равен 1, то первый параметр явля- является целочисленным значением, а если он равен 0, то первый параметр — символ.
Глава 6. Особенности разработки и применения подпрограмм на ассемблере 203 Соответственно, первый параметр обрабатывается разными способами, в за- зависимости от типа. Если это целое число, то функция иех возвращает его ин- инверсию, если символ, то выполняется преобразование к верхнему регистру. Разработаем в C++ .NET приложение на основе диалогового окна. Размес- Разместим на главной форме приложения два элемента редактирования (Edit control), две текстовые метки (static Text) и кнопку Button. Свяжем с одним из элементов редактирования переменную iEdit целого типа и пере- переменную символьного типа cEdit — с другим. В обработчике нажатия кноп- кнопки будем выводить значения переменных в поля редактирования. Исходный текст обработчика на C++ вместе с объявлением внешней функции представлен в листинге 6.18. Л и с т и н г 6.18 Основ и ь i с ф и a i м в н i ь i npot u a v м ь; i -t з С ++, в к о го рой и с г. о: ¦ ь з у с тс я ф у н к ц и и и в у. // Union_in_ASM_procDlg.срр : implementation file #include "stdafx.h" #include "Union_in_ASM_j>roc. h" #include "Union_in_ASM_jjrocDlg.h" #include ".\union_in_asm_procdlg.h" extern "C" int _stdcall uex(int il, int id); void CUnion_in_ASM_procDlg::OnBnClickedButtonl() { // TODO: Add your control notification handler code here union test__union { int i; char c; union test_union tu, *ptu ptu = &tu; ptu->i = -56; iEdit = uex(ptu->i, 0);
204 Часть II. Интерфейс с языками высокого уровня ptu->c = 'г'; cEdit = (char)uex(ptu->c, 1); UpdateData(FALSE); В основной программе используется объединение testunion. После ини- инициализации выполняем последовательные присваивания элементам объеди- объединения соответственно числового значения -56 и символа 'г1. Функция иех обрабатывает эти элементы в соответствии с их типом. Для числового зна- значения -56 выполняются операторы ptu->i = -56; iEdit = uex(ptu->i, 0); Если элемент объединения — символ ' г •, то выполняются операторы ptu->c = 'г1; cEdit = (char)uex(ptu->c, 1); В остальном исходный текст программы несложен и в пояснениях не нуж- нуждается. Вид окна работающего приложения показан на рис. 6.8. Рис. 6.8. Окно приложения, отображающего манипуляции с элементами объединения На этом мы закончим рассмотрение интерфейсов отдельно скомпилирован- скомпилированных модулей на языке ассемблера с программами на Visual C++ .NET. Все примеры из этой главы могут быть легко модифицированы читателями для использования в собственных разработках.
Глава 7 Компоновка ассемблерных модулей с программами на C++ .NET В этой главе будут подробно рассмотрены вопросы компоновки и выполне- выполнения программ, написанных на C++ .NET и включающих в себя ассемблер- ассемблерные модули. Существует много вариантов компоновки программ на C++ и ассемблере, я же попробую выделить ключевые аспекты этого процесса. Приложение на C++ может включать в себя один или несколько ассемб- ассемблерных модулей. Модули представляют собой или файлы с расширением obj, или стандартные библиотеки (файлы с расширением lib). В этих моду- модулях могут содержаться вызовы функций из других модулей. Более того, ас- ассемблерные функции, в свою очередь, могут ссылаться на библиотечные функции C++ .NET. Программы обычно действительно используют отдель- отдельно скомпилированные объектные файлы или библиотеки, поэтому задача построения законченного приложения требует от разработчика определен- определенных усилий. В чем преимущества применения объектных файлов, сгенери- сгенерированных с помощью автономных компиляторов? Разработанные объектные файлы, как правило, требуют минимума памяти и других системных ресур- ресурсов, что является очень ценным при разработке быстрых приложений. Мы будем рассматривать компоновку ассемблерных модулей с профаммой на C++ .NET в предположении, что для разработки таких модулей исполь- используется автономный компилятор ассемблера. В качестве такого компилятора можно использовать либо ML.EXE пакета профамм MASM 6.14, либо ком- компилятор ML среды профаммирования C++ .NET, находящийся в подката- подкаталоге \bin рабочего каталога C++ .NET. Компиляция файлов с расширением asm либо выполняется из командной строки, либо является составной частью процесса генерации исполняемого файла приложения. Принципиальных отличий между этими методами ком- компиляции не существует, хотя для удобства отладки и трассировки приложе- приложения желательно настроить компилятор ассемблера для использования в самой среде профаммирования. В обоих случаях мы получим файл объект- объектного модуля и можем его использовать.
206 Часть II. Интерфейс с языками высокого уровня Несколько слов о форматах получаемых объектных модулей. Компоновщик (линкер) LINK оперирует с OBJ-файлами, имеющими либо формат COFF, либо формат OMF. Компилятор Microsoft Visual C++ .NET генерирует объ- объектный файл, имеющий формат COFF. Компоновщик LINK преобразовывает OMF к типу COFF автоматически. Следует учитывать то, что существуют определенные ограничения, иногда препятствующие преобразованию объектных файлов типа OMF в COFF. Некоторые различия в структурах файлов этих типов могут препятствовать процессу преобразования. Чтобы избежать ошибок, необходимо задавать для компоновщика LINK в качестве входных объектные файлы формата COFF. Мы часто будем использовать командную строку ML /с /coff <файл с расширением ASM> для получения COFF-файла. Таким образом, для того, чтобы использовать файл, содержащий какие-либо полезные функции, в приложении, написанном на C++ .NET, необходимо выполнить два шага: 1. Откомпилировать исходный файл с расширением asm для получения файла объектного модуля с расширением obj и желательно в формате COFF. 2. Включить полученные OBJ-файлы в состав проекта, и выполнить генера- генерацию приложения с помощью компоновщика. Второй шаг может включать несколько этапов и реализуется несколькими способами, поскольку объектные файлы можно подключать к приложению по-разному. Вот некоторые варианты интеграции: П можно добавить файлы объектных модулей в проект, указав используе- используемые функции в разделе деклараций; ? можно объединить объектные файлы в статические библиотеки объект- объектных модулей. Для подобных операций используется утилита LIB.EXE из пакета Visual Studio C++ .NET. Полученный в результате сборки файл библиотеки с расширением lib можно включить в состав проекта; ? можно использовать библиотеки динамической компоновки (DLL), со- содержащие объектные файлы; ? конечно, можно использовать и комбинацию этих методов. Рассмотрим более подробно, какие опции компилятора ML.EXE использу- используются для генерации объектных файлов. Например, чтобы получить объект- объектный модуль из файла myproc.asm, необходимо выполнить команду ML /с /coff myproc.asm Предполагается, что файл с исходным текстом находится в том же каталоге, что и ML.EXE.
Глава 7. Компоновка ассемблерных модулей с программами на C++ .NET 207 Опция /с предписывает компилятору создать только объектный файл. По умолчанию, если компилятор не обнаружит ошибок, будет создан файл myproc.obj в формате COFF. Полученный объектный модуль включается в основное приложение на C++ .NET и используется для вызова находящихся в нем функций. Файл объектного модуля может содержать несколько функций, причем эти функции могут использовать общие данные и вызывать другие функции из этого же модуля. При относительно небольшом числе функций, переменных и связей между ними вариант с использованием одного или нескольких объектных модулей может быть наиболее приемлемым. В большинстве случаев наиболее удобным способом организации и исполь- использования функций из внешних модулей является генерация библиотеки (стандартной или импорта). В этом случае компилятор C++ .NET обладает значительно большими возможностями по оптимизации приложения. Кро- Кроме того, очень удобно содержать программный код из нескольких объект- объектных модулей в виде одной библиотеки. Файлы библиотечных модулей име- имеют расширение lib. Для создания библиотеки, например, из объектного файла myproc.obj можно воспользоваться утилитой LIB.EXE из пакета C++ .NET: LIB /OUT:myproc.lib myproc.obj Если нет ошибок, будет создан файл myproc.lib, который необходимо вклю- включить в проект приложения. Очень широко используется вариант применения ассемблерных функций, размещенных в библиотеках динамической компоновки. Этот вариант обла- обладает большой гибкостью и позволяет легко тиражировать DLL, а также ис- использовать различные варианты привязки DLL к приложению. Рассмотрим в деталях компоновку приложения и ассемблерных модулей. НаЧнем с использования объектных модулей. Ассемблерный модуль компи- компилируется с помощью-макроассемблера MASM 6.14 или компилятора ассемб- ассемблера среды Visual C++ .NET. Любая программа на ассемблере из наших примеров начинается с директив .686 .model flat, С Директива .686 разрешает ассемблеру компилировать все команды процес- процессора Pentium Pro и выше. Директива .model flat, с определяет модель памяти, используемой при- приложением, а также соглашение о вызовах (в данном случае qdeci). Рассмотрим следующий пример. Пусть необходимо найти разность двух ве- вещественных чисел и вывести результат на экран результат. Вычисление раз-
208 Часть II. Интерфейс с языками высокого уровня ности будет выполнено с помощью ассемблерной функции (назовем ее subf 2). Исходный текст функции поместим в файл sub2.asm. Предположим, что наша функция subf2 вызывается с использованием со- соглашения _cdeci. Исходный текст функции несложен (листинг 7.1). subf2.asm .686 .model flat, С .code subf2 proc push EBP mov EBP, ESP finit fid DWORD PTR [EBP+8] ;загрузить число fl в ST@) fsub DWORD PTR [EBP+12] ; вычесть из fl число f2 fwait pop EBP ret subf2 endp end Проведем краткий анализ исходного текста. Действие начальных директив было описано ранее. Пролог функции реализован следующими командами ассемблера push EBP mov EBP, ESP Они инициализируют регистр евр значением адреса стека, чтобы получить доступ к переменным. Вычисления выполняются с помощью команд мате- математического сопроцессора, а результат возвращается в вершине стека со- сопроцессора st(о): fid DWORD PTR [EBP+8] fsub DWORD PTR [EBP+12] Последние две команды выполняют выравнивание стека и выход из функ- функции. Обратите внимание на то, что команда ret выполняется без парамет- параметров, Как И ДОЛЖНО быТЬ ДЛЯ ВЫЗОВа _cdecl.
Глава 7. Компоновка ассемблерных модулей с программами на C++ .NET 209 Теперь нам нужно получить объектный модуль для нашего основного при- приложения на C++ .NET и включить его в состав проекта. Сгенерировать объ- объектный модуль можно одним из двух способов: либо с помощью автоном- автономного компилятора MASM 6.14, либо с использованием компилятора ассемблера среды Visual C++ .NET. Опишем эти два варианта отдельно. Если используется автономный компи- компилятор MASM 6.14, то, как уже говорилось, получить объектный модуль можно с помощью командной строки ML.EXE /с /coff sub2.asm Разработаем консольное приложение C++ .NET, в котором применяется функция subf2 из файла sub2.obj. Исходный текст программы приведен в листинге 7.2. // USING_STANDALONE_ASM_COMPILER.cpp : Defines the entry point for the // console application. #include "stdafx.h" extern "C" float subf2(float fl, float f2); int _tmain(int argc, _TCHAR* argv[]) { float fl,f2; printf("CUSTOM BUILD WITH ASM-FILE IN PROJECT DEMO\n\n"); printf("Enter float fl: ") ; scanf("%f", &fl); printf("Enter float f2: "); scanf("%f", &f2); float fsub = subf2(fl, f2) ; printf("fl - f2 = %.2f\n", fsub); getchar(); return 0; Проведем краткий анализ листинга. В окне консольного приложения вво- вводятся два вещественных числа f i и f2. Разность этих чисел находится с по-
210 Часть II. Интерфейс с языками высокого уровня мощью функции subf2. Функция является внешней по отношению к ис- исполняемому модулю, поэтому необходимо объявить ее с директивой extern: extern "С" float subf2(float fl, float f2) По умолчанию используется соглашение о вызовах cdeci. Функция subf2 принимает в качестве параметров две переменные fl и f2 вещественного типа и возвращает их разность. Спецификатор "С" запрещает декорирование имени функции. Оператор float fsub = subf2(fl, f2) вычисляет разность переменных fl и f2, помещая результат в переменную fsub. Объектный файл, содержащий функцию subf2, необходимо включить в со- состав проекта. Для этого следует в пункте главного меню Project выбрать оп- опцию Add Existing Item и далее в открывающемся диалоговом окне выбрать имя включаемого модуля (рис. 7.1). • .- ¦ ¦ ¦¦ ¦¦_¦¦¦ ... .. :¦ ..j • i i ¦ ! 14 ¦ .t \-'., ¦-.'л--; л| ••>.:. Рис. 7.1. Включение объектного модуля в состав проекта После включения объектного модуля в проект в списке файлов в Solution Explorer должен появиться sub2.obj. Следует заметить, что включение от-
Глава 7. Компоновка ассемблерных модулей с программами на C++ .NET 211_ дельного модуля в проект еще не означает, что все функции, размещенные в этом модуле, становятся видимыми для приложения, связанного с этим проектом. Необходимо указывать функции из включаемых модулей с дирек- директивой extern. Включение модуля в проект выполняется, как обычно, через пункт меню Project. При этом необходимо выбрать опцию Add Existing Item. В раскры- раскрывающемся диалоговом окне необходимо выбрать файл объектного модуля. После включения файла объектного модуля в проект имя файла появляется в списке файлов проекта (рис. 7.2). -'.У- Рис. 7.2. Окно проекта после включения файла объектного модуля Следует обратить внимание на то, что объектный файл sub2.obj включен в проект как файл ресурсов. Сохраним наш проект и перекомпилируем его с помощью опции Rebuild пункта меню Build. После запуска приложения и ввода двух вещественных чисел окно рабо- работающей программы будет выглядеть так, как показано на рис. 7.3. Компоновку ассемблерной функции и вызывающей программы можно вы- выполнить с использованием компилятора MASM среды разработки Visual C++ .NET. Хочу заметить, что макроассемблер C++ .NET аналогичен по своим возможностям автономному компилятору MASM 6.14 и поддерживает те же директивы и команды.
212 Часть II. Интерфейс с языками высокого уровня Рис. 7.3. Окно работающего приложения, вычисляющего разность чисел с помощью отдельно скомпилированного объектного модуля При этом не понадобятся никакие другие инструментальные средства, кро- кроме тех, что есть в C++ .NET 2003! Преимущество такого метода в том, что ассемблерный модуль очень легко редактировать и отлаживать, используя интерфейс, предоставляемый средой Visual C++ .NET 2003. Процесс может показаться вначале слишком сложным, поскольку подоб- подобные примеры практически нигде не описаны, поэтому я буду объяснять каждый шаг. В качестве примера разработаем консольное приложение. Исходный текст приложения разработаем на основе текста, имеющегося в листинге 7.2. Да- Далее необходимо добавить файл с исходным текстом ассемблерной функции в наше приложение. Для этого придется выполнить некоторые дополни- дополнительные действия. Первое, что нужно сделать, — добавить новый текстовый файл в наш про- проект. В этот файл будет помещен исходный текст ассемблерной функции. Создадим такой файл, используя пункт меню Project, где выберем опцию Add New Item. Это показано на рис. 7.4. Выберем тип файла, добавленного в наш проект. В среде программирования Visual C++ .NET нет шаблона для создания файла с расширением asm, по- поэтому можно воспользоваться одним из текстовых шаблонов. Это должен быть текстовый файл. Из имеющихся шаблонов выберем Text File (.txt). Да- Далее укажем имя файла в поле редактирования. Назовем файл sub2.asm. Хочу заметить, что текстовый файл необязательно должен иметь расшире- расширение asm. Ничто не мешает сохранить вновь созданный файл как, например, sub2.txt. Дело в том, что встроенный ассемблер C++ .NET будет обрабаты- обрабатывать любой текстовый файл, который ему передадут. Но вернемся к нашему файлу sub2.asm (рис. 7.5).
Глава 7. Компоновка ассемблерных модулей с программами на C++ .NET 213 \! S ,• S .!'¦¦ : '¦ S - V Рис. 7.4. Добавление нового файла в проект Рис. 7.5. Выбор типа файла и его расширения
214 Часть II. Интерфейс с языками высокого уровня Поместим в пустой файл sub2.asm ассемблерный код для функции sub2f. Сохраним проект. Далее необходимо указать компилятору, как обрабатывать файл с расширением asm. Для этого в Solution Explorer выберем файл sub2.asm и перейдем на закладку Properties (рис. 7.6). Рис. 7.6. Установка опций обработки для файла sub2.asm В открывающейся странице Properties необходимо указать параметры обра- обработки файла sub2.asm. Командную строку встроенного компилятора MASM можно представить так: ML /с /coff sub2.asm Если файл сохранен как txt, то единственное, что нужно изменить в опциях командной строки — это имя файла: ML /с /coff sub2.txt Установка опций компилятора MASM среды C++ .NET показана на рис. 7.7. Командная строка (параметр Command Line) должна иметь вид: ML.EXE /с /coff sub2.asm Параметр Outputs должен содержать имя объектного модуля, в данном слу- случае sub2.obj. Сохраним еще раз проект и откомпилируем его. После запус- запуска приложения окно выглядит так, как показано на рис. 7.8.
Глава 7. Компоновка ассемблерных модулей с программами на C++ .NET 215 Рис. 7.7. Установка параметров компиляции файла sub2.asm ITII ftStt-PlLE IN PROJECT ШЯв tei» flea* fis 17.13 Рис. 7.8. Окно приложения, демонстрирующего "применение ASM-файла в среде C++ .NET Можно сделать некоторые выводы о применении объектных модулей в при- приложении на C++ .NET. Хотя рассмотренные примеры являются не очень сложными, по ним можно судить о преимуществах того или иного метода компиляции и подключения к проекту. Использование среды Visual C++ .NET 2003 для компиляции ASM-файлов и включения их в проект предоставляет разработчику большие удобства. Осо- Особенно это касается этапа отладки приложений, когда неоднократно прихо- приходится перекомпилировать приложение или его отдельные модули, и приме- применение встроенного компилятора дает большой выигрыш во времени.
216 Часть IL Интерфейс с языками высокого уровня Хочу заметить, что для компилятора C++ .NET объектные файлы, сгенери- сгенерированные макроассемблером, независимо от того, как это сделано, являются внешними. Это означает, что все функции, определенные в этих модулях, должны быть объявлены с директивой extern. В ASM-файлах могут содержаться несколько функций, причем одни функ- функции объектного модуля могут вызываться другими из этого же модуля и ис- использовать результаты их выполнения. Немного изменим исходный текст ассемблерного модуля, включив в него программный код функции (назовем ее addioo), прибавляющей к полученному целочисленному параметру 100. В этом случае модифицированный текст будет выглядеть так, как показано в листинге 7.3. ; subf 2. asm .686 .model flat, С . code subf2 proc ;cdecl push EBP mov EBP, ESP finit fid DWORD PTR [EBP+8] /загрузить число fl в ST@) fsub DWORD PTR [EBP+12] ; вычесть из fl число f2 fwait pop EBP ret subf2 endp addlOO proc push EBP mov EBP, ESP mov EAX, DWORD PTR [EBP+8] add EAX, 100 pop EBP ret addlOO endp end
Глава 7. Компоновка ассемблерных модулей с программами на C++ .NET 217 Сохраним исходный текст в файле subO.asm. Разработаем консольное при- приложение, использующее функции subf2 и addioo, и включим файл subO.asm в проект. Исходный текст консольного приложения показан в лис- листинге 7.4. ! Лис?ии> 7.4. Копсои!..ноо прил^ке^-ио. игпопьоую-.доо функции ?¦/¦¦;¦ : :-¦ и ¦.:<¦::'.¦¦'¦¦¦:'¦¦ Ь. ю is^uwj'-'-DOvCi'c лес с-мол op мог о у с дул v; •[ #include <stdio.h> extern "C". float subf2(float fl, float f2);' extern "C" int addlOO(int il) ; void main(void) { float fl,f2; printf("CUSTOM BUILD WITH ASM-FILE BUILT-IN\n\n"); printf("Enter float fl: "); scanf("%f", &fl); printf("Enter float f2: "); scanf("%f", &f2); float fsub = sxibf2(fl, f2); printf("fl - f2 = %.2f\n", fsub); printf("Rounded +100 = %d\n", addlOO((int)(fsub/10))) getchar(); Окно работающего приложения показано на рис. 7.9. Рис. 7.9. Окно приложения, демонстрирующего применение двух функций из включенного ассемблерного модуля
218 Часть II. Интерфейс с языками высокого уровня До сих пор мы предполагали, что вызовы функций из ассемблерного модуля выполняются в соответствии с соглашением _cdeci. Чтобы использовалось соглашение, например, stdcaii, необходимо внести некоторые изменения в файл с исходным текстом ассемблерных функций. Предположим, что функция addioo должна вызываться в соответствии со _stdcaii. В этом случае исходный текст ассемблерного модуля subf2.asm выглядит так, как показано в листинге 7.5 (изменения выделены жирным шрифтом). Л v<<:~i\ н •¦: 7.5, А с <; е; А Ь; i t р я ;¦, з у. ;-, и a ф у и и у и и :, и г. ¦ ¦' ? и .:;¦.:.¦'.'. "¦¦ С С О Г г; n U о '< < и я ? л j * ¦ subf2.asm (вариант 3) .686 .model flat .code _subf2 proc ;cdecl push EBP mov EBP, ESP /• finit fid DWORD PTR [EBP+8] /загрузить число fl в ST@) fsub DWORD PTR [EBP+12] ; вычесть из fl число f2 fwait pop EBP ret _subf 2 endp _addl00@4 proc push mov mov add pop ret EBP EBP, ESP EAX, DWORD PTR [EBP+8] EAX, 100 EBP 4 _addl00@4 endp end
Глава 7. Компоновка ассемблерных модулей с программами на C++ .NET 219_ Обратите внимание: если для всех функций ассемблерного модуля, использу- используется соглашение cdeci, являющееся настройкой по умолчанию, то доста- достаточно указать квалификатор "С" в директиве model и не применять специ- специальную запись для имен функций с символом подчеркивания вначале. Если используется другое соглашение для функций или смешанный вариант, как показано в листинге 7.5, то необходимо явным образом указывать это в за- записи имен функций. В основной программе функциям subf2 и addioo должна соответствовать запись extern "С" float subf2(float fl, float f2) extern "C" int _stdcall addlOO(int il) Использование ассемблерных модулей не ограничивается вызовами -функ- -функций из основной программы на C++ .NET. Для обмена данными могут ис- использоваться и общие переменные. Этот термин позаимствован из более ранних версий компиляторов C++ фирмы Microsoft и не является абсолют- абсолютно точным, но вполне подходит для описания сути происходящего. Сказанное лучше всего пояснить на примере. За основу возьмем разрабо- разработанное консольное приложение и ассемблерный модуль из листинга 7.5. Исходный текст ассемблерного файла изменим таким образом, чтобы мож- можно было использовать общую переменную для получения результатов вы- вычислений. Напомню, что разность двух вещественных чисел возвращалась функцией subf2 в стеке сопроцессора st@) и использовалась в других фрагментах программы. Теперь результат выполнения функции будет помещаться в переменную fres размером в двойное слово и использоваться в основной программе. Исходный текст модифицированного листинга ассемблерного модуля пока- показан в листинге 7.6 (изменения выделены жирным шрифтом). subf2std.asm .686 .model flat public _fres .data _fres DD 0 .code _subf2 proc ;cdecl push EBP mov EBP, ESP
220 Часть II. Интерфейс с языками высокого уровня finit fid DWORD PTR [EBP+8] ;загрузить число fl в ST@) fsub DWORD P-TR [EBP+12] ; вычесть из fl число f2 lea ESI, _fres fst DWORD PTR [ESI] fwait pop EBP ret _subf2 endp _addlOO@4 proc push EBP mov EBP, ESP mov EAX, DWORD PTR [EBP+8] add EAX, 100 pop EBP ret 4 _addl00@4 endp end Проанализируем исходный текст ассемблерного модуля. Появилась секция .data, в которой описана переменная fres, объявленная как public. Это означает, что она доступна из других модулей. Поскольку не используется явное определение соглашения о вызовах, считаем, что переменная fres обрабатывается в соответствии с соглашением cdeci. Именно поэтому поя- появился символ подчеркивания в имени переменной. С помощью команд lea ESI, _fres fst DWORD PTR [ESI] выполняется сохранение результата, находящегося в стеке сопроцессора, в переменную fres. Напомню, что. ассемблерный модуль компилируется с помощью макроассемблера Visual C++ .NET в составе проекта. Исходный текст консольного приложения на C++ приведен в листинге 7.7. • Лк^тич: 7 /. Использование общей переменной в приложении на C++ .NET #include <stdio.h> extern "C" void subf2(float fl, float f2); extern "C" int _stdcall addl00(int il); extern "C" float fres;
Глава 7. Компоновка ассемблерных модулей с программами на C++ .NET 221 void main(void) { float fl,f2; printf("CUSTOM BUILD WITH COMMON VAR IN ASM MODULE\n\n"); printf("Enter float fl: "); scanf("%f", &fl); printf("Enter float f2: "); scanf{"%?", &?2); subf2(fl, f2); printf("fl - ?2 = %.2f\n", fres); printf("Rounded +100 = %d\n", addlOO((int)(fres/10))); getchar() ; Здесь функция subf2 объявлена как void. После вызова subf2 разность чи- чисел fl и f2 записывается в общую переменную fres. В консольном прило- приложении обязательно нужно объявить переменную fres как extern, причем запретить декорирование имени: extern "с" float fres. Среда программирования Visual C++ .NET позволяет использовать сторон- сторонние средства компиляции для генерации объектных модулей в процессе раз- разработки приложения. Например, для получения файла объектного модуля из ASM-файла можно воспользоваться внешним компилятором ассемблера. Хочу заметить, что ничто не мешает нам использовать и макроассемблер самой среды программирования. Редактирование и компиляцию исходного ассемблерного модуля можно выполнить в самой среде разработки C++ .NET, а полученный объектный модуль необходимо добавить в проект вручную. Лучше всего продемонстрировать работу метода на примере. Разработаем консольное приложение, которое должно вызывать ассемблер- ассемблерную функцию из объектного модуля для обработки строки символов. Обра- Обработка строки заключается в замене символов пробела на символ • +'. Исход- Исходная и обработанная строки отображаются в окне приложения. Назовем ассемблерную функцию conv и сохраним ее исходный текст в файле convstr.asm, например, на диске D:. Место выбрано произвольно, чтобы не ограничивать общность рассуждений. Исходный текст функции conv приведен в листинге 7.8. convstr.asm .686 .model flat, С
222 Часть II. Интерфейс с языками высокого уровня . code conv proc push EBP mov EBP, ESP mov ESI, DWORD PTR [EBP+8] ;pointer to string mov ECX, DWORD PTR [EBP+12] ;length of string mov AL, ' ' next: cmp AL, BYTE PTR [ESI] j ne next_addr mov BYTE PTR [ESI], '+' next_addr: inc ESI dec ECX jnz next pop EBP ret conv endp end Функция conv принимает в качестве первого аргумента (слева-направо) ад- адрес строки, а в качестве второго — ее размер. Оба параметра при вызове функции извлекаются с помощью команд mov ESI, DWORD PTR [EBP+8] mov ECX, DWORD PTR [EBP+12] причем адрес строки находится по адресу [евр+8], а ее размер — по адресу [ЕВР+12]. Поиск и замена символа пробела выполняются в следующем фрагменте кода: next: cmp AL, BYTE PTR [ESI] j ne next_addr mov BYTE PTR [ESI], '+' next_addr: Передача параметров и очистка стека выполняются в соответствии с согла- соглашением _cdeci, поэтому команда ret выполняется без параметров.
Глава 7. Компоновка ассемблерных модулей с программами на C++ .NET 223 Консольное приложение C++ .NET представлено следующим программным кодом (листинг 7.9). ЛиСТИ«Г 7.9. Программа, ИСПч>ЛЬЗу|О:.иС(>Ч функцию v.,o:; // VIRTUAL_ALLOC_AS_SHARE_MEM.cpp : Defines the entry point for the // console application. #include "stdafx.h" #include <windows.h> extern "C" void conv(char* pi, int cnt); int _tmain(int argc, JTCHAR* argv[]) char* pi = NULL; char* p2 = "this is a test string!!! "; printf(" USING EXTERNAL ASM TOOL IN C++ PROJECT\n\n"); pi = (char*)VirtualAlloc(NULL, 256, MEM_COMMIT, PAGE_READWRITE); strcpy(pl, p2); printf("Before conversion: %s\n\n", pi); conv(pl, strlen(pl)); printf("After conversion: %s\n", pi); VirtualFree(pl, 0, MEM_RELEASE); getchar(); return 0; Копия обрабатываемой строки с помощью функции strcpy помещается в Область ПаМЯТИ, ВЫДелеННОЙ С ПОМОЩЬЮ ФУНКЦИИ WIN API VirtualAlloc И адресуемой указателем pi. После выполнения оператора conv(pi, strien (pi)) все обнаруженные символы пробела заменены на • + •. Настроим среду разработки так, чтобы можно было использовать компиля- компилятор MASM для получения объектного модуля непосредственно в окне про- проекта. В качестве компилятора можно выбрать как автономное средство раз- разработки, например, ML.EXE пакета MASM 6.14, так и встроенное в среду разработки. Сделаем первый шаг — выберем пункт меню Tools и в нем опцию External Tools (рис. 7.10). На следующем шаге в окне External Tools устанавливаем опции, как показа- показано на рис. 7.11.
224 Часть II. Интерфейс с языками высокого уровня Рис. 7.10. Выбор сторонних средств разработки — шаг 1 Рис. 7.11. Выбор и установка параметров средств разработки — шаг 2
Глава 7. Компоновка ассемблерных модулей с программами на C++ .NET 225 В окне редактирования Title записываем название компилятора. Особого значения это не имеет, и можно выбрать название произвольно. В окне Command набираем (или выбираем) имя исполняемого файла, вклю- включая полный путь. В окне Arguments набираем опции компилятора (в данном случае, /с /coff) и полный путь к целевому файлу $ (itemPath). Наконец, в окне Initial directory указываем, что целевой файл (объектный модуль) дол- должен быть размещен в каталоге проекта $ (SoiutionDir). После установки в качестве внешнего средства разработки компилятора MASM можно открыть в окне проекта наш ассемблерный файл. Для компи- компиляции необходимо, чтобы окно редактирования ASM-файла было активным. Запуск компилятора выполняется через только что появившуюся опцию MASM меню Tools (рис. 7.12). ¦ив Fies\MicnHHift Visual Sluilo МШ?\Ш1\Ыт\т1мтш" /с /cofT "D:\cfHivifo4s— > Шшшгт Assembler Version 7-88 PJfi*i§irt 46> Kiel»©»oft Carparation. (III rights reserved Рис. 7.12. Вызов компилятора ассемблера из командной строки После успешной компиляции в каталоге проекта появится новый файл с расширением obj. Его нужно включить в наш проект через пункт меню Proj- Project, выбрав опцию Add Existing Item. Сохраняем проект и компилируем его. После запуска окно приложения выглядит так, как показано на рис. 7.13. Выбор и установка в качестве средства разработки внешнего компилятора посредством опции External Tools позволяют применить единые параметры компиляции для всех ассемблерных модулей, входящих в состав разных приложений. В этом случае нет необходимости каждый раз устанавливать опции компилятора для отдельно взятого ассемблерного модуля. Правда, есть и один недостаток такого метода — ассемблерный файл беспо- бесполезно добавлять в проект, его нужно сначала откомпилировать в объектный модуль и только тогда включить в состав проекта. Если C++ .NET-проект включает несколько объектных файлов, содержащих несколько ассемблерных функций каждый, то отладка приложения значи-
226 Часть //. Интерфейс с языками высокого уровня тельно усложняется. Кроме того, в большинстве случаев программист ис- использует одни и те же модули в других приложениях. Очень удобно было бы каким-то образом объединять подобные объектные модули в библиотечный файл и использовать его в приложениях. Для этой цели служит утилита LIB.EXE из пакета Visual C++ .NET. В общем случае ее синтаксис таков: LIB.EXE [options] filel file2 ... где filel, file2 и т. д. — объектные файлы. Рис. 7.13. Окно приложения, демонстрирующего преобразование строки С помощью утилиты LIB.EXE можно выполнять такие задачи: ? добавлять объектные файлы в библиотеку. В этом случае необходимо указывать имя существующей библиотеки и имена включаемых модулей; ? заменять объектный файл в библиотеке; ? удалять объектный файл из библиотеки. Утилита LIB.EXE генерирует файл библиотечного модуля с расширением lib. LIB-файл может быть включен в любой проект. Рассмотрим на примере применение компоновщика библиотек LIB.EXE. Модифицируем наш пре- предыдущий проект следующим образом: заменим все строчные литеры 't' на •т1 с помощью функции convt и строчные литеры fs' на 'S' с помощью функции convs. Обе функции написаны на ассемблере и сохранены в двух файлах convt.asm и convs.asm. Исходный текст функции convs представлен в листинге 7.10. convs.asm .686 .model flat, С
Глава 7. Компоновка ассемблерных модулей с программами на C++ .NET 227 . code convs proc push EBP mov ^ EBP, ESP mov ESI, DWORD PTR [EBP+8] /pointer to string mov ECX, DWORD PTR [EBP+12] ;length of string mov AL, 's' next: cmp AL, BYTE PTR [ESI] jne next_addr mov BYTE PTR [ESI], 'S' next_addr: inc ESI dec ECX jnz next , pop EBP ret convs endp end Аналогично, функция convt повторяет функцию convs, за исключением за- заменяемых литер. Консольное приложение C++, использующее эти функ- функции, модифицируем, как показано в листинге 7.11. // VIRTUAL_ALLOC_AS_SHARE_MEM.cpp : Defines the entry point for the // console application. #include "stdafx.h" #include <windows.h> extern "C" void convs(char* pi, int cnt); extern "C" void convt(char* pi, int cnt); int _tmain(int argc, _TCHAR* argv[]) { char* pi = NULL; char* p2 = "this is a test string!!! "; printf(" USING ASM LIB IN C++ PROJECT\n\n");
228 Часть II. Интерфейс с языками высокого уровня pi = (char*)VirtualAlloc(NULL,256,MEM_COMMIT, PAGE_READWRITE); strcpy(pl, p2); printf ("Before convers/fon: %s\n\n", pi); convt(pl, strlen(pl)); printf("After conversion t->T: %s\n", pi); convs(pl, strlen(pl)); printf("After conversion s->S: %s\n", pi); . VirtualFree(pl, 0, MEM_RELEASE); getchar()/ return 0; При окончательной сборке приложения будем использовать вместо файлов объектных модулей convs.obj и convt.obj один библиотечный файл conv.lib. Для генерации conv.lib выполним последовательно такие шаги: 1. С помощью компилятора ML макроассемблера MASM 6.14 получим файлы объектных модулей. Для этого из командной строки выполним операторы ML /с /coff convs.asm ML /с /coff convt.asm 2. Полученные объектные файлы объединим в один библиотечный файл conv.lib с помощью утилиты LIB.EXE: LIB /OUT:conv.1ib convs.obj convt.obj 3. Включим наш библиотечный файл в проект и после компиляции запус- запустим его. Окно работающего приложения показано на рис. 7.14. Основное преимущество библиотечных модулей состоит в том, что в них можно включать объектные модули, скомпилированные как из ASM- файлов, так и из СРР-файлов. Это позволяет создавать очень мощные биб- библиотеки. Следующий пример демонстрирует, как в одном библиотечном мо- модуле можно скомпоновать две арифметические функции и использовать их в приложении. Первая функция add2 написана на C++ и выполняет сложение двух целых чисел. Исходный текст этой функции сохраним в файле асШ.срр. Вторая функция (назовем ее sub2) написана на ассемблере и выполняет вычитание
Глава 7. Компоновка ассемблерных модулей с программами на C++ .NET 229 двух целых чисел. Исходный текст этой функции сохраним в файле sub2.asm. Обе функции возвращают результат в виде целочисленной переменной. Рис. 7.14. Окно приложения, демонстрирующего использование библиотечного модуля Исходный текст функции add2 представлен в листинге 7.12. extern "C" int add2(int il, int i2) return (il+i2); Обратите внимание на объявление функции add2. Для доступа к ней из других модулей необходимо использовать директиву extern. Исходный текст функции sub2 показан в листинге 7.13. I i ¦*: »:л и к г ?'. 13. Ф у н к ци >i ;¦.¦.;:>:.,..; .7 ,-п . sub2.asm .686 .model flat, С .code sub2 proc push EBP mov EBP, ESP
230 Часть II. Интерфейс с языками высокого уровня mov ЕАХ, DWORD PTR [ЕВР+8] ;il sub EAX, DWORD PTR [EBP+12] ;il-i2 pop EBP ret sub2 endp end Для компоновки файла библиотеки необходимо вначале получить объект- объектные модули из файлов асШ.срр и sub2.asm. Для компиляции ассемблерного файла будем использовать компилятор ML макроассемблера MASM, входя- входящий в состав среды C++ .NET 2003: ML /с /coff sub2.asm Если компиляция выполнена без ошибок, получим файл sub2.obj. Для компиляции файла add2.cpp воспользуемся стандартным компилятором C++ .NET: CL /с add2.cpp Оба объектных модуля add2.obj и sub2.obj можно объединить в файле стати- статической библиотеки addsub.lib с помощью командной строки LIB /OUT:addsub.lib add2.obj sub2.obj Полученную библиотеку можем использовать в консольном приложении, исходный текст которого приведен в листинге 7.14. : Листинг 1 Лч. Программа, в которой испот,дуется г^Йлистекз // C_n_ASM_LIB.срр : Defines the entry point for the console application. #include "stdafx.h" extern "C" int add2(int il, int i2) ; extern "C" int sub2(int il, int i2); int _tmain(int argc, _TCHAR* argv[]) { int il, i2; printf(" USING ASM & С OBJ MODULES IN LIB-FILE\n\n"); printf("Enter integer il: "); scanf("%d",
Глава 7. Компоновка ассемблерных модулей с программами на C++ .NET 231 printf("Enter integer i2: "); scanf("%d", &i2); printf("\nResult of ADD2 call: %d\n", add2(il, i2)); printf("\nResult of SUB2 call: %d\n", sub2(il, i2)); getchar(); return 0; Поскольку функции add2 и sub2 находятся в отдельном модуле, их необхо- необходимо объявить как внешние: extern "С" int add2(int il, int i2); extern "C" int sub2(int il, int i2); Окно работающего приложения показано на рис. 7.15. Рис. 7.15. Окно приложения, демонстрирующего работу библиотечных функций add2 и sub2 В приложении можно использовать несколько стандартных библиотек. Сле- Следующий пример, который мы рассмотрим, представляет собой консольное приложение C++ .NET с двумя включаемыми библиотеками libiasm.iib и lib2asm.lib, причем библиотека libiasm содержит две взаимосвязанные функции signmul и sub20, а в состав библиотеки libiasm входит функция signdiv. Исходный текст ассемблерного кода, входящего в библиотеку libiasm, представлен в листинге 7.15. .686 .model flat, С
232 Часть II. Интерфейс с языками высокого уровня . code signmul proc push EBP mov EBP, ESP mov EM, DWORD PTR [EBP+8] mov ECX, DWORD PTR [EBP+12] imul ECX call sub20 pop EBP ret signmul endp sub20 proc sub EAX, 20 ret sub20 endp end Функция signmul выполняет умножение двух знаковых-целых чисел. Ре- Результат умножения уменьшается на 20 с помощью вызова функции sub20. Особенностью этой библиотеки является то, что здесь используется вспо- вспомогательная функция sub20, которую не нужно объявлять в основном при- приложении. Библиотека iib2asm.iib содержит одну функцию signdiv, возвращающую адрес результата деления двух целых знаковых чисел. Исходный текст ас- ассемблерной функции signdiv приведен в листинге 7.16. .686 .model flat, С .data fres DD 0 .code signdiv proc push EBP mov EBP, ESP finit
Глава 7. Компоновка ассемблерных модулей с программами на C++ .NET 233 fild DWORD PTR [EBP+8] fidiv DWORD PTR [EBP+12] fistp DWORD PTR fres fwait mov EAX, offset fres pop EBP ret signdiv endp end Для выполнения операции деления двух целых чисел используются команды математического сопроцессора. Результат операции сохраняется в переменной fres с помощью команды fistp DWORD PTR fres, а адрес переменной возвращается в основную программу в регистре еах. Это выполняет команда mov EAX, offset fres Исходный текст функций сохранен в файлах liblasm.asm и Iib2asm.asm. Генерация объектных модулей libiasm.obj и iib2asm.obj выполняется командами ML /с /coff liblasm.asm ML /с /coff Iib2asm.asm Файлы статических библиотек легко получить с помощью утилиты LIB.EXE: LIB /OUT:liblasm.lib libiasm.obj LIB /0UT:lib2asm.lib Iib2asm.obj Разработаем приложение, в котором будут использоваться созданные нами библиотечные файлы. Исходный текст приложения показан в листинге 7.17. |]мс*и;¦¦;?¦ 7,17, Консольное приложение, используюшее библиотечные файлы // LIB1_PLUS_LIB2.ерр : Defines the entry point for the console // application. #include "stdafx.h" extern "C" int signmul(int il, int i2); extern "C" int* signdiv(int il, int i2) ;
234 Часть //. Интерфейс с языками высокого уровня int _tmain(int argc, _TCHAR* argv[] int il, i2; printf("USItfG SOME STANDARD LIBRARIES EXAMPLE\n\n"); printf("Enter integer il:"); scanf("%d", &il); printf("Enter integer i2:"); scanf("%d", &i2); printf("MUL(il, i2)-20 = %d\n", signmul(il,i2)); printf("DIV(il, i2)= %d\n", *signdiv(il,i2)); getchar(); return 0; В проект приложения необходимо включить оба файла библиотек, сохра- сохранить и перекомпилировать проект. Окно работающего приложения показано на рис. 7.16. Рис. 7.16. Окно приложения, в котором используются два библиотечных модуля Следующая тема, которую я хотел бы рассмотреть, — применение ассемб- ассемблерных функций в библиотеках динамической компоновки DLL. He буду останавливаться на теоретических аспектах построения DLL — они доста- достаточно хорошо описаны в литературе. Применение ассемблерных функций в этих библиотеках позволяет улучшить качество программного кода, но ком- компоновка ассемблерного модуля и DLL нуждается в детальных объяснениях. Рассмотрим основные аспекты этого процесса на практическом примере.
Глава 7. Компоновка ассемблерных модулей с программами на C++ .NET 235 Пусть наше консольное приложение использует те же функции, что и в предыдущем примере — sub2 и add2, причем обе эти функции вызываются на исполнение из библиотеки динамической компоновки (назовем ее DLL_n_ASM.dll). Разработаем вариант решения (solution), состоящий из двух проектов. Один из проектов будет представлять собой DLL, второй — кон- консольное приложение, использующее функции из этой библиотеки. Разработку DLL выполним с помощью мастера приложений C++ .NET. Не- Необходимо выбрать за основу консольное приложение Windows и в свойствах Application Settings выбрать DLL. Модифицируем исходный текст СРР- файла библиотеки, как показано в листинге 7.18 (изменения выделены жир- жирным шрифтом). // DLL_n_ASM.срр : Defines the entry point for the DLL application. #include "stdafx.h" extern "C" int sub2(int il, int i2) ; BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { return TRUE; } extern "C" declspec(dllexport) int add2(int il, int i2) { return (il+i2); } extern "C" declspec(dllexport) int subdll2(int il, int i2) { return (sub2(il, i2));
236 Часть //. Интерфейс с языками высокого уровня Наша DLL содержит две функции. Одна из них — subdii2 — в качестве вспомогательной использует ассемблерную функцию sub2. Исходный текст функции sub2 представлен в листинге 7.19. sub2.asm .686 .model flat, С public sub2 .code sub2 proc push EBP mov EBP, ESP mov EAX, DWORD PTR [EBP+8] ;il sub EAX, DWORD PTR [EBP+12] ;i2 pop EBP ret sub2 endp end Включим ассемблерный файл sub2.asm, содержащий функцию sub2, в про- проект библиотеки DLL и установим опции компилятора для этого файла через опцию Properties. Сохраним наш проект и откомпилируем его. Если нет ОШИбоК, ТО ПОЛУЧИМ библиотеку ДИНаМИЧеСКОЙ КОМПОНОВКИ DLL_n_ASM.dll с двумя экспортируемыми функциями add2 и subdii2 и библиотеку импор- импорта DLL_n_ASM.lib. Для демонстрации работы DLL разработаем проект консольного приложе- приложения, связанный с этой библиотекой. Исходный текст приложения показан в листинге 7.20. // USE_DLL.cpp : Defines the entry point for the console application, tinclude "stdafx.h"
Глава 7. Компоновка ассемблерных модулей с программами на C++ .NET 237 extern "С" declspec(dllimpQrt) int add2<int il, int i2) ; extern "C" declspec(dllimport) int subdll2(int il, int i2); int _tmain(int argc, _TCHAR* argv[]) { int il, i2; printf("USING ASM FUNC IN DLL \n\n"); printf("Enter il:"); scanf("%d", &il); printf("Enter i2:"); scanf("%d", &i2); printf("\nResult of ADD2 call: %d\n", add2(il, i2)); printf("\nResult of ASM func SUB2 call: %d\n", subdll2(il, i2)); getchar(); return 0; Будем использовать статическое связывание DLL с основным приложени- приложением. Для этого вюиочим в проект консольного приложения библиотеку им- импорта DLL_n_ASM.iib и скопируем файл DLL_n_ASM.dll в системный ката- каталог Windows. Окно, содержащее оба проекта, будет выглядеть так, как показано на рис. 7.17. Еще одно замечание. Функции, вызываемые из DLL, должны быть объявле- объявлены как импортируемые: extern "С" declspec(dllimport) int add2(int il, int i2) extern "C" declspec(dllimport) int subdll2(int il, int i2) Компилировать проекты USE_DLL и DLL_n_ASM можно как по отдельно- отдельности, так и вместе, если выбрать в пункте меню Build опцию Rebuild Solution. Окно работающего приложения показано на рис. 7.18. Использование ассемблерных функций в составе библиотек динамической компоновки можно сделать более удобным, если объектные модули объеди- объединить в файл стандартной библиотеки. Предположим, необходимо найти большее из двух вещественных чисел с помощью функции max и меньшее — с помощью функции min. Исходные тексты функций сохраним в файле minmax.asm. Программный код этих функций представлен в листинге 7.21.
238 Часть II. Интерфейс с языками высокого уровня Рис. 7.17. Вид окна проектов Рис. 7.18. Окно приложения, демонстрирующего вызов ассемблерной функции из DLL
Глава 7. Компоновка ассемблерных модулей с программами на C++ .NET 239 .686 .model flat, С . code fraax proc push EBP mov EBP, ESP finit fid DWORD PTR [EBP+8] fcomp DWORD PTR [EBP+12] fstsw AX sahf jb set_op fid DWORD PTR [EBP+8] jmp .com set_op: fid DWORD PTR [EBP+12] com: fwait pop EBP ret fmax endp fmin proc push EBP mov EBP, ESP finit fid DWORD PTR [EBP+8] fcomp DWORD PTR [EBP+12] fstsw AX sahf jb set_op fid DWORD PTR [EBP+12] jmp com set_op: fid DWORD PTR [EBP+8] com: fwait
240 Часть II. Интерфейс с языками высокого уровня pop EBP ret fmin endp end Я не буду останавливаться на анализе исходного текста функций, поскольку похожие примеры мы уже анализировали. Поместим программный код этих функций в библиотеку minimax.iib. Для этого последовательно выполним команды: ML /с /coff minimax.asm LIB /OUT:minimax.iib minimax.obj Разработаем решение, состоящее из двух проектов. Первый проект — библио- библиотека ДИНамИЧеСКОЙ КОМПОНОВКИ (назовем ее USING_IMPDLL_STANDARD.dll). Исходный текст шаблона библиотеки, сгенерированный мастером приложе- приложений, откорректируем так, как показано в листинге 7.22. // USING_IMPDLL_STANDARD.cpp : Defines the entry point for the DLL // application. #include "stdafx.h" extern "C" float fmax(float fl, float f2); extern "C" float fmin(float fl, float f2) ; BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul__reason_for_call/ LPVOID lpReserved return TRUE; } extern "C" float dec1spec(dllexport) sub2f(float fl, float f2) { return(fmax(fl, f2)-min(fl,f2));
Глава 7. Компоновка ассемблерных модулей с программами на C++ .NET 241 Для корректной работы среди файлов проекта должна находиться и стандарт- стандартная библиотека minimax. lib, ссылки на функции которой определены как extern "С" float fmax(float fl, float f2) extern "C" float fmin(float fl, float f2) Кроме этого, сама функция sub2f должна быть объявлена как внешняя с атрибутом dilexport. После компиляции проекта получим библиотеку DLL и библиотеку импорта. Свяжем с проектом DLL еше один, в котором будет продемонстрировано использование стандартной библиотеки minimax. lib и DLL. Исходный текст приложения представлен в листинге 7.23. // USE_MINIMAX_LIB_IN_DLL.cpp : Defines the entry point for the console // application. #include "stdafx.h" extern "C" declspec(dllimport) float sub2f(float fl, float ?2); extern "C" float fmax(float fl, float f2) ; extern "C" float fmin(float fl, float f2); int _tmain(int argc, _TCHAR* argv[]) { float fl, f2; printf(" USING DLL WITH STANDARD LIB DEMO\n\n"); printf("Enter float fl:"); scanf("%f", &fl); printf("Enter float f2:"); scanf("%f", &f2); printf("\nMAX = %.2f\n", fmax(fl, f2)) ; printf("\nMIN - %.2f\n", fmin(fl, f2)); printf("\nMAX-MIN= %.2f\n", sub2f(fl, f2)); getchar(); return 0; Поскольку в приложении используются функции fmin и fmax, определен- определенные В ДруГОМ МОДуле, ТО Необходимо ВКЛЮЧИТЬ библиотеку minimax. lib В состав приложения. «
242 Часть //. Интерфейс с языками высокого уровня Окно работающего приложения показано на рис. 7.19. Рис. 7.19. Окно приложения, демонстрирующего применение стандартной библиотеки и DLL Рассмотрим еще один пример использования отдельно скомпилированного ассемблерного модуля в составе DLL. Пусть наша DLL (назовем ее coMMON_DLL.dll) содержит функцию absfdii, вычисляющую абсолютное значение произвольного вещественного числа. В свою очередь, absfdii ис- использует для вычисления ассемблерную функцию absf. Результат вычисления абсолютного значения числа отображается на экране консольным приложением C++. Основное приложение для вызова absf из DLL использует динамическую загрузку и обращение к функции посредст- посредством ВЫЗОВа WIN API LoadLibrary И GetProcAddress. Теперь перейдем к более детальному рассмотрению примера. Начнем с биб- библиотеки динамической компоновки coMMON_DLL.dll. Ее исходный текст представлен в листинге 7.24. ]игт-*Mf 7.24, И;; tu; :;.:.': ;¦ V-;.::¦..;'i ¦• у so ; j'>:/ M ['¦¦'Ю '.;L:....; II COMMON_DLL.cpp : Defines the entry point for the DLL application. #include "stdafx.h" extern "C" float absf(float fl); BOOL APIENTRY DllMain( HANDLE hModule, • DWORD ul reason for call,
Глава 7. Компоновка ассемблерных модулей с программами на C++ .NET 243 LPVOID lpReserved ) { return TRUE; } extern "С" declspec(dllexport) float absfdll(float fl) { return (absf(fl)); Поскольку функция absf находится в другом модуле, она должна быть объ- объявлена с директивой extern. Функция absfdii, экспортируемая из DLL, использует absf в операторе возврата return. Хочу заметить, что такой ва- вариант использования ассемблерной функции (через оператор return) позво- позволяет применять ассемблер для работы с управляемым кодом C++ .NET, что, в общем случае, сделать довольно сложно. Исходный текст ассемблерной функции absf не вызывает затруднений для анализа и представлен в листинге 7.25. Листинг 7.25. Ассемблерная Функция u . absf. asm 686 model : code flat, С absf proc push mov finit fid fabs fwait pop ret EBP EBP, ESP DWORD PTR [EBP+8] EBP absf endp end
244 Часть II. Интерфейс с языками высокого уровня Основное приложение, как было уже сказано, использует метод динамиче- динамической загрузки функции.absf из библиотеки и не требует предварительного объявления импортируемых из DLL функций с директивой extern. Кроме того, не требуется включать файл библиотеки импорта, как в предыдущем примере, в проект основного приложения. Исходный текст консольного приложения на C++ .NET приведен в листинге 7.26. // This is the main project file for VC++ application project // generated using an Application Wizard. #include "stdafx.h" #include <windows.h> int _tmain() // TODO: Please replace the sample code below with your own. typedef FLOAT (*myfunc) (FLOAT); myfunc absfdll; printf(" USE EXTERNAL OBJ IN DLL (DYNAMIC LOADING)\n\n"); HINSTANCE hLib = LoadLibrary("COMMON_DLL"); if (hLib == NULL) { printf("Unable to load library\n"); getchar() ; exit A); absfdll = (myfunc)GetProcAddress(hLib, "absfdll") if (!absfdll) printf("Unable to load functions!\n"); FreeLibrary(hLib);
Глава 7. Компоновка ассемблерных модулей с программами на C++ .NET getchar(); exit A); 245 float fl = -731.19; printf("ABS of float fl (%.3f) = %.3f", fl,absfdii(fl)); FreeLibrary(hLib); getchar(); return 0; Первое, что необходимо сделать, — определить указатель на функцию, и создать экземпляр этого указателя на используемую функцию: t ypede f FLOAT (*myfunc) (FLOAT); myfunc absfdii; Поскольку наша функция absfdii принимает в качестве параметра float и возвращает значение типа float, to это отображено в определении указате- указателя myfunc. Если функция LoadLibrary выполнена успешно, то полученный дескриптор модуля загруженной библиотеки используется для получения адреса функ- функции absfdii: absfdii = (myfunc)GetProcAddress(hLib, "absfdii") По окончании работы с DLL необходимо сообщить операционной системе Windows, что приложение более не нуждается в DLL. В этом случае Windows декрементирует значение счетчика использований DLL. Окно приложения показано на рис. 7.20. Рис. 7.20. Окно приложения, демонстрирующего применение ассемблерной функции при динамической загрузке библиотеки
246 Часть II. Интерфейс с языками высокого уровня Подведем итоги, касающиеся использования отдельно скомпилированных ассемблерных модулей в приложениях на C++ .NET 2003. Компиляция мо- модулей может выполняться как автономным компилятором MASM, так и встроенным в среду программирования C++ макроассемблером. При этом генерируется стандартный файл объектного модуля, имеющий формат COFF или OMF. Дальнейшая компоновка приложения и объектных моду- модулей определяется только разработчиком программы и особенностями функ- функционирования приложения. Мы рассматривали макроассемблер MASM и его возможности, хотя для ге- генерации объектных файлов можно использовать любой другой компилятор, генерирующий объектный файл формата COFF. Хочется верить, что изло- изложенный материал и примеры помогут читателю при реализации своих задач.
Глава 8 Разработка библиотек динамической компоновки (DLL) на ассемблере Библиотеки динамической компоновки (Dynamic Link Libraries — DLL) яв- являются неотъемлемой и, пожалуй, наиболее важной частью операционных систем Windows. Они служат хранилищем многочисленных процедур, в том числе и функций WIN API, и являются мощным средством для написания эффективных приложений. Я не буду останавливаться на принципах по- построения и функционирования DLL, поскольку имеется масса публикаций, посвященных этой теме. Гораздо более интересно научиться самому созда- создавать библиотеки динамической компоновки. Библиотеки DLL, независимо от того, какими средствами программирования они созданы, могут исполь- использоваться с любыми компиляторами и в любых программах. Применение DLL предоставляет значительные преимущества при разработ- разработке и тиражировании программного кода: ? уменьшается размер исполняемого кода, поскольку несколько приложе- приложений могут использовать одну и ту же библиотеку; ? помещенный в DLL код требует меньших усилий на разработку, чем по- подобные функции, используемые в нескольких приложениях; ? крупные проекты могут легче структурироваться и становиться более лег- легкими в управлении; ? реализация новых функций значительно упрощается: достаточно выпус- выпустить новую версию DLL. Библиотеки DLL обычно создаются на языках высокого уровня (С, Pascal), хотя можно разрабатывать их и с помощью ассемблера. Мы рассмотрим во- вопросы, касающиеся создания и использования DLL, в таком порядке: 1. Инициализация DLL. 2. Экспорт функций и данных из DLL.
248 Часть II. Интерфейс с языками высокого уровня 3. Загрузка DLL в момент запуска приложения. 4. Загрузка DLL во время выполнения приложения. Для каждой DLL должна быть определена точка входа. В Visual C++ .NET 2003 такой точкой входа является функция DiiMain. В макроассемблере MASM такой точкой входа служит функция LibMain. Операционная система вызывает эти функции в следующих случаях: ? когда приложение (процесс) вызывает DLL впервые; ? когда процесс, связанный с этой библиотекой, создает новый поток; П когда процесс, связанный с этой библиотекой, удаляет поток; ? при удалении DLL из памяти. Все функции и данные могут экспортироваться из DLL одним из следую- следующих способов: ? путем создания DEF-файла библиотеки, в раздел export которого поме- помещаются имена экспортированных элементов; ? путем создания ссылок на экспортируемые элементы с помощью ключе- ключевых СЛОВ declspec (dllexport). Метод, использующий файл описания DEF, к настоящему времени явля- является устаревшим, хотя все еще применяется. Управление и компоновка DEF-файла могут оказаться довольно сложными задачами, особенно при использовании различных компиляторов для разработки DLL и проекта приложения. Метод declspec является предпочтительным для работы с 32-разрядными приложениями и применяется в большинстве случаев. В 32-разрядных вер- версиях компиляторов (не только в C++ .NET) можно экспортировать данные, функции, классы и функции-члены классов из DLL, используя ключевое СЛОВО declspec(dllexport). Ключевое СЛОВО declspec(dllexport) включает директиву экспорта в объектный файл проекта, что делает ненуж- •ным использование DEF-файла. Библиотека DLL может загружаться для выполнения одновременно с за- загрузкой основного приложения (явная загрузка) и во время выполнения приложения (динамическая загрузка). При явной загрузке компоновщику необходимо указать то, что приложение зависит от DLL. Для этого в основной проект включается библиотека им- импорта с расширением lib. Библиотека LIB создается компилятором C++ .NET автоматически при построении DLL. LIB-файл — это не обычная ста- статическая библиотека. В нем содержится не сам код библиотеки, а только ссылка на все функции, экспортируемые из файла DLL, в котором хранится сам код. Библиотеки импортирования имеют, как правило, меньший раз- размер, чем DLL-файлы.
Глава 8. Разработка библиотек динамической компоновки (DLL) на ассемблере 249 При запуске приложения Windows находит необходимые DLL и вычисляет адреса каждой ссылки на DLL. Поиск библиотеки выполняется вначале в каталоге приложения, далее в подкаталоге System и в каталоге Windows. Для иллюстрации метода явной загрузки разработаем в среде Visual C++ .NET две программы — библиотеку DLL (под названием impdii) и приложение testimpdii, в котором она используется. Библиотека DLL содержит одну функцию (sub2dii), возвращающую разность двух целочисленных парамет- параметров. Воспользуемся мастером приложений C++ .NET и выберем в качестве шаблона Win32 Project. Зададим тип приложения — DLL и сгенерируем фай- файлы проекта. В созданном шаблоне библиотеки impdii.cpp находится только точка входа в DLL — функция DUMain. Включим исходный текст нашей функции sub2dii (он выделен жирным шрифтом) в шаблон проекта. Оконча- Окончательный вариант исходного текста показан в листинге 8.1. // impdii.cpp : Defines the entry point for the DLL application. #include "stdafx.h" BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved return TRUE; } int declspec(dllexport) sub2dll(int 11, int i2) { return (il-i2); Для того чтобы функция sub2dii была доступной из других модулей, она описана с ключевым словом declspec (dllexport) (используются два символа подчеркивания!). Разработаем приложение, вызывающее функцию sub2dii из библиотеки impdii.dll. В качестве шаблона выберем Win32 Project и установим опцию для создания консольного приложения (назовем его testimpdii). Исход- Исходный текст приложения показан в листинге 8.2.
250 Часть II. Интерфейс с языками высокого уровня II test_impdll.cpp : Defines the entry point for the console application. #include "stdafx.h" int _declspec(dllimport) sub2dll(int il, int 12); int _tmain(int argc, _TCHAR* argv[]) { int isub; int il = 56; int 12 = -34; printf("il = %d, ", il); printf("i2 = %d\n", i2); printf("il - i2 = %d", sub2dll(il, 12)); getchar(); return 0; Приложению необходимо указать, что используемая функция sub2dii им- импортируется из другого модуля (это указание выделено* жирным тестом). При окончательной сборке приложения необходимо включить в проект библиотеку импорта impdii.lib и скопировать в каталог приложения (или в системный каталог) файл impdll.dll. Вид окна работающего приложения показан на рис. 8.1. Рис 8.1. Окно приложения, демонстрирующего применение библиотеки импорта impdll. dll Макроассемблер MASM также позволяет создать DLL. Применение ассемб- ассемблерного кода в библиотеках динамической компоновки позволяет сущест- существенно увеличить быстродействие приложения в целом и уменьшить размер программного кода. Разработаем аналог impdll.dll с использованием MASM. Вначале посмотрим, как создать шаблон DLL на ассемблере. Ис-
Глава 8. Разработка библиотек динамической компоновки (DLL) на ассемблере 251 ходный текст такого шаблона (назовем его tempidii.asm) несложен и вы- выглядит, как показано в листинге 8.3. templdll.asm- ,686 .model flat, С option casemap :none .code LibMain proc hlnstDLL:DWORD, reason:DWORD, unused:DWORD mov ЁАХ, 1 ret LibMain Endp End LibMain Шаблон содержит только функцию LibMain, являющуюся точкой входа мо- модуля. В листинге 8.3 представлен простейший вариант этой функции. Для компиляции и сборки библиотеки DLL необходимо выполнить команды: ml /с /coff tempidii.asm link /SUBSYSTEM-.WINDOWS /DLL templdll.obj Созданная библиотека tempidii.dll — просто "заглушка", и ничего полез- полезного она не делает. Поэтому несколько изменим исходный текст DLL, включив в него функцию, вычисляющую разность двух целых чисел (назовем ее sub2). Модифицированный вариант файла с исходным текстом DLL (назовем его sub2.asm) показан в листинге 8.4. Листинг 8.4.. Модифицированный ыариант библиотеки нз ассем&лорс ¦ . sub2.asm .686 .model flat, С option casemap :none .code LibMain proc hlnstDLL:DWORD, reason:DWORD, unused:DWORD mov EAX, 1 ret LibMain endp
252 Часть II. Интерфейс с языками высокого уровня sub2 push mov mov sub pop ret sub2 proc EBP EBP, EAX, EAX, EBP endp end LibMain ESP [EBP+8] ;il [EBP+12] ;-i2 Для использования DLL в программе C++ .NET лучше всего указать тип языка в директиве model: .model flat, С Это означает, что используется соглашение о вызовах cdeci, поэтому команда ret в функции sub2 выполняется без параметров, и стек очищается вызывающей программой на C++. Исходный текст функции sub2 несложен и в дополнительных пояснениях не нуждается. Для создания библиотеки импорта нам понадобится файл описания экспор- экспортируемых функций с расширением def. Создадим такой файл и назовем его sub2.def. Он должен содержать следующие строки: LIBRARY sub2 EXPORTS sub2 DEF-файл необходим только для создания библиотеки импорта с помощью ассемблера и нигде более применяться не будет. Выполним команды: ml /с /coff sub2.asm link /SUBSYSTEM:WINDOWS /DLL /DEF:sub2.def sub2.obj Если компиляция прошла успешно, то мы получим файлы sub2.obj sub2.1ib sub2.dll sub2.exp Протестируем созданную на ассемблере DLL, воспользовавшись методом явной загрузки. Для этого нам понадобятся библиотеки sub2.dll и sub2.iib. Разработаем программу в C++ .NET. Воспользуемся мастером приложений и создадим консольное приложение на базе Win32 Project. В проект необходимо включить файл sub2.1ib, сгенерированный ассембле-
Глава 8. Разработка библиотек динамической компоновки (DLL) на ассемблере 253 ром, а в рабочий каталог проекта скопировать файл sub2.dll. Исходный текст программы представлен в листинге 8.5. // test_asmdll.cpp : Defines the entry point for the console application. #include "stdafx.h" extern "C" int sub2(int il, int i2); int _tmain(int argc, _TCHAR* argv[]) { int il ¦¦ 23; int i2 = -19; printf("il = %d, ", il); printf("i2 = %d\n", i2); printf("il - i2 = %d", sub2(il, i2)); getchar(); return 0; Следует обратить особое внимание на строку extern "С" int sub2(int il, int i2); Для библиотек DLL, сгенерированных с помощью ассемблера, применять ключевое слово decispec(diiimport) нет необходимости, достаточно объявить функцию внешней и запретить декорирование имен. Окно прило- приложения показано на рис. 8.2. Рис. 8.2. Окно приложения, демонстрирующего вызов функции DLL при явном связывании (ассемблерный вариант) В библиотеки динамической компоновки можно включать и функции из объектных модулей на ассемблере. Весьма распространенным является спо- способ, когда шаблон DLL генерируется в C++ .NET и в него включаются
254 Часть II. Интерфейс с языками высокого уровня функции из OBJ-файлов, сгенерированных макроассемблером. Это очень удобно, поскольку подобная комбинация функций позволяет варьировать размер программного кода, ресурсы операционной системы и производи- производительность при относительно небольших затратах времени на разработку. Рассмотрим пример, в котором основная программа на C++ использует для вычислений три функции из DLL. Одна из функций (назовем ее add2) воз- возвращает сумму двух целых чисел. Вторая функция (назовем ее sub2) возвра- возвращает разность двух целых чисел, она написана на ассемблере и откомпили- откомпилирована как отдельный OBJ-файл. Третья функция (назовем ее submuis) использует результат, возвращаемый sub2, и выполняет умножение разности двух целых чисел на 5. Воспользуемся мастером приложений Visual C++ .NET и разработаем DLL, содержащую эти функции. Шаблон DLL, сгене- сгенерированный мастером, представлен файлом addsub.cpp в листинге 8.6. ¦¦ Листинг 8.6. Шаблон DLL // addsub.cpp : Defines the entry point for the DLL application. #include "stdafx.h" BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { return TRUE; } Включим в исходный текст листинга 8.6 текст функций. Функция add2 определяется следующим образом: extern "С" declspec(dllexport) int add2(int il, int i2) { return (il+i2); }; Функция sub2 находится во внешнем ассемблерном модуле и объявляется как extern "С" int sub2(int il, int i2); а ее исходный текст представлен в листинге 8.7. ; sub2.asm- .686
Глава 8. Разработка библиотек динамической компоновки (DLL) на ассемблере 255 .model flat, С public sub2 .code sub2 proc push EBP mov EBP, ESP mov EAX, DWORD PTR [EBP+8] sub EAX, DWORD PTR [EBP+12] pop EBP ret sub2 endp end Исходный текст функции sub2 сохраним в файле sub2. asm и откомпилиру- откомпилируем с помощью MASM. Полученный файл объектного модуля включим в проект нашей DLL. Функция выполняет вычитание двух целых чисел и возвращает их разность. Результат выполнения sub2 используется экспортируемой функцией submuis: extern "С" declspec(dllexport) int submul5(int il, int iZ) { return (sub2(il, i2)*5); В окончательном виде исходный текст файла addsub.cpp DLL-проекта пред- представлен в листинге 8.8. // addsub.cpp : Defines the entry point for the DLL application. #include "stdafx.h" BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved return TRUE; extern "C" declspec(dllexport) int add2(int il, int i2)
256 Часть //. Интерфейс с языками высокого урдвня return (il+i2); }; extern "С" int sub2(int il, int i2); extern "C" declspec(dllexport) int submul5(int il, int i2) { return (sub2(il, i2)*5); После компиляции проекта DLL в числе сгенерированных модулей получим библиотеку динамической компоновки addsub.dll и библиотеку импорта addsub.lib. Разработаем тестовое приложение, использующее функции из библиотеки addsub.dll. Для этого воспользуемся мастером приложений, который сгене- сгенерирует приложение на базе диалогового окна. Разместим на главной форме приложения четыре элемента редактирования (Edit control), четыре элемен- элемента статического текста (static Text) и кнопку (Button). Элементам редакти- редактирования поставим в соответствие целочисленные переменные il, i2, add2Edit и submui5Edit. В полях редактирования, соответствующих переменным il и i2, будем вводить целочисленные значения, а в полях редактирования, соот- соответствующих результатам выполнения функций add2 и submuis, отображать вычисляемые значения. Все эти манипуляции выполняются в обработчике нажатия кнопки onBnciickedButtoni. Ввиду важности темы я предоставлю полный исходный текст программного кода в листинге 8.9. ЛИСТИНГ 8.9. Программный КОД ПрИЛОЖ^НИЯ. ИСПОШ.ЗуЮЩиГй СТОТИЧООКОС' j подключение DLL. : // testdllDlg.cpp : implementation file #include "stdafx.h" #include "testdll.h" #include "testdllDlg.h" #include ".\testdlldlg.h" int declspec(dllimpor.t) add2(int il, int i2); int declspec(dllimport) submul5(int il, int i2); #ifdef _DEBUG #define new DEBUG_NEW #endif
Глава 8. Разработка библиотек динамической компоновки (DLL) на ассемблере 257 11 CAboutDlg dialog used for App About class CAboutDlg : public CDialog { public: CAboutDlg(); // Dialog Data enum { IDD = IDD_ABOUTBOX }; protected: virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support // Implementation protected: DECLARE_MESSAGE_MAP() }; CAboutDlg::CAboutDlg() : CDialog(CAboutDlg::IDD) void CAboutDlg::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); BEGIN_MESSAGE_MAP(CAboutDlg, CDialog) END_MESSAGE_MAP() // CtestdllDlg dialog CtestdllDlg::CtestdllDlg(CWnd* pParent /*=NULL*/) : CDialog(CtestdllDlg::IDD, pParent) , i2@) , add2Edit@) , submul5Edit@) m_hlcon = AfxGetApp()->LoadIcon(IDR__MAINFRAME) }
258 Часть II. Интерфейс с языками высокого уровня void CtestdllDlg::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); DDXJText(pDX, IDC_EDIT1, il); DDXJText(pDX, IDC_EDIT2, i2); DDX_Text (pDX, IDC_EDIT3, add2Edit); DDXJText (pDX, IDC_EDIT4, submul5Edit) ; BEGIN_MESSAGE_MAP(CtestdllDlg, CDialog) ON_WM_SYSCOMMAND() ON_WM_PAINT() ON_WM_QUERYDRAGICON() //}}AFX_MSG_MAP ON_BN_CLICKED(IDC_BUTTON1, OnBnClickedButtonl) END_MESSAGE_MAP() // CtestdllDlg message handlers BOOL CtestdllDlg::OnInitDialog() { CDialog::OnlnitDialog(); // Add "About..." menu item to system menu. // IDM_ABOUTBOX must be in the system command range. ASSERT((IDM_ABOUTBOX & OxFFFO) == IDM_ABOUTBOX); ASSERT(IDM_ABOUTBOX < OxFOOO); CMenu* pSysMenu = GetSystemMenu(FALSE); if (pSysMenu != NULL) { CString strAboutMenu; strAboutMenu.LoadString(IDS_ABOUTBOX); if (!strAboutMenu.IsEmpty()) { pSysMenu->AppendMenu(MF_SEPARATOR); pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu); // Set the icon for this dialog. The framework does this automatically // when the application's main window is not a dialog
Глава 8. Разработка библиотек динамической компоновки (DLL) на ассемблере 259 SetIcon(m_hlcon, TRUE); // Set big icon Setlcon(m_hlcon, FALSE); // Set small icon // TODO: Add extra initialization here return TRUE; // return TRUE unless you set the focus to a control void CtestdllDlg: :OnSysCornmand(UINT nID, LPARAM lParam) { if ((nID & OxFFFO) «= IDM_ABOUTBOX) { CAboutDlg dlgAbout; dlgAbout.DoModal(); } else { CDialog::OnSysCommand(nID, lParam) ; } } void CtestdllDlg::OnPaint{) { if (IsIconicO ) { CPaintDC dc(this); // device context for painting SendMessage(WMJECONERASEBKGND, reinterpret_cast<WPARAM>(dc.GetSafeHdc{)), 0); // Center icon in client rectangle int cxlcon = GetSystemMetrics(SM_CXICON); int cylcon = GetSystemMetrics(SM_CYICON); CRect rect; GetClientRect(&rect); int x = (rect.WidthO - cxlcon + 1) /2; int у = (rect.Height() - cylcon +1) / 2; // Draw the icon dc.DrawIcon(x, y, m_hlcon); } else { CDialog::OnPaint();
260 Часть II. Интерфейс с языками высокого уровня } // The system calls this function to obtain the cursor to display while // the user drags the minimized window. HCURSOR CtestdllDlg::OnQueryDraglcon() return static cast<HCURSOR>(m hlcon); void CtestdllDlg::OnBnClickedButtonl() { // TODO: Add your control notification handler code here UpdateData(TRUE); add2Edit = add2(il, i2); submul5Edit = submul5(il, i2); UpdateData(FALSE); Импортируемые из DLL функции должны быть объявлены, что сделано в следующих строках программного кода: int declspec(dllimport) add2(int il, int i2); int dec1spec(dllimport) submul5(int il, int i2); В проект приложения необходимо включить библиотеку импорта addsub.iib, а в рабочий каталог приложения скопировать файл addsub.dll. Вид окна работающего приложения показан на рис. 8.3. хг Рис. 8.3. Окно приложения, использующего функции библиотеки addsub.dll
Глава 8. Разработка библиотек динамической компоновки (DLL) на ассемблере 261 До сих пор мы рассматривали явную загрузку DLL с использованием биб- библиотеки импорта. Другим, весьма распространенным методом является ди- динамическая загрузка библиотеки во время выполнения приложения. В этом случае нет необходимости связывать приложение с библиотекой импорта. Для динамической загрузки DLL используется функция LoadLibrary. Функция возвращает дескриптор экземпляра, который ссылается на DLL. В случае ошибки возвращается значение null. Чтобы использовать любую функцию, экспортируемую из DLL, необходимо вызвать функцию GetProcAddress с дескриптором экземпляра библиотеки и именем функции. Функция GetProcAddress возвращает указатель на вызываемую из DLL функцию, или null в случае ошибки. После завершения работы библиотеки ее следует удалить из памяти путем вызова функции EreeLibrary. Для демонстрации метода динамической загрузки DLL я воспользуюсь раз- разработанной библиотекой addsub.dll из предыдущего примера. Все, что нужно сделать — это разработать приложение в C++ .NET. Исходный текст приложения показан в листинге 8.10. : Листинг 5.10, Демонстрация динамической ззгрузки DLL // testsub2.cpp : Defines the entry point for the console application. ¦include "stdafx.h" #include <windows.h> int _tmain(int argc, _TCHAR* argv[J) { typedef UINT (*LPFNDLLFUNC)(UINT,UINT); LPFNDLLFUNC add2, submul5; HINSTANCE hDll - LoadLibrary("addsub"); if (!hDll) { printf("Unable to load library\n"); cfetchar () ; exit A); } add2 = (LPFNDLLFUNC)GetProcAddress (hDll, lfadd2"); submul5 = (LPFNDLLFUNC)GetProcAddress(hDll, "submul5"); if ((add2 == NULL)I I(submul5 == NULL))
262 Часть II. Интерфейс с языками высокого уровня printf("Unable to load functions!\n"); FreeLibrary(hDll); getchar(); exit A); } int il = 5; int i2 = -3; int ires = add2(il, i2); printf(" add2 (%d, %d)\t = %d\n", il, i2, ires); ires = submul5(il, i2); printf(" submul5 (%d, %d) = %d\n", il, i2, ires); FreeLibrary(hDll); getchar(); return 0; Проанализируем исходный текст приложения. Прежде всего, определим указатель на функцию, принимающую два целочисленных параметра, и проинициализируем переменные add2 и submuis как указатели на функцию: typedef UINT (*LPFNDLLFUNC)(UlNT,UINT); LPFNDLLFUNC add2, submul5; Функция LoadLibrary выполняет загрузку модуля DLL в память и в случае успешного завершения возвращает дескриптор загруженного модуля, ина- иначе — 0. В этом случае происходит выход из программы: HINSTANCE hDll = LoadLibrary("addsub"); if (IhDll) { printf ("Unable to load libraryW); getchar(); exit A); } После успешной загрузки нужно получить указатели на ячейки памяти, куда загружены функции из DLL: add2 = (LPFNDLLFUNC)GetProcAddress(hDll, "add2")/ submul5 = (LPFNDLLFUNC) Get ProcAddress (hDll, flsubmul5") ; if ((add2 == NULL)||(submul5 == NULL))
Глава 8. Разработка библиотек динамической компоновки (DLL) на ассемблере 263 I printf("Unable to load functions!\n"); FreeLibrary(hDll); getchar(); exit A); В случае успешного выполнения этого блока программного кода в перемен- переменных add2 и submui5 находятся начальные адреса исполняемых образов (executive images). Операторы int ires = add2(il, i2) ; ires = submul5(il, i2); вызывают функции DLL на исполнение. После работы с данным экземпля- экземпляром DLL или для загрузки его другой версии необходимо освободить деск- дескриптор библиотеки С ПОМОЩЬЮ ФУНКЦИИ FreeLibrary. Вид окна работающего приложения показан на рис. 8.4. Рис. 8.4. Окно приложения, демонстрирующего динамическую загрузку функций add2 и submu!5 Мы рассмотрели пример динамической загрузки DLL, когда сама библиоте- библиотека была создана с помощью мастера приложений C++ .NET, и в ней был использован объектный модуль на ассемблере. Однако DLL, созданные с помощью макроассемблера MASM, без каких-либо изменений в исходном тексте можно динамически загружать в процессе работы приложения. Раз- Разработаем DLL на ассемблере, содержащую функции add2 и sub2, выпол- выполняющие сложение и вычитание двух целых чисел соответственно. Исходный текст ассемблерного модуля представлен в листинге 8.11. addsub2.asm- 686
264 Часть II. Интерфейс с языками высокого уровня .model flat,С option casemap :none .code LibMain proc hlnstDLL:DWORD, reason:DWORD, unused:DWORD mov EAX, 1 ret LibMain endp add2 proc push EBP mov EBP, ESP mov EAX, [EBP+8] ;il add EAX, [EBP+12] /+i2 pop EBP ret add2 endp sub2 proc push EBP mov EBP, ESP mov EAX, [EBP+8] /il sub EAX, [EBP+12] ;-i2 pop EBP ret sub2 endp end LibMain Для создания библиотеки динамической компоновки необходим файл опи- описания экспортируемых функций addsub2.def, содержащий строки: LIBRARY addsub2 EXTORTS ' add2 sub2 Библиотеку addsub2.dll можно сгенерировать с помощью последовательно- последовательности команд @echo off if exist addsub2.obj del addsub2.obj if exist addsub2.dll del addsub2.dll
Глава 8. Разработка библиотек динамической компоновки (DLL) на асоемблере 265 ml /с /coff addsub2.asm Link /SUBSYSTEM:WINDOWS /DLL /DEF:addsub2.def addsub2.obj dir addsub2.* pause Исходный код консольного приложения (названного test_asmdyn), которое вызывает функции из библиотеки addsub2.dll, показан в листинге 8.12. // TEST_ASMDYN.срр : Defines the entry point for the' console application. #include "stdafx.h" #include <windows.h> int _tmain(int argc, JTCHAR* argv[]} { typedef UINT (*LPFNDLLFUNC)(OINTfUINT); LPFNDLLFUNC add2, sub2; HINSTANCE hDll = LoadLibrary("addsub2"); if (ihDll) { printf("Unable to load library\n"); getchar(); exit A); } add2 = (LPFNDLLFUNC)GetProcAddress(hDll, "add2"); sub2 = (LPFNDLLFUNC)GetProcAddress(hDll, "sub2"); if ((add2 == NULL) I I (sub2 = NULL)) { printf("Unable to load functions!\n"); FreeLibrary(hDll); getchar(); exit A); } int il = -5; int i2 = -13; int ires = add2(il, i2) ; printf(" add2 (%d, %d)\t = %d\n", il, i2, ires);
266 Часть II. Интерфейс с языками высокого уровня ires = sub2(il, i2); printfГ sub2 (%d, %d) = %d\n", il, i2, ires); FreeLibrary(hDll); getchar(); return 0; Как видно из последних двух листингов, DLL, написанная на ассемблере и загружаемая динамически, очень удобна и не требует каких-либо дополни- дополнительных директив и соглашений. Окно использующего ее приложения пока- показано на рис. 8.5. Рис. 8.5. Окно приложения, демонстрирующего динамическую загрузку функций из библиотеки, разработанной на ассемблере Хочу сделать важное замечание относительно применения DLL, написан- написанных на ассемблере (хотя это важно и для библиотек, написанных на C++). Компилятор C++ .NET по умолчанию использует соглашение cdeci. При разработке DLL на ассемблере необходимо учитывать этот факт, и компи- компилировать модули в соответствии с этим соглашением. Если, например, в ас- ассемблерном модуле с исходным текстом DLL в командах возврата указать параметры (выделено жирным шрифтом в листинге 8.13), то последствия будут весьма неприятные. addsub2.asm- .686 .model flat,С option casemap :none .code LibMain proc hlnstDLL:DWORD, reason:DWORD, unused:DWORD mov EAX, 1
Глава 8. Разработка библиотек динамической компоновки (DLL) на ассемблере 267 ret LibMain endp add2 proc push EBP mov EBP, ESP mov EAX, [EBP+8] ;il add EAX, [EBP+12] ;+i2 pop EBP ret 8 add2 endp Компилятор MASM откомпилирует исходный модуль без ошибок, но при вызове функции из главной программы приложение завершится аварийно (рис. 8.6). Рис. 8.6. Аварийное завершение программы при несоблюдении соглашения о вызовах В принципе, можно настроить компилятор C++ .NET на использование соглашений о вызовах, отличных от cdecl, но это может привести к ус- усложнению процесса отладки приложения. Поэтому, если нет особой необ- необходимости, проще настроить DLL в соответствии с опциями по умолчанию, принятыми в C++ .NET. На этом рассмотрение вариантов использования ассемблера при разработке DLL можно закончить. Этот анализ не является исчерпывающим, и некото- некоторые вопросы, возможно, оказались незатронутыми. Я надеюсь, что материал этой главы поможет читателю решать довольно трудные задачи разработки и применения библиотек динамической компоновки с использованием языка ассемблера.
ЧАСТЬ III Встроенный ассемблер Visual C++ .NET 2003 И ЕГО ИСПОЛЬЗОВАНИЕ
Глава 9 Базовые структуры встроенного ассемблера Visual C++ .NET 2003 Эта глава посвящена применению встроенного ассемблера C++ .NET 2003 для оптимизации приложений. Встроенный ассемблер является весьма эф- эффективным средством для повышения производительности приложений, и не случайно, что фирма Microsoft сделала встроенный ассемблер частью среды разработки. Обзор средств встроенного ассемблера касается только процессо- процессоров Pentium фирмы Intel, хотя методика использования встроенного ассемб- ассемблера может успешно применяться и с другими типами процессоров. На заре развития аппаратно-программных средств компьютеров наличие языка низкого уровня в составе С позволяло осуществлять управление ПК с высокой эффективностью. Операционная система MS-DOS давала возмож- возможность пользовательским приложениям полностью контролировать персо- персональный компьютер, а сочетание ассемблера и С в программах позволяло разработчикам писать высокопроизводительные программы. С приходом операционных систем Windows все изменилось. Программа все еще могла использовать ассемблер для управления и аппаратурой компью- компьютера, и, частично, операционной системой, но только в Windows 95/98/ME. Другие операционные системы, такие как Windows NT/2000/XP, резко огра- ограничили возможности пользователя контролировать работу как операцион- операционной системы, так и аппаратуры самого ПК. Казалось, что роль ассемблера в разработке программ, равно как и в повышении эффективности работы приложений, сошла на нет. Встроенный ассемблер многих языков высокого уровня в середине 1990-х воспринимался скорее как дань прошлому, чем как серьезное средство раз- разработки программ. Однако со временем выяснилось, что языки высокого уровня, несмотря на обширные библиотеки функций, в большинстве случа- случаев генерировали не очень эффективный код. Как это ни кажется странным, но новые поколения процессоров требовали новых подходов к проблеме
272 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование оптимизации, а их реализацию мог обеспечить только язык низкого уровня. Кроме того, с появлением операционных систем Windows NT/2000/XP резко обострилась проблема работы приложений в режиме реального времени. Все эти причины заставили ведущие фирмы-производители, такие как Microsoft, Borland, IBM и Intel, усовершенствовать встроенный ассемблер средств раз- разработки на языках высокого уровня. Встроенный ассемблер оказывается весьма эффективным для оптимизации циклических вычислений, для обработки больших объемов данных и реали- реализации высокопроизводительных математических вычислений. Алгоритмы и отдельные функции, разработанные на ассемблере, широко используются при написании драйверов устройств и системных служб Windows. Следует учитывать и тот факт, что современные компиляторы языков высокого уровня (не только C++ .NET) не учитывают на 100% возможности послед- последних поколений процессоров. Это объективная закономерность, связанная с архитектурой средств проектирования на языках высокого уровня. Реализо- Реализовать полностью вычислительные возможности процессора можно только с помощью языка ассемблера. В настоящее время дискуссии о том, нужен ли ассемблер разработчикам приложений на языках высокого уровня, уже не ведутся, т. к. стало понят- понятно, что этот язык является неотъемлемой частью всех программ и одним из основных средств улучшения производительности приложений на языках высокого уровня. Если сравнивать преимущества и недостатки встроенного ассемблера по сравнению с автономными компиляторами, такими как MASM фирмы Microsoft или IA-32 фирмы Intel, то однозначного ответа на вопрос "Что луч- лучше — встроенный ассемблер или отдельный компилятор?" мы не получим. К преимуществам автономных компиляторов ассемблера можно отнести возможность написания программного кода, в котором расход ресурсов компьютера (памяти и процессорного времени) будет минимальным. От- Отдельно скомпилированные модули, представленные в виде объектных файлов, легко использовать при тиражировании алгоритмов для других программ. Встроенный ассемблер C++ .NET не позволяет создавать отдельные модули для использования их в других приложениях, хотя и здесь есть выход: мож- можно написать библиотеки динамической компоновки (DLL) практически полностью на встроенном ассемблере. Встроенный ассемблер обладает пре- преимуществом в плане интеграции со средой программирования и использо- использования многих возможностей, которые предоставляет эта среда разработчику. К недостаткам встроенного ассемблера можно отнести его относительно же- жесткую зависимость от компилятора C++ .NET и, как следствие этого, боль- большие трудности при отладке приложения с требуемой производительностью.
Глава 9. Базовые структуры встроенного ассемблера Visual C++ .NET 2003 273 Среда разработки Microsoft Visual C++ .NET включает в себя мощнейшие средства поддержки программирования на языке ассемблера. Любой блок ассемблерного кода, встречающийся в программе, должен начинаться с ключевого слова asm и заключаться в фигурные скобки, как, например: _asm { mov EAX, vail sub EAX, EBX > Можно применять альтернативную форму записи команд ассемблера в строку. Предыдущий фрагмент кода в этом случае будет выглядеть так: _asm mov EAX, vail _asm sub EAX, EBX Допускается и третья форма написания ассемблерного кода — в одну строку: _asm mov EAX, vail. _asm sub EAX, EBX Среда программирования Visual C++ .NET предлагает весьма удобный спо- способ использования встроенного ассемблерного кода в виде макросов. Мак- Макрос можно вставить в нужное место программы, однако следует помнить несколько простых правил: ? не забывать брать блок asm в скобки; ? не забывать включать ключевое слово asm в начало каждой ассемблер- ассемблерной инструкции; П использовать более ранний стиль для комметариев (/* комментарий */) вместо ассемблерного стиля (;) или однострочного варианта С (// ком- комментарий). Следующий фрагмент показывает, как создать простой макрос для вывода байта данных в параллельный порт принтера: #define PORTIO378 asm /* Port output */ { asm mov AL,0x3 asm mov EDX, 0x378 asm out EDX, AL ) Этот макрос можно записать и по-другому: ttdefine PORTIO378 { asm mov AL, 0x3 asm mov EDX, 0x378 asm out EDX, AL}
274 Часть III. Встроенный ассемблер Visual C++ .NET 2003 й его использование Макрос, написанный на ассемблере, может принимать один или несколько параметров. В противоположность обычному макросу, написанному на C++, ассемблерный не возвращает результат. Поэтому такие макросы нель- нельзя использрвать в выражениях на C++. Нужно быть очень внимательным при использовании ассемблерных макросов с параметрами. Например, вы- вызов макроса в функции, используемой в другой функции, объявленной как jfastcaii, может вызвать непредсказуемые результаты. Немного о терминологии. В языке C++ для определения отдельных под- подпрограмм принято использовать термин "функция" независимо от того, воз- возвращает она результат или нет. В дальнейшем по тексту мы будем придер- придерживаться этого определения. У программистов, использующих ассемблер MASM, сразу может возникнуть вопрос: в какой мере среда разработки C++ .NET поддерживает синтаксис этого языка. Многие конструкции MASM, такие как db, dw, dd, dq, df или операторы dup и this, не поддерживаются. Встроенный ассемблер не под- поддерживает и такие директивы, как struc, record, width, mask. Операторы ассемблера length, size или type ограничены в применении в Visual C++ .NET. Их нельзя применить с оператором dup, поскольку для определения данных директивы db, dw, dd, dq и df не используются. Однако их можно использовать для определения размеров переменных следующим образом: ? оператор length возвращает число элементов массива или единицу для обычных переменных; О оператор size возвращает размер переменной языка С или C++; ? оператор type возвращает размер переменной. Если переменная указывает на массив, то этот оператор возвращает размер одного элемента массива. Например, если в программе используется массив целых чисел из 20-ти эле- элементов, объявленный 'как: int iarray[20], то результат применения этих операторов ассемблера будет выглядеть так, как показано в табл. 9.1. Таблица 9.1. Соответствие операторов ассемблера операторам C++ Оператор _asm Аналог оператора в С Размер LENGTH iarray sizeof(iarray) / 20 sizeof(iarray[0]) SIZE iarray sizeof(iarray) 80 TYPE iarray sizeof(iarray[0]) 4
Глава 9. Базовые структуры встроенного ассемблера Visual C++ .NET 2003 275 Комментарии в исходном тексте программы отделяются от операторов, как и в MASM, точкой с запятой, например, _asm { mov EAX, vail ; Это комментарий к первой строке sub EAX, EBX ; Это комментарий ко второй строке Поскольку команды встроенного ассемблера чередуются с операторами C++, то они могут ссылаться на структуры и переменные, используемые в C++ .NET. В ассемблерном блоке могут использоваться разнообразные эле- элементы языка C++: ? символы, включая метки, переменные и имена функций; ? константы, в том числе символьные и строковые; П макросы и директивы препроцессора; О комментарии в стиле С (/**/ и //); ? имена typedef, используемые обычно вместе с операторами ptr и type или для доступа к элементам объединения (union) или структуры (structure). Внутри ассемблерного блока можно определять целочисленные константы, соответствующие правилам, принятым как в C++, так и' в ассемблере. На- Например, символ пробела может быть записан и как 0x20, и как 20h. Допускается использование директивы define для определения констант. Такое определение будет действовать как в ассемблерном блоке, так и в программе на C++. Прежде чем продолжить рассмотрение средств встроеного ассемблера, раз- разработаем небольшой пример, иллюстрирующий теоретический материал. В этом примере ассемблерный макрос вычисляет разность двух целых чисел. Исходный текст программы приведен в листинге 9.1. ;... :..{ // USE_ASM_MACRO_IN_C.срр : Defines the entry point for the console // application. #include "stdafx.h" #define sub2(xl,x2) {_asm mov EAX, xl _asm sub EAX, x2 _asm mov xl, EAX} int _tmain(int argc, TCHAR* argv[])
276 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование int il = 357; int i2 = -672; sub2(il, i2); printf("il - i2 = %d\n", il) ; getchar(); return 0; Окно приложения показано на рис. 9.1. Рис. 9.1. Окно приложения, показывающего применение ассемблерного макроса Существуют некоторые особенности применения операторов C++ в ассемб- ассемблерных блоках. В блоке asm нельзя использовать специфические для языка C++ операторы. В то же время, некоторые операторы совершенно по- разному трактуются в ассемблере и в C++. Например, оператор квадратных скобок [ ] в C++ используется для указания размеров массива. Во встроен- встроенном ассемблере этот же оператор применяется для индексирования доступа к переменным. Если неправильно применять операторы в блоке asm, то обнаружить ошибки в программе будет очень трудно. Следующий пример показывает правильное и неправильное использова- использования оператора квадратных скобок. Для этого разработаем приложение на C++. NET. Поместим на главную форму приложения три поля редактирования Edit, кнопку Button и три метки статического текста Label. Основная программа будет содержать функцию, написанную на встроенном ассемблере, и обра- обработчик нажатия кнопки. В обработчике будет происходить визуализация вы- вычислений, выполненных на ассемблере. Для тестирования возьмем 5- элементный массив целых чисел. Попробуем заменить в нем элемент с ин- индексом 3 (четвертый по порядку) на число -115 (число выбрано произволь- произвольно). Замену выполним двумя способами, в обоих случаях будем использо- использовать блок asm.
Глава 9. Базовые структуры встроенного ассемблера Visual C++ .NET2003 277 Для форматирования вывода и отображения элементов массива в полях ре- редактирования понадобятся переменные типа cstring, которые мы свяжем с элементами Edit. Полю Editi (метка original) присвоим переменную iOrigin, ПОЛЮ Edit2 (метка Correct) ПРИСВОИМ iAsmCorr И ПОЛЮ Edit3 (метка wrong) — iAsmWrong. При нажатии на кнопку в полях редактирования будут отображены элементы исходного массива (Editi), элементы коррект- корректно преобразованного массива (Edit2) и элементы неправильно преобразо- преобразованного массива (Edit3). Исходный текст обработчика нажатия кнопки, в котором выполняется обработка массива, представлен в листинге 9.2. #include <string.h> #define NUM_BYTES 4 void CUSING_OPERATORS_BRACKETSDlg::OnBnClickedButtonl() { // TODO: Add your control notification handler code here int arr[5] - {4, 0, 9, -7, 50}; int arrw[5], arrc[5]; memcpy(arrw, arr, NUM_BYTES * 5); memcpy(arrc, arr, NUM_BYTES * 5); int* parr = arr; int isize = sizeof(arr) /4; int cnt; CString stmp; stmp.Empty(); for (cnt = 0; cnt < isize; cnt++) { stmp.Format("%d", *parr); iOrigin = iOrigin + " " + stmp; parr++;
278 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование parr = arrc; _asm { mov EAX, -115 mov arrc[3 * TYPE int], EAX stmp.Empty(); for (cnt = 0; cnt < isize; cnt++) { stmp.Format("%d", *parr); iAsmCorr = iAsmCorr + " " + stmp; parr++; parr = arrw; _asm { mov EAX, -115 mov arrw[3], EAX stmp.Empty(); for (int cnt = 0; cnt < isize; cnt++) { stmp.Format("%d", *parr); iAsmWrong = iAsmWrong + " " + stmp; parr++; }; UpdateData(FALSE); Результат работы программы изображен на рис. 9.2. Проведем анализ программного кода обработчика. В начале программы соз- создаются две копии исходного массива с помощью операторов: memcpy(arrw, arr, NUM_BYTES * 5); memcpy(arrc, arr, NUM_BYTES * 5); Здесь копируются байты, поэтому последний параметр функции memcpy ра- равен количеству байт в массивах. Прототип функции определен в файле
Глава 9. Базовые структуры встроенного ассемблера Visual C++ .NET2003 279 string.h, поэтому в текст программы в раздел деклараций необходимо доба- добавить строчку: #include <string.h> Рис. 9.2. Окно приложения, демонстрирующего правильное и неправильное использование операторов C++ В обработчике нажатия кнопки присутствуют три цикла for, которые подго- подготавливают буферы Переменных iOrigin, iAsmCorr И iAsmWrong ДЛЯ ВЫВОДЭ элементов массивов в поля редактирования. Рассмотрим подробно, что про- происходит с массивами аггс и arrw при попытке заменить в них четвертый элемент. Для массива аггс запись числа —115 выполняется следующим об- образом: parr = аггс; // инициализация указателя _asm { mov EAX, -115 // запись числа в'регистр ЕАХ mov arrc[3 * TYPE int], EAX // запись содержимого ЕАХ по адресу // элемента с индексом 3 (правильно!) После выполнения этих команд в четвертом элементе массива находится —115. Другая ситуация с массивом arrw. Запись по адресу четвертого эле- элемента выполняется операторами: parr = arrw; _asm { mov EAX, -115 ¦ mov arrw[3], EAX // НЕПРАВИЛЬНАЯ КОМАНДА! После выполнения этих команд будут перезаписаны четыре байта памяти, начиная с элемента с индексом 3. Поскольку 4-й байт является последним для 1-го элемента массива, а последующие (с 5-го по 7-ой) захватывают вто-
280 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование рое число в массиве, то в результате содержимое первых двух элементов массива будет разрушено, что видно на рис. 9.2. Ситуации, подобные этой, могут встретиться при работе со встроенным ас- ассемблером C++ .NET, поэтому надо тщательно отслеживать все преобразо- преобразования с применением ассемблера. Как уже было сказано, в ассемблерном блоке можно ссылаться на любые символы языка C++, хотя существуют некоторые ограничения: ? в каждой ассемблерной команде может содержаться ссылка только на один символ (переменную, функцию или метку). Для использования не- нескольких символов в одной команде необходимо, чтобы все они приме- применялись в выражениях типа length, type и size; ? функции, на которые ссылаются команды ассемблерного блока, должны быть заранее объявлены в программе, иначе компилятор не сможет отли- отличить ссылку на функцию от метки; ? в ассемблерном блоке нельзя использовать символы C++, которые схожи по написанию с директивами MASM; ? в ассемблерном блоке не распознаются структуры и объединения. Наиболее ценной особенностью встроенного ассемблера C++ .NET является его способность распознавать и использовать переменные языка C++. Если в модуле, где используется ассемблер, определены, например, переменные vail и vai2, то следующая ссылка в ассемблерном блоке будет корректной: _asm { mov EAX, vail add EAX, val2 } Как известно, функции в языке C++ возвращают результат в основную программу, используя оператор return. Например, следующая функция (назовем ее Muiints) возвращает в основную программу значение il * 12 + 100 (ЛИСТИНГ 9.3). Функиин, ио^воущпющая ролулычп с помощью оператора int CReturnValueinregisterEAXwithinlineassernblerDlg::MulInts(int il, int i2) { int valMul; _asm { mov EAX, il mov EBX, i2
Глава 9. Базовые структуры встроенного ассемблера Visual C++ .NET 2003 281 mul xchg add mov return valMul EBX EAX, EDX EDX, 100 valMul, EDX i Встроенный ассемблер позволяет обходиться без оператора return при воз- возвращении результата, используя для этого регистр еах. Та же самая функция Mulints при определенных изменениях исходного текста может использо- использовать такую возможность (листинг 9.4). ¦ Листинг 9.4. Функция, сокращающая результат в регистре еак int CRetumValueinregisterEAXwithinlineassemblerDlg::Mulints(int il, int 12) { _asm { mov EAX, il mov EBX, i2 mul EBX add EAX, 100 Несмотря на то, что функция не возвращает результат посредством операто- оператора return, компилятор не выдаст сообщение об ошибке. При написании ассемблерного кода нет необходимости сохранять регистры евх, esi и edi. Однако если регистры используются в программе, то компи- компилятор будет сохранять их при вызове функции и автоматически восстанав- восстанавливать после выхода из нее. При частом вызове такой функции может не- несколько снизиться быстродействие. Если ваша программа использует команды cid или std, то необходимо вос- восстанавливать значение флага направления при выходе из функции. Довольно часто возникает необходимость использовать библиотечные функ- функции C++ .NET в ассемблерных блоках или макросах. Сочетание ассемблер- ассемблерных команд и библиотечных функций в ассемблерном блоке позволяет как уменьшить размер кода, так и увеличить быстродействие приложения. Для использования такой возможности нужно четко представлять себе механиз-
282 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование мы взаимодействия встроенного ассемблера и стандартных функций C++ .NET. Я покажу это на примере и затем проведу анализ исходного текста. В качестве такой функции возьмем, например, printf. Основная программа представляет собой консольное приложение, состоящее практически из од- одного ассемблерного блока, в котором выполняется вычитание двух целых чисел и вывод результата на экран с помощью функции printf. Исходный текст программы показан в листинге 9.5. Лис?инг У.Ь. Испопьзоио^ие оиолиоточиои функция рг:м".¦;:::: и исс*.-молорном // CALL_C_FUNC_IN_INLINEASM.cpp : Defines the entry point for the console // application. % #include "stdafx.h" #include <stdio.h> int _tmain(int argc, _TCHAR* argv[]) { int il, ±2, ires; char cl[] = "Result of substraction = %d\n"; while (true) { printf("\nEnter il: "); scanf("%d", &il); printf("Enter i2: "); scanf("%d", &i2); _asm { mov EAX, il sub EAX, i2 mov ires, EAX push ires lea EAX, cl push EAX call printf add ESP, 8 return 0;
Глава 9. Базовые структуры встроенного ассемблера Visual C++ .NET2003 Вид окна работающего приложения показан на рис. 9.3. 283 Рис. 9.3. Окно приложения, демонстрирующего вызов функции C++ из ассемблерного блока Первые три строки блока asm {...} нашей программы понятны, и оста- останавливаться на их анализе я не буду. При вызове функции printf необхо- необходимо передать ей параметры. Чтобы правильно это сделать, представим се- себе, как бы выглядел обычный оператор вывода результата на экран, в котором присутствует функция printf: printf("Result of substraction - %d\n", ires) Функции printf требуются два параметра — адрес строки и значение пере- переменной ires. Оба параметра извлекаются из стека справа налево, т. е. пер- первым идет ires, затем адрес строки. Фрагмент кода, выполняющий вызов printf, реализован строками: push ires lea EAX, cl push EAX call printf Поскольку для всех проектов в C++ .NET используется соглашение о вызо- вызовах cdeci, то вызывающая программа или функция должна позаботиться об очистке стека. Это выполняется командой add esp, 8. Этот момент очень важен. Если забыть очистить стек или ошибиться в количестве уда- удаляемых байт, то наступит немедленный крах приложения. Если команду add esp, 8 закомментировать и откомпилировать приложение, то после за- запуска программы отладчик выдаст ошибку (рис. 9.4). И последний момент, касающийся этого примера. Обратите внимание на то, что нет необходимости изменять имя функции printf, как это бывает в слу- случае вызова отдельно скомпилированной ассемблерной процедуры!
284 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование Рис. 9.4. Сообщение отладчика при неправильной работе со стеком Приведу еще один, более сложный пример. Программа должна вводить два числа как символьные строки, преобразовывать их к целочисленному фор- формату с помощью библиотечной функции atoi (ASCII to Integer), выполнять их умножение и выводить результат на экран. Для хранения символьного представления целых чисел и и i2 используются произвольно выбранные переменные строкового типа si [16] и s2[i6]. Преобразования, умножение и вывод результата выполним с помощью блока ассемблерных команд. Ис- Исходный текст программы представлен в листинге 9.6. // ATOI_INLINE.cpp : Defines the entry point for the console application. #include "stdafx.h" int _tmain(int argc, _TCHAR* argv[]) int il, i2, ires; char si[16], s2[16]; - char cl[] = "il * i2 = %d\n"; while (true) printf("\nEnter il: "); scanf("%s", si); printf ("Enter' 12: ") ; scanf("%s", s2); __asm { lea EAX, si push EAX
Глава 9. Базовые структуры встроенного ассемблера Visual C++ .NET2003 call atoi add ESP, 4 mov il, EAX lea EAX, s2 push EAX call atoi add ESP, 4 mov i2, EAX mov EAX, il mov EBX, i2 imul EBX mov ires, EAX push ires lea EAX, cl push EAX call printf add ESP, 8 285 return 0; Окно работающего приложения изображено на рис. 9.5. Рис. 9.5. Окно приложения, демонстрирующего применение библиотечных функций atoi и printf
286 Часть III. Встроенный ассемблер Visual C++ .NET 2003 и его использование Остановимся на ключевых моментах этой программы. Функция atoi имеет синтаксис int atoi(const char* str); где переменная str представляет собой строку. Функция возвращает значе- значение целого типа. Следовательно, atoi принимает единственный параметр — адрес строки. Ассемблерная реализация вызова этой функции (для строки si и переменной ii) в нашем примере имеет вид: lea EAX, si push EAX call atoi add ESP, 4 mov il, EAX Результат выполнения функции возвращается, как обычно, в регистре еах и запоминается в переменной il. Аналогичное представление будет и для пе- переменных s2 И i2. Умножение выполняется с помощью блока команд mov ЕАХ> il mov EBX, 12 imul EBX Вывод результата выполняется функцией printf так же, как и в предыду- предыдущем примере. Не следует забывать очищать стек командой add esp, n, где n — число байт, занимаемых параметрами. Хочу упомянуть еще один важный аспект применения библиотечных функ- функций C++ .NET. Для разработки консольных приложений я использовал каркас Win32 с указанием типа console application. При разработке кар- каркаса приложения с установленной опцией Empty project понадобится ука- указывать файл заголовка, в котором описана та или иная функция. Наш последний, пример самый сложный и демонстрирует технику примене- применения встроенного ассемблера в различных комбинациях. Разработаем прило- приложение диалогового окна с помощью мастера приложений C++ .NET. При- Приложение должно отображать на экране результат вычисления по формуле (Х1-Х2)* (Х1+Х2). Поместим на главную форму приложения три элемента редактирования Edit control. Два поля редактирования принимают числа XI и Х2, третье поле редактирования служит для вывода результата. Помес- Поместим три элемента статического текста static Text и кнопку Button. Резуль- Результат вычислений будет выведен при щелчке левой кнопкой мыши на кнопке. Поставим в соответствие элементам управления Edit Control переменные целого типа xi, Х2 и ximulx2, где xi и Х2 — входные переменные и ximulx2 — результат. Кроме того, будем использовать две вспомогательные
Глава 9. Базовые структуры встроенного ассемблера Visual C++ .NET 2003 287 переменные и и 12 целого типа (выделены жирным шрифтом в листинге). Для промежуточных вычислений разработаем следующие функции с приме- применением встроенного ассемблера: ? функцию Add2ints — для вычисления суммы XI + Х2; П функцию Sub2ints — для вычисления разности xi - Х2; О ФУНКЦИЮ Imul2 — ДЛЯ ВЫЧИСЛеНИЯ (XI - Х2) * (XI + Х2). После создания каркаса функций следует закомментировать оператор return о (выделен в листинге жирным шрифтом). Наши функции возвра- возвращают результат в регистре еах, и этот оператор не нужен. Обратите внима- внимание На ТО, Как ПРОИСХОДИТ Обращение К ФУНКЦИЯМ Add2ints И Sub2ints ИЗ функции imui2. Параметры при вызове передаются через стек обычным способом, при этом не нужно использовать команду add esp, n для очист- очистки стека, компилятор автоматически включает команды пролога-эпилога для функций. Поэтому включение команды add esp, n после команд call вызова функций вызовет ошибку в стеке и крах программы! В остальном программный код не вызывает затруднений при анализе. Исходный текст программы показан в листинге 9.7. //CALL_FROM_INLINEASMDlg.cpp : implementation file #include "stdafx.h" #include "CALL_FROM_INLINEASM.h" #include "CALL_FROM_INLINEASMDlg.h" ¦include ".\call_from_inlineasmdlg.h" #ifdef _DEBUG #define new DEBUG_NEW #endif int II, 12; int CCALL_iFROM_INLINEASMDlg::Add2ints(int il, int i2) { _asm { mov EAX, il
288 Часть III. Встроенный ассемблер Visual C++ .NET 2003 и его использование add EAX, i2 //return 0; int CCALL_FROM_INLINEASMDlg::Sub2ints(int il, int i2) _asm { mov EAX, il sub EAX, i2 //return 0; int CCALL_FROM_INLINEASMDlg::Imul2(void) _asm { push 12 push II call Add2ints mov EDX, EAX push 12 push II call Sub2ints mov EBX, EAX mov EAX, EDX imul EBX //return 0; void CCALL_FROM_INLINEASMDlg::OnBnClickedButtonl() // TODO: Add your control notification handler code here UpdateData(TRUE); II = XI;
Глава 9. Базовые структуры встроенного ассемблера Visual C++ .NET 2003 12 = Х2; X1MULX2 = Imul2(); UpdateData(FALSE); 289 Вид окна приложения показан на рис. 9.6. Рис. 9.6. Окно приложения, демонстрирующего технику применения встроенного ассемблера На этом рассмотрение возможностей встроенного ассемблера Visual C++ .NET можно закончить. В следующих главах основное внимание будет уде- уделено практическим аспектам профаммирования с использованием встроен- встроенного ассемблера.
Глава 10 Встроенный ассемблер и оптимизация приложений. Технологии ММХ и SSE В этой главе будут рассмотрены практические аспекты использования встроенного ассемблера в программах, написанных на C++ .NET. Если в предыдущей главе были изложены базовые принципы применения встроен- встроенного ассемблера, то здесь рассмотрим практические примеры использования этого средства для решения различных задач. Встроенный ассемблер наибо- наиболее часто применяется в задачах вычислительного характера, мультимедийных приложениях, для обработки символьных данных. Некоторые возможности ассемблера были продемонстрированы в главе 2, сейчас же остановимся на этом более подробно. Для демонстрации тех или иных технологий я буду использовать практиче- практические примеры. Все теоретические аспекты будут рассмотрены в контексте примеров, и необходимые пояснения будут даваться при анализе примеров. 10.1. Встроенный ассемблер и оптимизация математических операций Наш первый пример — вычисление суммы элементов массива веществен- вещественных чисел. Помимо демонстрации работы математического сопроцессора здесь будет показано, как использовать указатель для возврата результата. Разработаем приложение диалогового окна на C++ .NET. На главной форме приложения разместим два элемента редактирования Edit control, два элемента статического текста static Text и одну кнопку Button. В первом поле редактирования Editi выведем весь массив вещест- вещественных чисел, а во втором поле Edit2 будет выведен результат вычислений. Свяжем с элементами управления Editi и Edit2 две переменные — s_Array и fjsumina соответственно. Переменной sArray присвоим строковый тип cstring, а переменной f_summa — вещественный float.
Глава 10. Встроенный ассемблер и оптимизация приложений... 291 Вычисление суммы элементов массива вещественных чисел выполним с помощью функции sumReais. Эта функция имеет два параметра: адрес мас- массива и его размер. Функцию необходимо добавить в класс диалогового окна, при этом следует закомментировать оператор return о (выделен жирным шрифтом). Исходный текст этой функции и обработчика нажатия кнопки приведен в листинге 10.1. float* CSummaofRealsDlg::sumReais(float* farray, int If) float fsum; _asm { mov ESI, farray mov ECX, If dec ECX finit fldz fid [ESI] • next: add ESI, 4 fadd [ESI] loop next fstp fsum fwait lea EAX, fsum //return 0; void CSuramaofRealsDlg: :OnBnClickedButtonl () { // TODO: Add your control notification handler code here float farray[] = {9.34, 15.05, -4.32, -173.12,-88.45}; int fsize = sizeof{farray)/4; CString stmp;
292 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование UpdateData(TRUE); stmp.Empty; for (int cnt = 0; cnt < fsize; cnt++) stmp.Format("%.2f", farray[cnt]); s_Array = s_Array + " " + stmp; f_Summa = *suroReals(farray, fsize); UpdateData(FALSE); Как я уже упоминал, при вызове функции нет необходимости сохранять регистр esi. Поэтому в самом начале функции sumReais загружаем адрес массива farray в регистр esi, а его размер — в регистр есх: mov ESI, farray mov ЕСХ, If Адресом массива farray является указатель на первый элемент этого масси- массива. Содержимое регистра есх используется для организации вычисления в цикле. Самое первое значение элемента массива загружается в вершину сте- стека сопроцессора командой: fid [ESI] В каждой итерации цикла выполняется продвижение адреса к следующему элементу массива и прибавляется значение последующего элемента к со- содержимому вершины стека: next: add ESI, 4 fadd [ESI] loop next Результат суммирования сохраняется в локальной переменной f sum: fstp fsum Последняя команда ассемблерной функции: lea EAX, fsum загружает адрес переменной fsum в регкстр еах. Этот адрес функция воз- возвращает в основную программу. В обработчике нажатия кнопки мы используем строковые переменные для преобразования числовых значений в текстовый формат. Отложим рас-
Глава 10. Встроенный ассемблер и оптимизация приложений... 293 смотрение строк до следующих примеров, а сейчас проанализируем один оператор: f_Suinma = *sumReals (farray, fsize); В этом операторе f_summa — переменная вещественного типа, а функция sumReais возвращает адрес. Для получения значения по адресу используется оператор раскрытия ссылки "*" перед адресным выражением, в нашем слу- случае — перед идентификатором функции. Первый параметр, передаваемый в функцию sumReais, — адрес массива. Он может быть представлен в альтернативной форме. Как мы знаем, адрес мас- массива указывает на первый элемент, поэтому рассмотренный оператор можно записать и в другой форме: f_Summa = *sumReals(&farray[0], fsize); где для представления первого параметра в корректной форме используется оператор получения адреса "&". Окно работающего приложения показано на рис. 10.1. Рис. 10.1. Окно приложения, выполняющего подсчет суммы элементов массива вещественных чисел В следующих примерах рассмотрим реализацию простых алгоритмов сорти- сортировки и поиска максимального элемента. На этих примерах Постараемся оценить эффективность встроенного ассемблера для оптимизации программ. Вначале рассмотрим механизм поиска максимального элемента в массиве целых чисел. Используя мастер приложений Visual C++ .NET, разработаем каркас приложения на основе диалогового окна. Разместим на главной форме приложения два элемента редактирования Edit control, два элемента статического текста static Text и кнопку Button. Поиск максимального элемента целочисленного массива будет вы- выполнять функция f indMax. В качестве параметров функция принимает адрес массива и его размер. Результатом выполнения функции является значение
294 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование максимального элемента в массиве. Фрагменты кода функции findMax и обработчика нажатия кнопки приведены в листинге 10.2. ! Лисшж 1G.2. Вь:числсн^с va локально» о Mh u м а ".:. с уш : ;....; х чисел int CFIND_MAX_IN_ARRAY_OF_INTSDlg::findMax(int * pi1, int si1) int maxVal; asm { mov mov mov mov next: add mov cmp jl- push pop no change: loop ex: mov //return 0; EDI, pil ECX, sil EDX, [EDI] maxVal, EDX EDI, 4 EAX, [EDI] EAX, maxVal no change EAX maxVal next EAX, maxVal void CFINDjyiAX_INJ^RAY_OF_INTSDlg: : OnBnClickedButtonl () { // TODO: Add your control notification handler code here CString si; int il[] = {2, 33, -19, -7, 32, -90, 13}; int sil = sizeof(il)/4; si.Empty; for (int cnt = 0; cnt < sil; cnt++) si.Format("%d", il[cnt]);
Глава 10. Встроенный ассемблер и оптимизация приложений... 295 s_Array - s_Array + " " + si; }; i_Max = findMax(il, sil); UpdateData(FALSE); Рассмотрим алгоритм работы функции findMax. Первый элемент массива считается максимальным и сравнивается в цикле с последующими элемен- элементами. Если следующий элемент больше текущего максимума, то максималь- максимальным элементом становится он. Цикл повторяется до тех пор, пока не будет достигнут конец массива. Команды: mov EDI, pil mov ECX, sil загружают в регистры edi и есх, соответственно, указатель массива и его размер. Сравнение текущего значения максимума и элемента массива, а также выбор нового значения в качестве максимума выполняются группой команд: next: add mov crop jl push pop EDI, 4 EAX, [EDI] EAX, maxVal no_change EAX maxVal Функция возвращает значение максимума, как обычно, в регистре еах: mov EAX, maxVal Обработчик нажатия кнопки ничего особенного не делает. Полученное зна- значение максимума присваивается переменной i_Max, соответствующей эле- элементу управления Edit2, и выводится на экран. Окно работающего приложения изображено на рис. 10.2. Было бы интересно сравнить программы поиска максимума как с использо- использованием ассемблерной функции, так и на "чистом" C++. Ассемблерный вариант у нас есть, поэтому разработаем такую же программу на C++. Программный код обработчика нажатия кнопки приведен в лис- листинге 10.3. Фрагмент кода, выполняющий поиск максимального значения, выделен жирным шрифтом.
296Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование Рис. 10.2. Окно приложения, выполняющего поиск максимального элемента в массиве целых чисел void CFindMaxIntwithCDlg::OnBnClickedButtonl() { // TODO: Add your control notification handler code here CString si; int il[] = {2, 33, -19, -7, 32, -90, 13}; int sil = sizeof(il)/4; // Вывод элементов массива на экран si.Empty; for (int cnt = 0; cnt < sil; cnt++) { si.Format("%d", il[cnt]); s_Array = s_Array + " " + si; }; // Поиск максимального элемента в массиве целых чисел i_Max for (int cnt = 1; cnt < sil; cnt++)
Глава 10. Встроенный ассемблер и оптимизация приложений... 297 if (i_Max >•= il[cnt]) continue; else i_Max ¦ il[cnt]; UpdateData(FALSE); Программный код этого фрагмента в особых пояснениях не нуждается. По- Посмотрим теперь на дизассемблированный листинг С++-варианта програм- программы, точнее, на ту его часть, которая соответствует циклу for, вычисляюще- вычисляющему i_Max (ЛИСТИНГ 10.4). i Max = il[0] ; 0041380С mov 0041380F mov 00413812 mov eax,dword ptr [this] ecx,dword ptr [il] dword ptr [eax+7Ch],ecx for (int cnt = l;cnt < eil; cnt++) 00413815 0041381C 0041381E 00413821 00413824 00413827 0041382A 0041382D mov jmp mov add mov mov cmp jge dword ptr [cnt],1 CFindMaxIntwithCDlg::OnBnClickedButtonl+167h D13827h) eax,dword ptr [cnt] eax,l dword ptr [cnt],eax eax,dword ptr [cnt] eax,dword ptr [sil] CFindMaxIntwithCDlg::OnBnClickedButtonl+18Fh D1384Fh) if (i_Max >= il[cnt]) continue; 0041382F mov eax,dword ptr [this] 00413832 mov ecx,dword ptr [cnt] 00413835 mov edx,dword ptr [eax+7Ch] 00413838 cmp edx,dword ptr il[ecx*4] 0041383C jl CFindMaxIntwithCDlg::OnBnClickedButtonl+180h D13840h) 0041383E jmp CFindMaxIntwithCDlg::OnBnClickedButtonl+15Eh D1381Eh)
298 Часть III. Встроенный ассемблер Visual C++ .NET 2003 и его использование else i Мах « ii[cnt]; 00413840 mov 00413843 mov 00413846 mov 0041384A mov eax,dword ptr [this] ecx,dword ptr [cnt] edx,dword ptr il[ecx*4] dword ptr [eax+7Ch],edx 0041384D jmp CFindMaxIntwithCDlg::OnBnClickedButtonl+15Eh D1381Eh) Сравните дизассемблированный фрагмент кода, вычисляющего максимум, и функцию f indMax в ассемблерном варианте программы. Даже беглого взгля- взгляда достаточно, чтобы понять избыточность второго варианта. Прежде чем проанализировать дизассемблированный код, рассмотрим еще один при- пример — сортировку массива целых чисел по убыванию. Это приложение построено на основе диалогового окна. На главную форму приложения поместим два элемента редактирования Edit control, два эле- элемента статического текста static Text и кнопку Button. В одно из полей редактирования (соответствующее переменной s_src) будет выводиться ис- исходный (неупорядоченный) массив целых чисел, в другом поле (переменная s_Dst) можно будет видеть тот же массив, но упорядоченный по убыванию. Сортировку массива выполняет функция sortMax, принимающая в качестве параметров адрес массива и его размер. Программные элементы обработчи- обработчика нажатия кнопки во многом схожи с программным кодом предыдущих примеров, и останавливаться на них мы не будем. Программный код функ- функции sortMax и обработчика кнопки, в котором вызывается эта функция, приведен в листинге 10.5. void CSORT_ARRAY_BY_MAXIMUMDlg::sortMax(int* pil, int sil) int isize = sil; _asm { push EBX mov EDI, DWORD PTR pil mov EBX, EDI big_loop: mov ECX, DWORD PTR isize mov EAX, [EDI]
Глава 10. Встроенный ассемблер и оптимизация приложений. 299 next: mov cmp jl jrop change: xchg mov cont: add loop dec cmp je mov jmp ex: pop EAX, [EDI] EAX, [EDI+4] change cont EAX, [EDI+4] [EDI], EAX EDI, 4 next isize isize, 0 ex EDI, EBX big_loop EBX void CSORT_ARRAY_BY_MAXIMUMDlg::OnBnClickedButtonl() // TODO: Add your control notification handler code here CString si; int il[] = {17, -9, 31, -7, 4, 76, 47, -59}; int sil = sizeof(il)/4; si.Empty; for (int cnt = 0; cnt < sil; cnt++) sl.Format("%d", i sSrc = sSrc + " " + si; sortMax(i1, si1); si.Empty; for (int cnt =0; cnt < sil; cnt++) si.Format("%d", il[cnt]);
300 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование s_Dst = s_Dst + " " + sl; UpdateData(FALSE); Окно работающего приложения изображено на рис. 10.3. Рис. 10.3. Окно приложения, выполняющего сортировку массива целых чисел по убыванию Решим ту же задачу сортировки массива при помощи программы, написан- написанной на "чистом" C++, и рассмотрим дизассемблированный листинг, точнее, тот его фрагмент, который выполняет сортировку. Фрагменты программного кода на C++, с помощью которых выполняются сортировка и вывод резуль- результата, представлены в листинге 10.6. Жирным шрифтом выделен интересую- интересующий нас участок программного кода. void CSortbyDecreasewithCNETDlg::OnBnClickedButtonl() // TODO: Add your control notification handler code here CString sl; int il[] = {17, -9, 31, -7, 4, 76, 47, -59}; int itmp; int size_il = sizeof(il)/4; s_S re.Empty; s_Dst. Empty; UpdateData(FALSE);
Глава 10. Встроенный ассемблер и оптимизация приложений... 301 si.Empty; // Вывод элементов исходного массива в поле редактирования for (int cnt = 0; cnt < size_il; cnt++) { si.Format("%сГ, il[cnt]); s_Src = s_Src + " " + si; }; // Сортировка массива int tSize_il = size_il; while (tSize_il != 0) { for (int cnt ¦ 0; cnt < tSize_il; cnt++) { if (il[cnt] >= il[cnt+l]) continue; else { itmp « il[cnt]; il[cnt] - iltcnt+1]; il[cnt+l] * itnp; }; }; tSize_il—; }; // Вьшод элементов отсортированного массива в.поле редактирования for (int cnt = 0; cnt < size_il;cnt++) { s1.Format("%d", il[cnt]); s_Dst = s_Dst + " " + si; };^ UpdateData(FALSE);
302 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование Я не буду детально останавливаться на анализе выделенного фрагмента ко- кода, поскольку для программистов на C++ он достаточно очевиден. Дизас- семблированный фрагмент этого участка кода представлен в листинге 10.7. Листинг 10.7. Программный код днзассем Г: пи ронянного фрагмента на О+ int tSize_il « size_il; 0041382D mov eax,dword ptr [size_il] 00413830 mov dword ptr [tSize_il],eax while (tSize_il !« 0) 00413833 onp dword ptr [tSize_il],0 00413837 je CSortbyDecreasewithCNETDlg::OnBnClickedButtonl+lE2h <P D138B2h) { for (int cnt «0; cnt < tSize_il; cnt++) 00413839 mov dword ptr [cnt],0 00413843 jmp CSortbyDecreasewithCNETDlg::OnBnClickedButtonl+184h <P D13854h) 00413845 mov eax,dword ptr [cnt] 0041384B add eax, 1 0041384E mov dword ptr [cnt],eax 00413854 mov eax,dword ptr [cnt] 0041385A cmp eax,dword ptr [tSize_il] 0041385D jge CSortbyDecreasewithCNETDlg::OnBnClickedButtonl+lD7h <$• D138A7h) { if (il[cnt] >« il[cnt+l]) continue; 0041385F mov eax,dword ptr [cnt] 00413865 mov ecx,dword ptr [cnt] 0041386B mov edx,dword ptr il[eax*4] 0041386F cmp edx, dword ptr [ebp+ecx*4-44h] 00413873 jl CSortbyDecreasewithCNETDlg::OnBnClickedButtonl+lA7h <P D13877b)
Глава 10. Встроенный ассемблер и оптимизация приложений... 303 00413875 jmp CSortbyDecreasewithCNETDlg::OnBnClickedButtonl+175h <P <413845h) else itmp = il[cnt]; 00413877 mov eax,dword ptr [cnt] 0041387D mov ecx,dword ptr il[eax*4] 00413881 mov dword ptr [itmp],ecx il[cnt] ¦ il[cnt+1]; 00413884 mov eax,dword ptr [cnt] 0041388A mov ecx,dword ptr [cnt] 00413890 mov edx,dword ptr [ebp+ecx*4-44h] 00413894 mov dword ptr il[eax*4],edx il[cnt+1] = itmp; 00413898 mov eax,dword ptr [cnt] 0041389E mov ecx,dword ptr [itmp] 004138A1 mov dword ptr [ebp+eax*4-44h],ecx 004138A5 jmp CSortbyDecreasewithCNETDlg::OnBnClickedButtonl+175h D13845h) tSize__il— ; 004138A7 mov eax,dword ptr [tSize_il] 004138AA sub eax, 1 004138AD mov dword ptr [tSize_il],eax 004138B0 jmp CSortbyDecreasewithCNETDlg::OnBnClickedButtonl+163h D13833h)
304 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование У этих двух дизассемблированных фрагментов кода есть общие черты. Если вы заметили, С++-варианты программ для работы с переменными активно используют основную память и значительно реже — регистры процессора. Ничего плохого в этом нет, однако это ведет к избыточности программного кода и замедлению быстродействия программы в целом. Это незаметно при небольших объемах вычислений и сравнительно небольших объемах дан- данных, подверженных обработке. Однако при больших размерах массивов данных снижение производительности станет заметным. Например, обмен значений двух элементов массива в реализации на язы- языке C++: itmp = il[cnt]; il[cnt] = il[cnt+l]; il[cnt+l] = itmp; представлен следующим эквивалентом ассемблерного кода: mov eax,dword ptr [cnt] mov ecx,dword ptr il[eax*4] mov dword ptr [itmp],ecx mov eax,dword ptr [cnt] mov ecx,dword ptr [cnt] mov edx,dword ptr [ebp+ecx*4-44h] mov dword ptr il [eaxM] ,edx mov eax,dword ptr [cnt] mov ecx,dword ptr [itmp] mov dword ptr [ebp+eax*4-44h],ecx В то же время, используя комбинацию команд ассемблера: mov EAX, [EDI] xchg EAX, [EDI+4] mov [EDI], EAX можно добиться повышения производительности в программе поиска мак- максимума. В этом случае значения переменных хранятся в регистрах, и вычис- вычисления выполняются очень быстро, т. к. значительно уменьшен обмен дан- данными по системной шине. То же самое относится и к оптимизации циклов. Объективно заставить компилятор C++ сгенерировать программный код с максимальным исполь- использованием регистров вместо памяти очень трудно, а во многих случаях не- невозможно. Хорошо спроектированные на ассемблере циклические вычисле- вычисления выполняются существенно быстрее и требуют меньшего числа команд.
Глава 10. Встроенный ассемблер и оптимизация приложений... 305 Рассмотрим, как работает цикл for в программе сортировки. Оператор for (int cnt = 0; cnt < tSize_il; cnt++) распадается на несколько команд ассемблера. Дизассемблированный код этого оператора я модифицирую так, чтобы он воспринимался легче. Для этого уберу ссылки на адреса физической памяти в командах переходов и в соответствующих местах заменю их метками. Модифицированный код будет выглядеть так: mov jmp LI: mov add mov L2: mov cmp jge DWORD PTR L2 EAX,DWORD EAX,1 DWORD PTR EAX,DWORD EAX,DWORD <адрес> < операторы цикла jmp LI [cnt], 0 PTR [cnt] [cnt], EAX PTR [cnt] PTR [tSize_il] > Нельзя сказать, что этот код неоптимален. Если бы вы имели в своем рас- распоряжении только регистры процессора еах, edx и есх, то для организации цикла for использовали бы, скорее всего, тот же самый алгоритм. В силу этих офаничений пришлось бы хранить переменные циклов в памяти и ка- каждый раз (как в этом фрагменте кода) извлекать их для очередной итерации. Если использовать ассемблер, то реализация оператора for при помощи стандартного алгоритма с использованием регистра есх mov ЕСХ, DWORD PTR isize loop next выполняется быстрее. Как видите, применение языка ассемблера способно решить многие проблемы оптимизации профаммы, но только если вы хо- хорошо представляете себе, что оптимизировать и как. Это касается не только C++ .NET, но и других компиляторов.
306 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование 10.2. Встроенный ассемблер и MMX-расширение Следующая очень важная тема, на которой мы остановимся, — применение встроенного ассемблера для оптимизации приложений, использующих SIMD-расширения фирмы Intel. В практическом плане SIMD реализована как две взаимосвязанные технологии обработки данных: ? ММХ-технология, с помощью которой выполняется высокоэффективная обработка данных целочисленного типа, имеющих разрядность 64 бита; ? SSE-технология, предназначенная для эффективной обработки данных вещественного типа с разрядностью 128 бит. -В Visual C++ .NET 2003 включена поддержка этих технологий. В главе 1 я достаточно подробно останавливался на теоретических аспектах применения SIMD, сейчас же рассмотрим практические аспекты применения этой тех- технологии. SIMD-технологии применяются при написании следующих типов приложений: ? кодирования-декодирования и обработки звуковых сигналов; ? распознавания речи; ? обработки и захвата видеосигналов; ? работы с ЗЭ-графикой; ? работы с ЗЭ-звуком; ? CAD/CAM; ? криптографических приложений. Программирование ММХ и SSE требует принципиально новых подходов по сравнению с разработкой классических программ. Одним из них является векторизация или преобразование последовательно выполняемого про- программного кода в параллельно выполняемый. Основным преимуществом архитектуры SIMD является то, что многие вычисления можно выполнять одновременно над несколькими операндами, что и используется при векто- векторизации программного кода. Применение ассемблера в SIMD-расширениях несет в себе два преимущества: ? возможность написания компактного и быстрого кода в критических участках приложения; ? высокоэффективное кодирование с использованием специальных инст- инструкций ассемблера для SIMD-расширений. Начнем с рассмотрения операций, использующих MMX-расширение. Не- Несмотря на широкое применение ММХ, практических примеров программ-
Глава 10. Встроенный ассемблер и оптимизация приложений... 307 ного кода с детальными объяснениями встречается очень мало. Документа- Документация Microsoft дает весьма скудные сведения об использовании технологии ММХ в разработках. Прежде чем продемонстрировать возможности встро- встроенного ассемблера C++ .NET в плане оптимизации ММХ-приложений, не- необходимо разобраться, как эта технология реализована в принципе. Перед тем как разрабатывать программы с поддержкой MMX-расширения, следует убедиться в том, что данный тип процессора поддерживает эту тех- технологию. Для этого можно использовать ассемблерную команду cpuid. Пе- Перед выполнением этой команды необходимо поместить в регистр еах значе- значение 1. После выполнения команды проверка 23-го бита в регистре edx показывает, поддерживается ли ММХ процессором. Процессор поддержива- поддерживает ММХ, если бит равен единице. Простейшее консольное приложейие, ис- исходный текст которого показан в листинге 10.8, выполняет проверку про- процессора. // MMX_CPUID_TEST.cpp : Defines the entry point for the console // application. #include "stdafx.h" int _tmain(int argc, _TCHAR* argv[]) { bool supMMX = true; _asm { mov supMMX, 1 mov EAX, 1 cpuid test EDX, 0x800000 jnz sup mov supMMX, 0 sup: }; if (supMMX)printf("MMX is supported!\n"); else printf("MMX not supported!\n"); getchar(); return 0;
308 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование Для выполнения операций SIMD (MMX, SSE) в C++ .NET используются так называемые собственные функции (intrinsics). Эти функции имеют сле- следующий синтаксис: _тт_<операция>_<суффикс> где <операция> указывает на тип операции (например, add для операции сложения и sub для вычитания). Суффикс указывает на данные, с которыми инструкция оперирует. Первые две литеры каждого суффикса определяют, упакованы ли данные (литера р), упакованы ли они с расширением (ер) или представляют собой скалярный тип (s). Остальные литеры суффикса опре- определяют тип данных: ? s — переменная вещественного типа с одинарной точностью; ? d — переменная вещественного типа с двойной точностью; О И28 — знаковое 128-битовое целое; П i64 — знаковое 64-битовое целое; П иб4 — беззнаковое 64-битовое целое; ? i32 ~ знаковое 32-битовое целое; ? и32 — беззнаковое 32-битовое целое; ? не — знаковое 16-битовое целое; ? ui6 — беззнаковое 16-битовое целое; П 18 — знаковое 8-битовое целое; П и8 — беззнаковое 8-битовое целое Собственные функции подобны обычным функциям C++. Они оперируют.с целочисленными 64-разрядными величинами. Операции могут выполняться над упакованными байтами (8x8), словами A6x4), двойными словами C2x2) и учетверенным словом F4x1). Независимо от способов обработки 64- разрядной величины в C++ .NET принято обозначение шб4. Собственные функции делятся на несколько основных групп: ? функции общего назначения. К ним относятся функции перемещения, упаковки и распаковки целочисленных данных; П функции арифметики упакованных чисел. К ним относятся функции, выполняющие операции сложения, вычитания, умножения целых чисел. Легко понять, почему в этом списке отсутствуют функции деления: в об- общем случае результатом деления является вещественное число; ? функции сдвига, которые выполняют логический сдвиг операндов влево и вправо на определенное число позиций; ? функции двоичной логики (and, or, xor), которые выполняют логические операции над операндами;
Глава 10. Встроенный ассемблер и оптимизация приложений... 309 ? функции сравнения, которые выполняют сравнение операндов; ? функции установки байт, слов и двойных слов в 64-разрядных операндах. Собственные функции позволяют добиться значительного повышения про- производительности приложений. Все они построены с использованием ас- ассемблерных инструкций, однако удобство применения приводит к избыточ- избыточности кода этих функций. Все собственные функции не оперируют напрямую с ММХ-регистрами, а только с одной или двумя 64-разрядными ячейками памяти. С одной сторо- стороны, это удобно, поскольку избавляет от необходимости глубокого изучения архитектуры ММХ, с другой стороны — затрудняет оптимизацию вычисли- вычислительных алгоритмов. Поясню сказанное на примере собственной функции int _mm_cvtsi64_si32 ( тб4 т). Эта функция преобразует младшие 32 би- бита 64-разрядной переменной в целое число. Предположим, есть фрагмент исходного текста программы: int il; m64 msres; il = _mm_cvtsi64_si32 (msres); Дизассемблированный листинг этого фрагмента кода мог бы выглядеть так: 00411C9D movq ¦ mmO,imiword ptr [msres] 00411CA4 movd eax,mmO 00411CA7 mov ecx,dword ptr [il] 00411CAD mov dword ptr [ecx],eax Последние три команды этого листинга можно легко заменить одной, если воспользоваться встроенным ассемблером: movd DWORD PTR il, mmO Более того, если использовать команды ассемблера movd и movq, которые работают напрямую с ММХ-регистрами, то это позволит избежать примене- применения промежуточных 64-разрядных переменных (в нашем случае msres). Многие программисты мало знакомы с практическими аспектами примене- применения MMX-расширений, поэтому некоторые примеры программ приводятся как с использованием собственных функций C++ .NET, так и в ассемблер- ассемблерном варианте. Как было уже сказано в главе 1, MMX-расширение позволяет выполнять операции над целочисленными данными параллельно. Потоковая модель обработки данных позволяет оптимизировать операции в мультимедийных приложениях, в обработке многобайтных последовательностей символов и чисел, в операциях сортировки и поиска. Поскольку операции над данными
310 Часть ///. Встроенный ассемблер Visual C++ .NET 2003 и его использование выполняются в ММХ-инструкциях параллельно, это позволяет выполнить команды за значительно меньшее процессорное время. Рассмотрим первый пример. Пусть в 8-байтной последовательности симво- символов необходимо заменить каждый байт его значением, увеличенным на 2. Размерность 8 выбрана мной из соображений максимального упрощения алгоритма вычислений. Вначале разработаем консольное приложение, вы- выполняющее операции с помощью собственных функций. Исходный текст приложения показан в листинге 10.9. // MMX_2_ADD_BYTES.срр : Defines the entry point for the console // application. #include "stdafx.h" #includa <nmintrin.h> int _tmain(int argc, _TCHAR* argv[]) { unsigned char si[9] = "ABCDEFGH"; unsigned char s2[9] = {0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2, 0x2} unsigned char sres[9]= " "; unsigned char* psres = sres; m64 msl, ms2, msres; msl = _im_setjDi8 (sl[7] ,sl [6] ,sl [5] ,sl [4], ms2 * _mm_set_pi8 (s2[7] ,s2 [6] ,s2[5] ,s2[4], s2[3]fs2[2],s2[l], s2[0]); msres = _mm_adds_pu8 (msl ,ms2); for (int cnt =* 0;cnt < 8;cnt++) { *psres = (char)_mm_cvtsi64_si32 (msres); psres++; msres = _mm_srli_si64 (msres , 8); }; _mm_empty(); printf("USING INTRINSICS IN MMX : PACKED ADDING OF BYTES \n\n"); printf(" Before operation: %s\n", si);
Глава 10. Встроенный ассемблер и оптимизация приложений... 311 printf(" After operation (+2): %s\n", sres); getchar(); return 0; Для использования собственных функций MMX-расширения необходимо включить в исходный текст файл заголовка mmintrin.h (эта строка выделена жирным шрифтом), содержащий определения собственных функций и пе- переменных. В нашей программе определены две строки, состоящие из 8 сим- символов (si, s2), и строка sres, содержащая результат сложения упакованных байтов. Для преобразований и хранения промежуточных результатов вычис- вычислений используются 64-разрядные переменные msi, ms2 и msres типа" m64. Обратите внимание, что тип переменной m64 содержит два символа под- подчеркивания! Для выполнения операций с 64-разрядными величинами скопируем строки si и s2 в msi и ms2 с помощью двух команд jmm_set_pi8. Далее выполня- выполняем побайтовое сложение переменных msi и ms2 инструкцией mmaddspus, а результат помещаем в переменную mares. Собственная функция _mm_adds_pu8 выполняет сложение 8-битовых беззнаковых величин, что нам и нужно. Далее следует быть очень внимательным. Перед выполнением операторов цикла for в переменной msres хранится 64-байтное значение. Но нам нуж- нужно получить и вывести на экран результирующую строку sres, состоящую из 8 байт. Следовательно, нужно каким-то образом выделить из 64- разрядной переменной 8 байт и записать их в строку sres. В C++ .NET нет функций, выполняющих подобное преобразование. Для решения этой зада- задачи воспользуемся двумя собственными функциями MMX-расширения: int _rran_cvtsi64_si32 ( m64 m) тб4 _mm_srli_si64 ( тб4 т, int count) Первая из этих функций преобразует младшие 32 бита в целое число, вторая выполняет сдвиг 64-разрядного числа вправо на количество бит, равное count. Для большего быстродействия Microsoft рекомендует в качестве счет- счетчика бит использовать константу. Чтобы выделить 1 байт и записать его на соответствующую позицию в строку sres, нам понадобится указатель на эту строку, определенный как unsigned char* psres = sres Запись одного байта из переменной msres в строку sres легко выполнить с помощью оператора *psres = (char)_mm_cvtsi64_si32 (msres);
312 Часть III. Встроенный ассемблер Visual C++ .NET 2003 и его использование Чтобы записать следующий байт в строку, необходимо выполнить инкре- инкремент указателя psres и сдвиг вправо на 8 бит переменной msres. Эта после- последовательность операций реализована в цикле for: for (int cnt = 0;cnt < 8;cnt++) *psres = (char)_mm_cvtsi64_si32 (msres); psres++; msres = _mm_srli_si64 (msres , 8); После выполнения операций ММХ необходимо восстановить состояние со- сопроцессора С ПОМОЩЬЮ ФУНКЦИИ jmm_empty () . Вид окна работающего приложения показан на рис. 10.4. Рис. 10.4. Окно приложения, демонстрирующего работу собственных функций C++ при сложении цепочки байт Посмотрим теперь на дизассемблированный фрагмент кода, где выполняет- выполняется сложение двух массивов байт (листинг 10.10). Жирным шрифтом выделе- выделены команды из исходного текста программы. m64 msl, ms2, msres; msl = _nm_set_pi8 (si[7],si[6],si[5],si[4], 00411ABF mov 00411AC2 mov 00411AC8 mov 00411ACB mov al,byte ptr [si] byte ptr [ebp-198h],al cl,byte ptr [ebp-OFh] byte ptr [ebp-197h],cl
Глава 10. Встроенный ассемблер и оптимизация приложений... 313 00411AD1 mov 00411AD4 mov 00411ADA mov 00411ADD mov 00411AE3 mov 00411AE6 mov 00411AEC ' mov 00411AEF mov 00411AF5 mov 00411AF8 mov 00411AFE mov [ebp-191h],cl 00411B07 movq 00411B0E movq 00411B15 movq 00411B1C movq dl,byte ptr [ebp-OEh] byte ptr [ebp-196h],dl al,byte ptr [ebp-ODh] byte ptr [ebp-195h],al с1,byte ptr [ebp-OCh] byte ptr [ebp-194h],cl dl,byte ptr [ebp-OBh] byte ptr [ebp-193h],dl al,byte ptr [ebp-OAh] byte ptr [ebp-192h],al cl,byte ptr [ebp-9] 00411B01 mov byte mmO,mmword ptr [ebp-198h] mmword ptr [ebp-148h] ,mmO mmO,mmword ptr [ebp-148h] mmword ptr [msl],imO ms2 - _nm_set_pi8 (s2[7] ,s2[6] ,s2[5] ,s2[4] , s2[3],s2[2],82[l], s2[0]); 00411B20 mov 00411B23 mov 00411B29 mov 00411B2C mov 00411B32 mov 00411B35 mov 00411B3B mov [ebp-195h],al 00411B44 mov 00411B47 mov 00411B4D mov 00411B50 mov 00411B56 mov 00411B59 mov 00411B5F mov 00411B62 mov 00411B68 movq 00411B6F movq 00411B76 movq 00411B7D movq al,byte ptr [s2] byte ptr [ebp-198h],al cl,byte ptr [ebp-23h] byte ptr [ebp-197h],cl dl,byte ptr [ebp-22h] byte ptr [ebp-196h],dl al,byte ptr [ebp-21h] 00411B3E cl,byte ptr [ebp-20h] byte ptr [ebp-194h],cl dl,byte ptr [ebp-lFh] byte ptr [ebp-193h],dl al,byte ptr [ebp-lEh] byte ptr [ebp-192h],al cl,byte ptr [ebp-IDh] byte ptr [ebp-191h],cl mmO,mmword ptr [ebp-198h] mmword ptr [ebp-158h],mmO mmO,mmword ptr [ebp-158h] mmword ptr [ms2],mmO mov byte ptr<P
314 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование msres » jnm_adds_pu8 (msl ,ms2) ; 00411B81 movq mmO,mmword ptr [ms2] 00411B85 movq mml,mmword ptr [msl] 00411B89 paddusb mml,mmO 00411B8C movq mmword ptr [ebp-168h],mml 00411B93 movq mmO,mmword ptr [ebp-168h] 00411B9A movq mmword ptr [msres],mmO for (int cnt ¦ 0;cnt < 8;cnt++) 00411B9E mov dword ptr [cnt],0 00411BA8 jmp main+189h D11BB9h) 00411BAA mov eax,dword ptr [cnt] 00411BB0 add eax,l 00411BB3 mov dword ptr [cnt],eax 00411BB9 cmp dword ptr [cnt],8 00411BC0 jge main+lC3h D11BF3h) *psres ¦ (char)_imn_cvtsi64_si32 (msres), 00411BC2 movq mmO,mmword ptr [msres] 00411BC6 movd eax,mmO 00411BC9 mov ecx,dword ptr [psres] 00411BCC mov byte ptr [ecx],al psres++; 00411BCE mov eax,dword ptr [psres] 00411BD1 add eax,1 00411BD4 mov dword ptr [psres],eax msres * _jnm_srli_si64 (msres , 8); 00411BD7 movq mmO,mmword ptr [msres] 00411BDB psrlq mmO,8 00411BDF movq mmword ptr [ebp-188h],mmO
Глава 10. Встроенный ассемблер и оптимизация приложений... 315 00411ВЕ6 movq mmO,mmword ptr [ebp-188h] 00411BED movq mmword ptr [msres],mmO 00411BF1 jmp main+17Ah D11BAAh) 00411BF3 einms Дизассемблированный фрагмент кода позволяет сделать некоторые важные выводы. Во-первых, во всех собственных функциях в той или иной степени задействованы ММХ-регистры. Во-вторых, при обмене данными .между ММХ-регистрами и 64-разрядными ячейками памяти используется команда movq, реже — movd. Поскольку при работе с собственными функциями ММХ-регистры непосредственно недоступны для программиста, ассемблер- ассемблерный вариант операций MMX-расширения может дать выигрыш в произво- производительности. Рассмотрим ассемблерный вариант нашей задачи. Заменим собственные функции расширения ММХ на ассемблерные команды и немного изменим строки si и s2. Исходный текст программы выглядит намного проще (листинг 10.11). // ADD_8_BYTES_MMX_ASM.cpp : Defines the entry point for the console // application. #include "stdafx.h" int _tmain(int argc, _TCHAR* argv[]) { unsigned char si[9] - 2345678"; unsigned char s2[9] * {0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1}; unsigned char sres[9]= " "; _asm { movq mmO, QWORD PTR si movq mml, QWORD PTR s2 paddd mmO, mml movq QWORD PTR sres, mmO emms
316 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование printf("USING ASSEMBLER IN MMX: PACKED ADDING OF BYTES \n\n"); printf(" Before operation: %s\n", si); printf(" After operation (+2): %s\n", sres); getchar(); return 0; Обратите внимание на то, что все вычисления выполнены в ассемблерном блоке. Листинг 10.12 показывает дизассемблированный код примера. Как видно, программный код практически оптимален в случае использования ассемблерных команд. asm { movq ramO, qword ptr si 00411AA7 movq irmO,nimword ptr [si] movq nanl, qword ptr s2 00411AAB movq mml,mmword ptr [s2] paddd romO, mml 00411AAF paddd mmO,mml movq qword ptr sres, mmO 00411AB2 movq mmword ptr tsres],mmO 00411AB6 emms Окно приложения показано на рис. 10.5. \Vs Рис. 10.5. Окно приложения, показывающего сложение упакованных байт с помощью ассемблерных команд MMX-расширения
Глава 10. Встроенный ассемблер и оптимизация приложений... 317 Разработаем приложение, в котором будет осуществляться попарное сложе- сложение элементов двух целочисленных массивов, и результат будет помещен в третий массив. Все три массива имеют одинаковый размер. Рассмотрим три варианта программы, выполняющих такую операцию сложения, каждая из которых представляет собой консольное приложение. Первая программа складывает элементы массивов с использованием обычных операторов сло- сложения C++. Во второй выполним сложение с помощью собственных функ- функций MMX-расширения. В третьем варианте применим ассемблерные коман- команды ММХ. Обычный вариант представлен в листинге 10.13. // ММХ_2_1,срр : Defines the entry point for the console application. #include "stdafx.h" int _tmain(int argc, _TCHAR* argv[]) { int al[] = {23, 12, -45, -9, 44, -16, -7, 19, 1, 14, -2}; int bl[] = {12, -70, 12, 33, 12, 35, 29, -33, -99, -5, -7}; int ires[16] ; int isize = sizeof(al)/4; for (int cnt = 0;cnt < isize;cnt++) ires[cnt] = al[cnt]+bl[cnt]; printfCal: "); for (int cnt = 0; cnt < isize; cnt++) printf("%d ", al[cnt]); printf("\nbl: "); for (int cnt = 0; cnt < isize; cnt++) printf("%d ", bl[cnt]); printf("\n\n"); for (int cnt = 0; cnt < isize; cnt++) 'printf("ires[%d] = %d\n", cnt,ires[cnt]); getchar(); return 0;
318 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование Можно усовершенствовать нашу программу, если воспользоваться собст- собственными функциями C++ .NET для MMX-расширения. Исходный текст модифицированной программы показан в листинге 10.14. i ' ' ' ¦ t i Л >л с т и н г 10.14. С л о ж о н v- е j п е м е н v о ь д > j у у ш:- л о ч ?; с п о н i < ¦¦-, t х м а с с и г i; ¦. к ! с испольчоза»ие« собственных (функций // MMX_ADD_ARRAYS_INTRINSICS.ерр : Defines the entry point for the // console application. ¦include "stdafx.h" ¦include <mmintrin.h> int _tmain(int argc, _TCHAR* argv[]) { int al[] = {23, 12, -45, -9, 44, -16, -7, 19, 1, 14, -2}; int* pal = al; int bl[] = {12, -70, 12, 33, 12, 35, 29, -33, -99, -5, -7}; int* pbl = Ы; int ires[16] ; int* pires = ires; m64 mal, mbl, mires; int isize = sizeof(al)/4; for (int cnt = 0;cnt < isize;cnt++) { mal = _mm_cvtsi32_si64 (*pal); mbl = _mm_cvtsi32_si64 (*pbl); mires = _mm_add_pi32 (mal, mbl); *pires = _mm_cvtsi64_si32 (mires); pires++; }; printf("al: "); for (int cnt = 0; cnt < isize; cnt++) printf("%d ", al[cnt]);
Глава 10. Встроенный ассемблер и оптимизация приложений... 319 printf (и\пЫ: "); for (int cnt = 0; cnt < isize; cnt++) printf("%d ", bl[cnt]); printf("\n\n") ; for (int cnt = 0; cnt < isize; cnt++) printf("ires[%d] = %d\n", cnt,ires[cnt]); getchar() ; return 0; Второй вариант программы быстрее, чем первый, несмотря на большее чис- число операторов. Однако в этом варианте возможности 64-разрядной обработ- обработки данных полностью не используются, поскольку мы обрабатываем одно двойное слово в каждой итерации. Исправить ситуацию поможет ассемблер. В третьем варианте программы используются ассемблерные команды MMX- расширения, что позволяет еще больше ускорить обработку данных. Это показано в листинге 10.15. ¦ Л >\ с т ;¦; н г 1'5.1 о. i; р о s'iJLi\i v; '¦ i с; : и ж и н; • >; :.; j i у у;.; н : о н п и у :< ц *:¦ л с ч и с s) с- н и ?, i x f л а с с И с о f; ¦ С П О f>5 О :.Ц;., > О G 4 5 р' 7 О С!. i >'. ,Ч С С О М О Л <? р н i ¦ ? ЧI* М Л ¦¦ < Д ¦ // ADD_2_ARRAYS_MMX_ASM.cpp : Defines the entry point for the console // application. tinclude "stdafx.h" int _tmain(int argc, JTCHAR* argv[]) { int al[] - {23, 12, -45, -9, 44, -16, -7, 19, 1, 14, -2}; int bl[] = {12, -70, 12, 33,2, 35, 29, -33, -99, -5, -7} int ires[16]; bool flag = false; if((sizeof(al)%8) != 0)flag = true; int isize = sizeof(al)/4; int qsize = sizeof(al)/8; _asm { lea ESI, DWORD PTR al lea EDI, DWORD PTR bl lea EDX, DWORD PTR ires
320 Часть III. Встроенный ассемблер Visual C++ .NET 2003 и его использование sub ESI, 8 sub EDI, 8 sub EDX, 8 mov ECX, DWORD PTR qsize next: add ESI, 8 add EDI, 8 add EDX, 8 movq mmO, QWORD PTR [ESI] movq mml, QWORD PTR [EDI] paddd mmO, raml movq DWORD PTR [EDX], mmO loop next ernms cmp jne mov add mov ex: flag, 1 ex EAX, DWORD PTR [ESI+8] EAX, DWORD PTR [EDI+8] DWORD PTR [EDX+8], EAX printf("al: "); for (int cnt = 0; cnt < isize; cnt++) printf("%d ", alfcnt]); printf("\nbl: "); for (int cnt = 0; cnt < isize; cnt++) printf("%d ", blfcnt])/ printf("\n\n"); for (int cnt = 0; cnt < isize; cnt++) printf("ires[%d] = %d\n", cnt,ires[cnt]); getchar(); return 0; Исходный текст программы нуждается в подробных пояснениях. В про- программе определена переменная qsize, равная размерности массива ai в бай- байтах, деленная на 8. Иными словами, Переменная qsize определяет количе- количество учетверенных слов (по 64 бита), находящихся в массиве. Переменная
Глава 10. Встроенный ассемблер и оптимизация приложений... 321 flag определяет, осталась ли среди 64-разрядных чисел 32-разрядная пере- переменная. Для чего это нужно? Поясню на примере. Пусть имеется массив из 11 целых чисел. Размер целрчисленной переменной составляет 4 байта. Для операций с MMX-расширением желательно, чтобы переменная имела раз- размер 64 байта, в этом случае производительность таких операций будет очень высокой. Достичь этого можно, если обрабатывать два целых числа C2x2 бита) как одно F4xi). Из одиннадцати целых можно выделить пять 64- разрядных чисел плюс одно 32-разрядное, которое надо обрабатывать от- отдельно. Переменная flag как раз и показывает, присутствует ли в массиве 32- разрядная переменная. Теперь я расскажу более детально о блоке ассемб- ассемблерных команд. Команды lea ESI, DWORD PTR al lea EDI, DWORD PTR Ы lea EDX, DWORD PTR ires sub ESI, 8 sub EDI, 8 sub EDX, 8 mov ECX, DWORD PTR qsize загружают в регистры esi, edi и edx адреса массивов al, ы и ires соответ- соответственно. Вычитание 8 из значений, помещенных в регистры, сделано для удобства работы в цикле loop. В регистр есх помещается количество 64- битовых операндов, которые необходимо обработать. Сложение этих опе- операндов выполняется с помощью команд movq mmO, QWORD PTR [ESI] movq raml, QWORD PTR [EDI] paddd mmO, mml Результат помещается по адресу, находящемуся в регистре edx: movq DWORD PTR [EDX], iranO В начале каждой итерации в регистры esi, edi и edx помещаются адреса следующих 64-разрядных переменных путем прибавления к существующему адресу числа 8: add ESI, 8 add EDI, 8 add EDX, 8 Далее операция суммирования повторяется. При выходе из цикла loop про- проверяется значение переменной flag. Если fiag=o, 32-разрядного "остатка"
322 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование нет и можно выходить из ассемблерного блока. Если fiag=i, необходимо обработать 32-разрядное число. Это выполняется с помощью команд mov EAX, DWORD PTR [ESI+8] add EAX, DWORD PTR [EDI+8] mov DWORD PTR [EDX+8], EAX Остальная часть программного кода, думаю, понятна и в пояснениях не ну- нуждается. Окно работающего приложения показано на рис. 10.6. Рис. 10.6. Окно приложения, демонстрирующего сложение элементов двух целочисленных массивов с помощью ассемблера MMX-расширения Следующая группа команд MMX-расширения, на которой мы остановим- остановимся, — команды сравнения. В свою очередь они делятся на две группы — команды обычного сравнения ("равно — не равно") и команды сравнения по величине ("больше — меньше"). Операции сравнения проводятся для упако- упакованных байтов, слов и двойных слов. К Командам обычНОГО Сравнения ОТНОСЯТСЯ pcmeqb, pcmeqw, pcmeqd, а К командам сравнения по величине — pcmpgtb, pcmpgtw, pcmpgtd. Последние буквы в имени команды обозначают соответственно байт (ь), слово (w) и двойное слово (d). Команды имеют синтаксис: рстрххх <операнд_1>, <операнд_2> В качестве операнда 1 может выступать один из ММХ-регистров (mmO, mmi и т. д.), в качестве второго операнда — либо ММХ-регистр, либо 64-разрядная ячейка памяти. Результатом операции сравнения являются нулевые или единичные байты, слова или двойные слова. Для операций обычного сравнения нулевые значения формируются, если соответствующие байты, слова или двойные слова не равны. Единичные
Глава 10. Встроенный ассемблер и оптимизация приложений... 323 значения формируются в случае, если соответствующие байты, слова или двойные слова равны. Для операций сравнения по величине единичные значения формируются, если соответствующие байты, слова или двойные слова первого операнда больше соответствующих байт, слов или двойных слов второго операнда. Рассмотрим следующий пример. Требуется проанализировать две символь- символьные строки на равенство. Для упрощения будем использовать строки разме- размером в 8 байт. Разработаем один вариант примера с использованием собст- собственных функций C++ .NET, а другой — с использованием встроенного ассемблера. Исходный текст консольного приложения с использованием собственных функций представлен в листинге 10.16. // CMP_8_BYTES_INTRINSICS.cpp : Defines the entry point for the console // application. #include "stdafx.h" #include <nmintrin.h> int _tmain(int argc, _TCHAR* argv[]) { char si[32]; char s2[32]; char ires[32]; m64 msl, ms2, msres; printf("Comparison 2 strings with MMX Intrinsics\n\n"); while(true) { memset(sl, '\0', 16); memset(s2, '\0', 16); memset(ires, 'NO1, 16); char* pires = ires; printf("NnEnter string si: "); scanf("%s", si);
324 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование printf("Enter string s2: "); scanf("%s", s2); msl = _mm_set_pi8 (sl[7], sl[6], sl[5], sl[4], ms2 = _ram_set_pi8 (s2[7], s2[6], s2[5], s2[4], s2[3], s2[2], s2[l], s2[0]); msres = _mm_cmpeq_pi8 (msl, ms2); *(int*)pires = _rnm_cvtsi64_si32 (msres); msres =_mm_srli_si64 (msres, 32).; pires = pires+4; *(int*)pires = _mm__cvtsi64_si32 (msres); _mm_empty(); for (int cnt = 0;cnt < 8;cnt++) { if (ires[cnt] == 0) { printf("sl not equal s2 !\n"); break; if (cnt == 8)printf("sl = s2 !\n"); }; return 0; Операция сравнения массивов (не только символьных) базируется на срав- сравнении отдельных элементов, продвижения счетчика адресов к следующим элементам, сравнения следующих элементов, и так до тех пор, пока не будет достигнут конец массива. Технология ММХ позволяет выполнять одновре- одновременно сравнение нескольких байт, слов или двойных слов. Операция срав- сравнения 8-ми символов выполняется следующими командами msl = _mm_set_pi8 (si[7],si[6],si[5],si[4],sl[3],si[2],si[1],si [0]); ms2 = _mm_set_pi8 (s2[7],s2[6],s2[5],s2[4],s2[3],s2[2],s2[1],s2[0]); msres = _mm_cmpeq_pi8 (msl, ms2); Первые два оператора помещают в 64-разрядные переменные msl и ms2 по 8 байт из строк si и s2. Собственно сравнение выполняется последним опе- оператором, при этом результат помещается в переменную msres.
Глава 10. Встроенный ассемблер и оптимизация приложений... 325 Далее 8 байт из этой переменной помешаются в строку ires: * (int*)pires = _nim_cvtsi64_si32 (msres); msres =_mm_srli_si64 {msres, 32) ; pires = pires+4; *(int*)pires = _ram_cvtsi64_si32 (msres); Вид окна работающего приложения показан на рис. 10.7. Рис. 10.7. Окно приложения, демонстрирующего сравнение двух строк - с помощью собственных функций MMX-расширения Тот же вариант программы, но с использованием ассемблерных инструкций MMX-расширения выглядит намного проще. Исходный текст программы приведен в листинге 10.17. // CMP_8_BYTES_ASM.cpp : Defines the entry point for the console // application. #include "stdafx.h" int _tmain(int argc, _TCHAR* argv[]) { char si [32]; char s2[32]; char ires[32];
326 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование printf("Comparison 2 strings with MMX assembler instructions\n\n"); while(true) { memset(si, '\0', 16); memset(s2, 'NO1, 16); memset(ires, '\0*, 16); printf("\nEnter string si: "); scanf("%s", si); printf("Enter string s2: "); scanf("%s", s2); _asm { movq mmO, QWORD PTR si movq mml, QWORD PTR s2 pcmpeqb mmO, mml movq QWORD PTR ires, mmO emros for (int cnt = 0;cnt < 8;cnt++) { if (ires[cnt] == 0) { printf("si not equal s2 !\n"); , break; } }; if (cnt == 8)printf("si = s2 !\n }; return 0; Операция сравнения выполняется в блоке asm. Содержимое двух строк ко- копируется в ММХ-регистры, после чего и выполняется побайтовое сравнение содержимого регистров с помощью команды pcmpeqb mmO, mml. Результат сохраняется в строке ires командой movq qword ptr ires, mmO. Анализ по- полученного результата выполняется в цикле for.
Глава 10. Встроенный ассемблер и оптимизация приложений... 327 Вид окна работающего приложения показан на рис. 10.8. Рис. 10.8. Окно приложения, демонстрирующего сравнение двух строк с помощью ассемблерных команд MMX-расширения Рассмотрим пример, в котором выполняется-сравнение 8-ми байт на одних и тех же позициях в двух строках si и s2. Сравнение выполняется по прин- принципу "больше — меньше" с использованием команды ассемблера pcmpgtb. Если байт строки si больше байта строки s2, в строку ires записываются единицы в соответствующий байт. Если байт в строке si меньше байта s2, в соответствующий байт ires записываются 0. Результат сравнения выводится на экран. Исходный текст программы приведен в листинге 10.18. // CMPGT_8_ASM_MMX.срр : Defines the entry point for the console // application. #include "stdafx.h" int _tmain(int argc, _TCHAR* argv[] char si[32]; char s2[32]; char ires[32];
328 Часть III. Встроенный ассемблер Visual C++ .NET 2003 и его использование printf("Using PCMPGTB in string comparison with assembler instructions\n\n"); while(true) { memset(sl, '\0f, 16); memset(s2, 'NO1, 16); memset(ires, '\0', 16); printf("\nEnter string si: "); scanf("%s", si); printf("Enter string s2: "); scanf("%s", s2); _asm { movq inmO, QWORD PTR si movq mml, QWORD PTR s2 pcmpgtb mrnO, inml movq QWORD PTR ires, mmO emms }; for (int cnt = 0;cnt < 8;cnt++) { if (ires[cnt] != 0x0) printf("si[%d] > s2[%d]\n", cnt, cnt); if (ires[cnt] == 0x0) printf("si[%d] <= s2[%d]\n", cnt, cnt); return^); Окно приложения показано на рис. 10.9. Ассемблерные команды сравнения позволяют реализовать быстрые алгорит- алгоритмы поиска элементов массивов. Рассмотрим следующий пример. Пусть тре- требуется найти символ в строке. Операцию поиска можно выполнить с помо- помощью обычных команд ассемблера, но применение инструкций MMX- расширения позволяет сравнивать несколько байт одновременно, поэтому выигрыш в производительности получается значительный. За основу нашей программы возьмем пример, показанный в листинге 10.17, но сделаем неко- некоторые изменения в исходном тексте. Исходный текст примера показан в листинге 10.19.
Глава 10. Встроенный ассемблер и оптимизация приложений... 329 ;,L-J Рис. 10.9. Окно приложения, выводящего на экран результат побайтового сравнения строк с помощью команд ассемблера MMX-расширения // FIND_CHAR_PCMPEQB_ASM.срр : Defines the entry point for the console // application. #include "stdafx.h" int _tmain(int argc, _TCHAR* argv[]) char si [32]; char s2[32]; char cl = 'r'; char ires[32]; printf("Find char with PCMPEQB instruction\n\n' while(true) memset(sl, '\0', 16); roemset(s2, cl, 16); memset(ires, '\0', 16); printf("\nEnter string si: ") ; scanfC^s", si); _asm { movq mmO, QWORD PTR si movq raml, QWORD PTR s2
330 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование pcmpeqb пттО, rnml movq QWORD PTR ires, ramO emns for (int cnt = 0;cnt < 8;cnt++) { if (ires[cnt] != 0) { print?("Character %c found in si !\n", cl); break; if (cnt =» 8)printf("Character %c not found in si !\n", cl) return 0; Изменения, сделанные по отношению к примеру, приведенному в листин- листинге 10.17, выделены жирным шрифтом. Предположим, что в строке si ищет- ищется СИМВОЛ 'г1. ДЛЯ ТОГО ЧТОбы МОЖНО было ИСПОЛЬЗОВаТЬ КОМаНДУ pcmeqb, заполним строку s2 этим символом: memset(s2, cl, 16); Если литера г присутствует в si, то хотя бы один из первых 8-ми байт строки ires, содержащей результат, будет отличен от нуля. Окно приложения, выполняющего поиск элемента в строке, показано на рис. 10.10. • l i* ¦: ¦¦!= ».'A*.f_ : Li';tfv'.-,;*<tti;fc.>] Рис. 10.10. Окно приложения, выполняющего поиск элемента в символьной строке с помощью ассемблера MMX-расширения
Глава 10. Встроенный ассемблер и оптимизация приложений... 331 Следующий пример использования встроенного ассемблера в ММХ-при- ложениях более сложный. В этом примере я покажу применение команд сложения, упаковки и извлечения операндов для одновременного сложения двух целых чисел, находящихся в двух массивах. Для упрощения алгоритма операции проводятся только с положительными целыми числами. Опреде- Определим массивы, содержащие исходные числа, как ai и ы, а массив, содержа- содержащий сумму элементов, — el. Исходный текст консольного приложения по- показан в листинге 10.20. // PACK_n_ADD_2_in_4WORD.срр : Defines the entry point for the console // application. #include "stdafx.h" int _tmain(int argc, JTCHAR* argv[]) { int al[4] = {12, 1, 34, 17}; int bl[4] = {17, 7, 4, 33}; int cl[4] ; printf(" al: "); for (int cnt = 0; cnt < 4;cnt++) printf("%d\t", al[cnt]).; printf ("\n Ы: "); for (int cnt = 0; cnt < 4;cnt++) printf("%d\t", bl[cnt]); _asm { movq iranO, QWORD PTR al movq iml, QWORD PTR al+8 packssdw mmO, mml movq mml, QWORD PTR bl movq mm2, QWORD PTR bl+8 packssdw mml, mm2 paddw mmO, mml lea ESI, cl
332 Часть HI. Встроенный ассемблер Visual C++ .NET2003 и его использование pextrw EDI,inmO, О mov DWORD PTR [ESI], EDI add ESI, 4 pextrw EDI,ramO, 1 mov DWORD PTR [ESI], EDI add ESI, 4 pextrw EDI,rrtnO, 2 mov DWORD PTR [ESI], EDI add ESI, 4 pextrw EDI,mmO, 3 mov DWORD PTR [ESI], EDI add ESI, 4 emms }; . . printf("\n\n cl: \n"); for (int cnt = 0; cnt < 4;cnt++) printf(" al[%d] + bl[%d] = %d\n", cnt, cnt, cl[cnt]); getchar(); return 0; Проанализируем исходный текст примера. Вначале остановимся на командах movq mmO, QWORD PTR al movq mml, QWORD PTR al+8 packssdw mmO, mml ассемблерного блока. Первые две команды выполняют пересылку двух це- целых чисел из массива al в 64-разрядные регистры mmO и mml. После выпол- выполнения этих команд в каждом из регистров будет содержаться два 32-раз- 32-разрядных целых числа C2x2). Нашей задачей является одновременное сложе- сложение целых чисел массива al с числами массива ы, поэтому выполним упа- упаковку чисел из регистров mmO и mml и поместим результат в регистр mmO. Суть операции упаковки состоит в уменьшении размерности элементов в два раза. В нашем случае можно упаковать два 32-разрядных числа из mmO и mmi в четыре 16-разрядных числа и поместить результат в регистр mmO. Упа- Упаковка чисел позволяет одновременно выполнить арифметические и логиче-
Глава 10. Встроенный ассемблер и оптимизация приложений... 333 ские операции над вдвое большим числом элементов, а это ускоряет эффек- эффективность программы в целом. Следует помнить, что при операциях над упа- упакованными числами можно выйти за пределы разрядной сетки, поэтому команды упаковки необходимо применять осторожно. Вернемся к нашему примеру. Упаковка содержимого ММХ-регистров mmO и mml выполняется командой packssdw iranO, mml. Следующие три команды выполняют аналогичные операции над первыми двумя элементами массива ы: movq mml, QWORD PTR Ы movq mm2, QWORD PTR Ы+8 packssdw mml, mm2 Обратите внимание на то, что для операций над элементами массива ы ис- используются ММХ-регистры mml и mm2. Регистр mmo задействовать нельзя, по- поскольку в нем содержится 4 упакованных 16-разрядных числа из массива ai. После упаковки элементов массивов можно выполнить операцию сложения Четырех СЛОВ, НаХОДЯЩИХСЯ В mmO И mml, При ПОМОЩИ КОМаНДЫ paddw mmO, mml. Результат помещается в ММХ-регистр mmO. Теперь необходимо каким-то образом извлечь четыре суммы из регистра mmO и поместить их на место первых четырех элементов массива ci. Чтобы это сделать, воспользуемся одной из дополнительных команд MMX-расши- MMX-расширения, появившейся в процессорах Pentium III, — pextrw. Эта команда вы- выполняет извлечение одного из четырех упакованных слов операнда-ис- операнда-источника в 32-разрядный регистр общего назначения. Операнд, который извлекается, помещается в младшее слово регистра, а старшее слово регист- регистра обнуляется. Позиция извлекаемого операнда определяется маской, со- содержащей значение от 0 до 3. Небольшим неудобством при использовании этой ассемблерной команды является то, что маска должна быть констан- константой, что не позволяет использовать команду в циклических операциях. Эта команда не имеет аналога среди собственных функций Visual C++ .NET (во всяком случае, не имела на момент написания книги). В качестве приемника воспользуемся регистром edi, в который будет поме- помещаться 16-разрядный результат сложения. Для записи в массив ci использу- используется регистр esi, содержащий адрес массива. Команда lea esi, ci загру- загружает в регистр esi адрес первого элемента массива. Группы команд для извлечения 16-разрядных чисел в массив подобны, поэтому я приведу фраг- фрагмент кода для операций с нулевым элементом регистра mmo: pextrw EDI,mmO, 0 mov DWORD PTR [ESI], EDI add ESI, 4
334 Часть III. Встроенный ассемблер Visual C++ .NET 2003 и его использование Первые две команды этого фрагмента выполняют запись числа на соответ- соответствующую позицию в.массиве el, а команда add esi, 4 продвигает указа- указатель на позицию следующего элемента массива. В остальном исходный текст программы понятен и в комментариях не нуж- нуждается. Окно работающего приложения показано на рис. 10.11. Рис. 10.11. Окно приложения, демонстрирующего операции с упакованными данными с помощью ассемблера MMX-расширения В предыдущем примере я использовал команду pextrw из дополнительных команд MMX-расширения для процессоров Pentium III и выше. Еще четыре дополнительные команды позволяют увеличить вычислительную мощь при- приложений на C++ .NET. Это команды, позволяющие извлечь максимальные или минимальные значения из каждой пары упакованных элементов. Эле- Элементами могут быть беззнаковые байты или знаковые слова. Эти команды весьма эффективны для разработки алгоритмов сортировки, поиска, обра- обработки битовых полей в мультимедийных приложениях. Я приведу пример Программ, ИСПОЛЬЗУЮЩИХ КОМаНДЫ pmaxsw И pminsw. Воспользуемся исходным текстом предыдущего примера, в котором прове- проведем некоторые изменения. Это приложение выводит на экран большее из двух чисел, находящихся на соответствующих позициях в массивах ai и ы. Команда pmaxsw выделена жирным шрифтом. Исходный текст программы приведен в листинге 10.21. // MAX_FROM_PAIR_ARRAYS.срр // application. Defines the entry point for the console #include "stdafx.h"
Глава 10. Встроенный ассемблер и оптимизация приложений... 335 int _tmain(int argc, JTCHAR* argv[]) int al[4] - {9, 1, 34, 1}; int bl[4] = {7, 5, 79, 3}; int cl[4]; printf(" al: "); for (int cnt = 0; cnt < 4;cnt++) printf("%d\t", al[cnt]); printf("\n bl: "); for (int cnt = 0; cnt < 4;cnt++) printf("%d\t", bl[cnt]); _asm { movq mmO, QWORD PTR al movq mml, QWORD PTR a1+8 packssdw mmO, mml movq mml, QWORD PTR bl movq mm2, QWORD PTR bl+8 packssdw mml, mm2 pmaxsw omO, nral lea ESI, cl pextrw EDI,mmO, 0 mov DWORD PTR [ESI], EDI add ESI, 4 pextrw EDI,mmO, 1 mov DWORD PTR [ESI], EDI add ESI, 4 pextrw EDI,mmO, 2 mov DWORD PTR [ESI], EDI add ESI, 4 pextrw EDI,mmO, 3 mov DWORD PTR [ESI], EDI add ESI, 4
336 Часть III. Встроенный ассемблер Visual C++ .NET 2003 и его использование printf("\n\n cl: \п"); for (int cnt = 0; cnt < 4;cnt++) printf(" max of al[%d] and bl[%d] = %d\n", cnt, cnt, cl[cnt]) getchar(); return 0; Команда pmaxsw использует в качестве операндов ММХ-регистры mmo и mmi, а результат сохраняет в регистре mmo. Оставшаяся часть исходного текста была проанализирована в предыдущем примере. Для определения мини- минимального из двух элементов массивов следует просто заменить команду pmaxsw mmO, mmi на pminsw mmO, mmi. Читатель МОЖвТ сделать ЭТО В качвСТ- ве упражнения. Окно работающего приложения показано на рис. 10.12. Рис. 10.12. Окно приложения, демонстрирующего поиск максимальных элементов с помощью встроенного ассемблера MMX-расширения Команды логических операций MMX-расширения — еще одна группа команд, широко используемых в приложениях. Эти команды реализуют логические операции "И" (pand), "И-НЕ" (pandn), "ИЛИ" (рог), "исключаю- "исключающее ИЛИ" (рхог). Особенность этих команд в том, что они выполняют ло- логические операции над 64-разрядными величинами. Описание этих команд приводится в главе 1, поэтому я сразу приведу пример, в котором использу- используется команда рхог. С помощью этой команды можно реализовать алгоритм сравнения двух символьных строк. Мы рассматривали подобный алгоритм, реализованный с помощью команды сравнения pcmpeqb, сейчас же выпол- выполним eFO С ПОМОЩЬЮ рхог. Исходный текст консольного приложения приведен в листинге 10.22.
Глава 10. Встроенный ассемблер и оптимизация приложений... 337 | Листинг 10.22.. Сраанони-з стрск с помощыо команды oxer МЭДХ-расширй /I COMPARE_STR_WITH_PXOR.ерр : Defines the entry point for the console // application. #include "stdafx.h" int _tmain(int argc, _TCHAR* argv[]) { char si[32]; char s2[32]; char ires[32]; printf("Comparison of strings with PXOR instruction\n\n"); while(true) { memset(sl, '\0', 16); memset(s2, '\0', 16); • memset(ires, '\0', 16); printf("\nEnter string si: "); scanf("%s", si) ; printf("\nEnter string s2: "); scanf("%s", s2); _a.sm { movq rnmO, QWORD PTR si movq inml, QWORD PTR s2 pxor mmO, rnml movq QWORD PTR ires, mmO emms }; for (int cnt = 0;cnt < 8;cnt++) { if (ires[cnt] != 0) { printf("\nString si not equal s2!", cl); break;
338 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование if (cnt = 8)printf("\nString si = s2!'\ cl); return 0; Особенностью команды рхог является то, что в случае равенства операндов результат становится равным 0. Именно это свойство и использовано при сравнении двух строк. Окно приложения, выполняющего сравнение строк таким способом, показано на рис. 10.13. Рис. 10.13. Окно приложения, выполняющего сравнение символьных строк с помощью команды рхог Следующая группа команд MMX-расширения, применение которых я хочу продемонстрировать, — команды умножения. Алгоритм работы этих команд отличается от обычных команд целочисленного умножения. Размер резуль- результата, формируемого обычными командами, превышает в два раза размер- размерность исходных операндов. Команды MMX-расширения используют другой алгоритм умножения. Операция умножения выполняется одновременно для четырех 16-разряд- 16-разрядных операндов со знаком. Умножение целочисленных операндов можно ВЫПОЛНИТЬ ЛИбо Командами pmulhw И pmullw, Либо командой pmaddwd. Я ПО- кажу пример умножения двух целых чисел с помощью команды pmaddwd MMX-расширения. Вариант профаммы умножения с использованием команд pmuihw/pmuiiw читатели могут попробовать разработать самостоятельно. Вначале разработаем консольное приложение, выполняющее операцию ум- умножения двух целых чисел и вывод результата на экран с помощью собст- собственных функций C++ .NET. Исходный текст приложения показан в лис- листинге 10.23.
Глава 10. Встроенный ассемблер и оптимизация приложений... 339 Листинг 10.23 V «ношение дзух ц<?я;.;х чисел с **.\;яаиъ.:овдгМ'-<\*, с о б" ио Ф у и к ц и и ы М X ¦ р .з с i;..; поения // MUL_2_INTS_INTR.cpp : Defines the entry point for the console // application. #include "stdafx.h" #include <mmintrin.h> int _tmain(int argc, _TCHAR* argv[]) { int il, i2; int ires; __m64 mil, mi2, mires; printf("MULTIPLICATION OF 2 INTS WITH INTRINSICS (MMX_EXT.)\n\n"); while (true) { printf("\nEnter il: "); scanf("%d", &il); printf("Enter i2: ") ; scanf("%d", &i2); mil = _mm_cvtsi32_si64 (il); mi2 = _mm_cvtsi32_si64 (i2); mil = _mm_packs_pi32 (mil/inil); mi2 = _ram_packs_pi32 (mi2, mi2); mires = _mm_madd_pil6 (mil ,mi2); ires = _mm_cvtsi64_si32 (mires); printf("\nil * i2 = %d\n", ires); } . return 0; В ЭТОМ примере функции _mm_cvtsi32_si64 (il) И _mm_cvtsi32_si64 (i2) выполняют преобразование 32-разрядных целых чисел и и i2 к формату 64-разрядных упакованных переменных mil и mi2. В младшие 32 разряда
340 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование этих переменных помещаются il и i2, а старшие 32 разряда заполняются нулями. Операторы mil = _mm_packs_pi32 (mil,mil); mi2 = _mm_packs_pi32 (mi2, mi2) ; выполняют упаковку четырех двойных слов в 4 слова. Приемником и ис- источником в этих функциях является одна и та же переменная (mil для пер- первой команды и mi2 — для второй). Эти функции нужны для того, чтобы можно было выполнить умножение при помощи оператора mires = _mm_madd_pil6 (mil ,mi2) Наконец, оператор ires = _mm_cvtsi64_si32 (mires) помещает результат умножения из переменной mires в ires. Окно работающего приложения показано на рис. 10.14. - Рис. 10.14. Окно приложения, демонстрирующего умножение целых чисел с помощью собственных функций C++ .NET Этот же вариант программы с использованием встроенного ассемблера вы- выглядит куда более изящно и требует меньшего числа команд. Исходный текст программы представлен в листинге 10.24. // INT MUL_ASM.cpp : Defines the entry point for the console // application.
Глава 10. Встроенный ассемблер и оптимизация приложений... 341 #include "stdafx.h" int _tmain(int argc, _TCHAR* argv[]) { int il, i2; int ires; printf("MULTIPLICATION OF 2 INTS WITH ASM (MMX-EXT.)\n\n"); while (true) { printf("\nEnter il: "); scanf("%d", &il); printf("Enter i2: "); scanf("%d", &i2); _asm { pxor mmO, ramO movd mmO, DWORD PTR il packssdw mmO, nmO pxor inral, raml movd mml, DWORD PTR i2 packssdw mml, rnml pmaddwd inmO, mml movd DWORD PTR ires, mmO ernius }; printf("\nil * i2 = %d\n", ires); ) return 0; Если вспомнить, что ассемблерным аналогом функции _mm_cvtsi32_si64 является movd, функции _mm_packs_pi32 — packssdw и, наконец, функции _mm_madd_pii6 — команда pmaddwd, то фрагмент программного кода в ас- ассемблерном блоке становится понятным. Окно работающего приложения показано на рис. 10.15.
342 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование — i> ч ;' : i >'¦ i Рис. 10.15. Окно приложения, выполняющего умножение целых чисел с помощью ассемблерных команд MMX-расширения Следующим примером, который мы рассмотрим, будет программа, вычис- вычисляющая абсолютную величину целого числа с использованием нескольких операций ММХ. Здесь используются уже знакомые нам команды movd, pxor, pmaxsw. Результат вычисления запоминается в регистре edi с помо- помощью команды pextrw: pextrw EDI, iranl,0 Последний операнд команды, равный нулю, выбирает младшее слово из регистра mmi и записывает его в младшее слово регистра edi, при этом старшее слово edi обнуляется. Исходный текст приложения показан в листинге 10.25. // ABS_INT.cpp : Defines the entry point for the console application. #include "stdafx.h" int _traain(int argc, _TCHAR* argv[]) { int il, rnodil; printf(" Calculating the modulus of integer\n\n") while (true)
Глава 10. Встроенный ассемблер и оптимизация приложений. 343 printf("XnEnter il: "); scanf("%d", &il); _asm { movd mmO, DWORD PTR il pxor mml, mml psubw mml, mmO pmaxsw mml, mmO pextrw EDI, mml,0 mov DWORD PTR modil, EDI emms printf("Modulus of il = %d\n", modil) return 0; Думаю, читатель сможет самостоятельно проанализировать алгоритм опре- определения абсолютного значения. Окно приложения, выполняющего вычис- вычисление модуля числа, показано на рис. 10.16. Рис. 10.16. Окно приложения, определяющего модуль целого числа с помощью команд ассемблера Мы проанализировали далеко не все возможности оптимизации приложе- приложений при помощи ассемблерных функций MMX-расширения, но я надеюсь, что приведенные примеры помогут программистам в написании намного более сложных и эффективных программ.
344 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование 10.3. SSE-расширение и его программирование на встроенном ассемблере В этом разделе рассматриваются вопросы применения встроенного ассемб- ассемблера для оптимизации приложений, использующих операции с плавающей точкой SSE-расширения процессоров Pentium. SSE-расширение включает дополнительные 8 регистров разрядностью в 128 бит, имеющих обозначение xmmo ... xmm7. Данные в SSE-формате представляют собой набор из 4-х упакованных 32-разрядных чисел. Для программирования SSE-расширения система команд процессора дополнена целым рядом команд. Мы не будем рассматривать все ассемблерные команды SSE-расширения. Это потребовало бы сотен страниц текста. Автор счел необходимым сосре- сосредоточить внимание лишь на основных аспектах программирования SSE- расширения. Подробное описание архитектуры SSE, а также программиро- программирования этих инструкций на ассемблере можно найти в документации фирмы Intel. Прежде чем программировать SSE-расширение, необходимо определить, поддерживает ли процессор и операционная система это расширение. Под- Поддержку процессором расширения SSE легко определить, выполнив простое консольное приложение (листинг 10.26). // TEST_SSE_BY_PROC.cpp : Defines the entry point for the console // application. #include "stdafx.h" int _tmain(int argc, _TCHAR* argvf]) { bool supSSE = true; _asm { mov EAX, 1 cpuid test EDX, 0x2000000 j nz found mov supSSE, 0 found:
Глава 10. Встроенный ассемблер и оптимизация приложений... 345 if(supSSE)printf("SSE supported by CPU!\n"); else printf("SSE not supported by CPU!\n"); getchar(); return 0; Исходный текст программы очень напоминает профаммный код для опре- определения поддержки MMX-расширения, только здесь анализируется бит 25 ре- регистра EDX. Поддержку SSE-расширения операционной системой можно определить, если выполнить профамму, исходный текст которой приведен в листин- листинге 10.27. // CHECK_SSE_SUPPORT_BY_OS.срр : Defines the entry point for the console // application. #include "stdafx.h" #include <excpt.h> #include <windows.h> bool _tmain(int argc, _TCHAR* argv[]) { _try { _asm xorps xramO, xmmO } except (EXCEPTION_EXECUTE_HANDLER) { if (GetExceptionCodeO — STATUS_ILLEGAL_INSTRUCTION) { printf("SSE not supported by OS!\n"); getchar(); return (false); printf("SSE supported by OS!\n"); getchar(); return (true);
346 Часть HI. Встроенный ассемблер Visual C++ .NET2003 и его использование В C++ .NET поддержка SSE-расширения, как и MMX-расширения, обеспе- обеспечивается с помощью собственных функций. Как и в MMX-расширении, ка- каждая из собственных функций для работы с плавающей точкой представляет собой псевдокод ассемблерного эквивалента. Например, функция ml28 _rnm_add_ss( ml28 a , ml28 b ) представляет собой аналог ассемблерной команды addss. Операнды с пла- плавающей точкой представлены в C++ .NET как ml28. Более подробное описание собственных функций SSE-расширения имеется в справочной системе C++ .NET 2003. Сейчас мы рассмотрим практическое программи- программирование SSE с помощью встроенного ассемблера. Ассемблерные команды SSE-расширения можно разделить на несколько групп: ? команды пересылки; ? арифметические команды; ? команды сравнения; ? команды преобразования; ? команды, выполняющие логические операции. Кроме этих групп команд имеется целый ряд дополнительных команд. Арифметические команды, команды сравнения и команды преобразования могут выполняться либо над четырьмя упакованными двойными словами одновременно (параллельные), либо над 32-разрядными числами (скаляр- (скалярные). В скалярных операциях обрабатываются только младшие 32-битовые слова. Рассмотрим пример скалярного сложения двух вещественных чисел. Исход- Исходный текст программы представлен в листинге 10.28. // SSE_ADD_2_SCALAR.cpp : Defines the entry point for the console // application. #include "stdafx.h" int _tmain(int argc, _TCHAR* argv[]) { float al, Ы, cl; printf(" EXAMPLE OF SCALAR SUMMA IN SSE-EXT.ASM.OPERATIONSW) ; printf("\nEnter float al: ") ; scanf("%f", &al); printf ("Enter float Ы: ") ;
Глава 10. Встроенный ассемблер и оптимизация приложений. 347 scanf("%f", ьЫ); _asm { lea EAX, al lea EDI, Ы lea EDX, cl movss xmmO, DWORD PTR [EAX] addss xramO, DWORD PTR [EDI] movss DWORD PTR [EDX], xmmO printf ("cl getchar(); return 0; al+Ы = %.3f\n", cl); После ввода двух вещественных чисел ai и ы ассемблерный блок выполня- выполняет сложение этих чисел как обычных 32-разрядных величин. В регистры еах, edi, edx помещаются адреса элементов ai и ы, а также адрес их суммы cl. Затем с помощью команд movss xmmO, DWORD PTR [EAX] addss xmmO, DWORD PTR [EDI] movss DWORD PTR [EDX], xmmO выполняется суммирование и запоминание результата в переменой cl. Все скалярные команды имеют суффикс s, в то время как параллельные — р. Вид окна работающего приложения показан на рис. 10.17. Рис. 10.17. Окно приложения, выполняющего скалярное сложение двух вещественных чисел с помощью ассемблера SSE-расширения Параллельное сложение операндов позволяет значительно повысить произ- производительность приложений. Операции этого типа очень удобны для обра- обработки массивов вещественных чисел. Усложним наш предыдущий пример,
348 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование взяв в качестве переменных ai и ы два массива вещественных чисел, а в качестве ci — массив, содержащий их сумму. Исходный текст программы приведен в листинге 10.29. // SSE_ADD_2_FLOATS.срр : Defines the entry point for the console // application. #include "stdafx.h" int _tmain(int argc, _TCHAR* argv[]) { float al[4] = {34.5, -12.44, 7.53, -7.4}; float bl[4] = {3.54, 1.23, -3.56, 7.55}; float cl[4]; printf(" SUMMA 2 ARRAYS in parallel\n") printf("\n al: "); for (int cnt = 0;cnt <4;cnt++) printf("%.2f\t", al[cnt]); printf ("\n Ы: "); for (int cnt = 0;cnt <4;cnt++) printf("%.2f\t", bl[cnt]); _asm { lea EAX, al lea EDI, Ы lea ECX, cl movups xmmO, XMMWORD PTR [EAX] addps xrnmO, XMMWORD PTR [EDI] movups XMMWORD PTR [ECX], xrnmO }; printf("\n\n cl: "); for (int cnt = 0;cnt <4;cnt++) printf("%.2f\t", cl[cnt]); getchar(); return 0;
Глава 10. Встроенный ассемблер и оптимизация приложений... 349 Команды ассемблерного блока программы, как вы заметили, содержат суф- суффикс р. Кроме того, используется ключевое слово xmmword для обозначения 128-битовой переменной. Окно приложения показано на рис. 10.18. Рис. 10.18. Окно приложения, выполняющего параллельное сложение четырех вещественных чисел с помощью ассемблера SSE-расширения Команда параллельного вычитания 128-битовых операндов subps полезна при выполнении операции вычитания элементов массивов вещественных чисел. Следующий пример иллюстрирует кроме применения команды вычи- вычитания и некоторые важные моменты работы с SSE-расширением. Рассмот- Рассмотрим исходный текст программы (листинг 10.30), в котором для упрощения алгоритма используются массивы из 4-х вещественных чисел. // SSE_COMBO_SUB_2.срр : Defines the entry point for the console // application. #include "stdafx.h" #include <xrnmintrin.h> int _tmain(int argc, _TCHAR* argv[]) { declspec (alignA6)) float al[4]; declspec (alignA6)) float Ы[4]; declspec (alignA6)) float cl[4]; ml28 mal = {12.5, 32.7, -4.8, 6}; __ral28 mbl = {3.45, 12.67, -5V88, -23.1 ml28 mcl;
350 Часть HI. Встроенный ассемблер Visual C++ .NET2003 и его использование printf(" PARALLEL SUBSTRACTION OF 2 ALIGNED ARRAYS \n") ; _asm { lea EAX, al lea EDX, Ы lea ECX, cl movaps xmmO, mal movaps XMMWORD PTR [EAX], xmmO movaps xmml, mbl movaps XMMWORD PTR [EDX], xmml subps xmmO, xmml movaps mcl, xmmO movaps XMMWORD PTR [ECX], xmmO printf("\n al: "); for (int cnt = 0;cnt < 4; cnt++) printf("%.2f\t", al[cnt]); printf ("\n Ы: ") ; for (int cnt = 0;cnt < 4; cnt++) printf("%.2f\t", bl[cnt]); printf("\n\n cl: "); for (int cnt = 0;cnt < 4;cnt++) printf("%.2f\t", cl[cnt]); getchar(); return 0; Особенностью этого примера является то, что здесь демонстрируются опе- операции с переменными, выровнеными по 16-байтной границе. Для этого массивы вещественных чисел al, ы и cl определяются с ключевым словом align: declspec (alignA6)) float al[8]; declspec (alignA6)) float bl[8]; declspec (alignA6)) float cl[8]; Применение выравнивания данных может существенно увеличить быстро- быстродействие приложения. Разработка приложений, использующих продвинутые ассемблерные команды последних поколений процессоров, требует, чтобы данные были выровнены по 16-байтной границе. Кроме того, выравнивание
Глава 10. Встроенный ассемблер и оптимизация приложений... 351 часто используемых данных по размеру строки в кэше является весьма эф- эффективным средством для повышения производительности приложения. Например, если в программе определена структура, размер которой меньше 32 байт, то необходимо выровнять ее по 32-байтной границе для эффектив- эффективного кэширования. Для того чтобы читатель лучше понял взаимосвязь классических типов пе- переменных типа float и 128-битовых переменных SSE-расширения, я ис- использую в программе как переменные типа float, так и mi28. Перемен- Переменные типа mi28, как и собственные функции SSE-расширения, определены в заголовочном файле xmmintrin.h, поэтому он включен в наш проект. Для пересылки выровненных данных в программе используется команда movaps. Можно использовать вместо movaps команду movups для работы с невыровненными данными. Такая замена не вызовет ошибки, но тогда про- производительность программы будет ниже, и нет смысла использовать ключе- ключевое СЛОВО align. Окно работающего приложения показано на рис. 10.19. Рис. 10.19. Окно приложения, демонстрирующего вычитание выровненных по 16-байтной границе элементов массивов вещественных чисел с применением ассемблера SSE-расширения Можно модифицировать наш пример и не использовать переменные типа mi28. В этом случае можно исключить и файл заголовка xmmintrin.h, a также некоторые ассемблерные команды. Исходный текст программы будет выглядеть так, как показано в листинге 10.31. Г- у-. а и;; г * 0.3 '¦. М о д и ф и и * <; j о н а н н t- s и в а:-: « о ;¦¦< г '¦ ц к w j > л м v',! в к; ¦¦ i ;^ rt: < и и :> л о м с >:- то п // SSE_CCMBO_SUB_2_MQD.cpp : Defines the entry point for the console // application.
352 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование #include "stdafx.h" int _tmain(int argc, JTCHAR* argv[]) declspec(alignA6)) float al[4] = {34.5, -12.44, 7.53, -7.4}; declspec(alignA6)) float bl[4] = {3.54, 1.23, -3.56, 7.55}; declspec(alignA6)) float cl[4]; printf(" PARALLEL SUBSTRACTION OF 2 ALIGNED ARRAYS WITH ASM INSTRUCTIONS ONLY\n"); _asm { movaps xmmO, XMMWORD PTR al subps xmmO, XMMWORD PTR bl movaps XMMWORD PTR cl, xmmO printf("\n al: "); for (int cnt = 0;cnt < 4; cnt++) printf("%.2f\t", al[cnt]); printf("\n bl: "); • for (int cnt = 0;cnt < 4; cnt++) printf("%.2f\t", bl[cnt]}; printf("\n\n cl: ") ; for (int cnt = 0;cnt < 4;cnt++) printf("%.2f\t", client]); getchar(); return 0; Окно работающего приложения показано на рис. 10.20. Рис. 10.20. Окно модифицированного приложения, демонстрирующего параллельное вычитание элементов массивов
Глава 10. Встроенный ассемблер и оптимизация приложений... 353 Для операций умножения и деления 128-битовых данных можно использо- использовать следующие ассемблерные команды SSE-расширения: ? mulps — параллельное умножение 128-битовых операндов. Результат опе- операции помещается В ОДИН ИЗ регистров xmmO, . . ., xmm7; ? divps — параллельное деление двух 128-битовых операндов. Результат операции помещается в один из регистров xmmO, .... xmm7; О mulss — скалярное умножение младших двойных слов двух операндов. Результат операции C2-разрядное значение) помещается в один из реги- регистров xmmO, ..., xmm7. Один из операндов может быть 32-разрядной ячейкой памяти; ? divss — скалярное деление 32-разрядных операндов. Синтаксис команды такой же, как и для mulss. Далее (листинг 10.32) приведен исходный текст программы, демонстрирую- демонстрирующей параллельное умножение и деление. // SSE__MUL_DIV_ALIGN_2_ARRAYS. срр : Defines the entry point for the // console application. #include "stdafx.h" int _tmain(int argc, _TCHAR* argv[]) { ' declspec(alignA6)) float al[4] = {34.5, -12.44, 7.53, -7.4}; declspec(alignA6)) float Ы[4] = {3.54, 1.23, -3.56, 7.55}; declspec(alignA6)) float cl[4] = {1.5, 2.5, 3.5, 4.5}; declspec(alignA6)) float dl[4]; printf(" PAR. MUL/DIV OF 2 ALIGNED ARRAYS WITH ASM INSTRUCTIONS ONLY\n"); _asm { movaps xmmO, XMMWORD PTR al mulps xmmO, XMMWORD PTR Ы divps xmmO, XMMWORD PTR cl movaps XMMWORD PTR dl, xmmO }; printf("\n al: "); for (int cnt = 0;cnt < 4; cnt++) printf("%.2f\t", alfcnt]);
354 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование print f ("\n Ы: "); for (int cnt = 0;cnt < 4; cnt++) printf("%.2f\t", bl[cnt]); printf("\n cl: "); for (int cnt = 0;cnt < 4; cnt++) printf("%.2f\t", cl[cnt]); printf("\n\n dl: "); for (int cnt = 0;cnt < 4;cnt++) printf("%.2f\t", dlfcnt]); getchar(); return 0; Я не буду комментировать этот листинг, поскольку исходный текст не вы- вызывает затруднений для анализа. Окно работающего приложения показано на рис. 10.21. Рис. 10.21. Окно приложения, демонстрирующего параллельное умножение и деление элементов массивов вещественных чисел с помощью ассемблера SSE-расширения Умножение и деление скалярных величин рассмотрим в следующем приме- примере (листинг 10.33). // SCALAR_SSE_MUL_DIV_WITH_ASM.cpp : Defines the entry point for the // console application. #include "stdafx.h"
Глава 10. Встроенный ассемблер и оптимизация приложений.. 355 int _tmain(int argc, _TCHAR* argv[]) { float al[4] = {4.98, 1.44, 3.16, -0.42}; float bl[4] = {-3.54, 1.23, -9.56, 5.09}; float cl[4] = {1.5, 2.5, 3.5, 4.5}; float dl[4]; printf(" SCALAR MUL/DIV OF 2 ARRAYS WITH ASM \n") ; int asize = sizeof(al)/4; _asm { lea EAX, al lea EDX, Ы lea ESI, cl lea EDI, dl mov sub sub sub sub next_4: add add add add movss mulss divss movss loop ECX, EAX, EDX, ESI, EDI, EAX, EDX, ESI, EDI, xinmO, xmmO, xmmO, DWORD next asize 4 4 4 4 4 4 4 4 DWORD PTR DWORD PTR DWORD PTR PTR [EDI], 4 [EAX] [EDX] [ESI] xranO printf("\n al: "); for (int cnt = 0;cnt < 4; cnt++) printf("%.2f\t", al[cnt]); printf("\n bl: "); for (int cnt = 0;cnt < 4; cnt++) printf("%.2f\t", bl[cnt]); printf("\n cl: ");
356 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование for (int cnt = 0;cnt < 4; cnt++) printf("%.2f\t", cl[cnt]); printf("\n\n dl: "); for (int cnt = 0;cnt < 4;cnt++) printf("%.2f\t", dl[cnt]); getchar(); return 0; Поскольку мы имеем дело с 32-разрядными величинами, то для доступа к ним используем, как обычно, регистры общего назначения еах, edx, esi, edi. В эти регистры загружаются адреса массивов. Регистр есх используется в качестве счетчика элементов массивов. Для доступа к следующим элемен- элементам массивов значения регистров еах, edx, esi, edi после каждой итерации увеличиваются на 4. Для операций умножения и деления чисел применяются ассемблерные команды muiss и divss SSE-расширения. Окно работающего приложения показано на рис. 10.22. Рис. 10.22. Окно приложения, демонстрирующего скалярное умножение-деление элементов массивов вещественных чисел Следующая группа ассемблерных команд SSE-расширения, которую мы рассмотрим, — команды сравнения. Лучше всего понять работу команд сравнения можно на примерах. В первом примере рассмотрим способ определения равенства двух упако- упакованных 128-битовых величин. Результатом сравнения является 128-битовая маска. Равенство 1 всех битов маски означает равенство двух 128-битовых чисел с плавающей точкой.
Глава 10. Встроенный ассемблер и оптимизация приложений... 357 В нашем примере мы будем использовать этот принцип для определения равенства элементов массивов вещественных чисел. Для упрощения алго- алгоритма выберем размер массивов равным 4. Чтобы легче было понять после- последовательность действий, вначале рассмотрим вариант решения задачи с ис- использованием собственных функций C++ .NET 2003 для SSE-расширения. Исходный текст консольного приложения приведен в листинге 10.34. // SSE_CMPEQPS_INTR_EXAMPLE.cpp : Defines the entry point for the console // application. #include "stdafx.h" #include <xramintrin.h> int _tmain(int argc, _TCHAR* argv[]) { ml28 al = {12.4, 19.1, -4.68, 3.12}; _ml28 a2 = {12.4, 19.1, -4.68, 3.12}; ml28 ares; float res[4]; ares = _mm_cmpeq_ps (al, a2); _mm_storeuj?s(res, ares); printf("Result of comparison 2 packed elements\n\n") for (int cnt = 0;cnt < 4;cnt++) printf("res[%d] = %f\n", cnt, res[cnt]); for (int cnt = 0;cnt < 4;cnt++) { if(res[cnt] == 0) { printf("\nSSE-operands are not equals!\n"); getchar (); return 0; printf ("XnSSE-opera'nds are equals!\n");
358 getchar() ; return 0; Часть III. Встроенный ассемблер Visual C++ .NET 2003 и его использование Проанализируем исходный текст. Как обычно, если приложение использует собственные функции и переменные типа mi28, в листинг программы не- необходимо включить заголовочный файл xmmintrin.h (соответствующая строка выделена жирным шрифтом). Переменным ai и а2 типа ml28 присвоены значения 4-х вещественных чисел. Фактически элементы, находящиеся в фигурных скобках, представ- представляют собой массив вещественных чисел. Он не определен явным образом, но оперировать с элементами такого "виртуального" массива очень удобно посредством 128-битовых переменных. Подобные манипуляции допускаются в C++ .NET. Операция попарного сравнения массивов ai и а2 выполняется при помощи функций: ares = _itim_cmpeq_ps (al, a2) _mm_storeu_ps(res, ares) а результат сравнения записывается в массив res. Для данных значений элементов ai и а2 операция сравнения покажет равенство массивов. Это видно из рис. 10.23. Рис. 10.23. Результат выполнения операции сравнения элементов массивов из программы, представленной в листинге 10.34/ с помощью собственных функций Как видно из рис. 10.23, после сравнения всем элементам массива res при- присвоено значение -1, что соответствует единицам во всех разрядах. В данном случае это означает равенство элементов массивов ai и а2.
Глава 10. Встроенный ассемблер и оптимизация приложений... 359 Если немного изменить исходные данные, например, присвоить элементу 3 массива ai вместо 3.12 значение 3.13, то результат сравнения изменится. Это продемонстрировано на рис. 10.24. Рис. 10.24. Результат сравнения массивов в случае неравенства 3-х элементов Поскольку третьи элементы массивов не равны, то соответствующий эле- элемент массива res равен 0, что свидетельствует о неравенстве массивов ai И а2. Как и при работе с MMX-расширением, применение собственных функций для программирования SSE приводит к избыточности кода. Например, опе- операторы ares = _rnm_cmpeq_ps (al, a2) _mm_storeu__ps (res, ares) из листинга 10.34 в дизассемблированном виде выглядят, как представлено в листинге 10.35. area =» _mm_cmpec{_ps (al, a2) ; 00411С78 00411С7С 004ИС80 00411С84 00411С8В 00411С92 movaps movaps cmpeqps movaps movaps movaps xmmO,xmmword ptr [a2] xmml,xmmword ptr [al] xmmlfxmmO xmmword ptr [ebp-150h],xmml xmmO,xmmword ptr [ebp-150h] xmmword ptr [ares],xmmO
360 Часть III. Встроенный ассемблер Visual C++ .NET 2003 и его использование _mm_storeu_ps(res, ares); 00411С96 movaps xmmO,xmmword ptr [ares] 00411C9A lea eax,[res] 00411C9D movups xmrnword ptr [eax],xmmO Желание разработчиков Microsoft избежать манипуляций с регистрами xirano, ..., xinm7 в программе на C++.NET и хранить результат в 128-битовой ячейке памяти приводит к избыточности кода. Команды 00411С84 movaps xmmword ptr [ebp-150h],xmml 00411C8B movaps xmmO,xmmword ptr [ebp-150h] с успехом могут быть заменены на movaps xmmO, xmml Команду 00411С80 cmpeqps xmml,xmmO можно заменить командой, одним из операндов которой является ячейка памяти. Модифицируем наш пример так, чтобы можно было использовать команды встроенного ассемблера. Исходный текст программы показан в листин- листинге 10.36. // SSE_CMPEQPS_ASM_EXAMPLE.cpp : Defines the entry point for the console // application. #include "stdafx.h" int _tmain(int argc, _TCHAR* argv[]) { float al[4] = {12.4, 19.17, -4.68, 3.12}; float a2[4] = {12.4, 19.1, -4.68, 3.12}; float res [4] ; printf("Comparison 2 packed elements with SSE assembler\n\n"); printf("al: ") ; for (int cnt = 0;cnt < 4;cnt++) printf("%.2f\t", al[cnt]);
Глава 10. Встроенный ассемблер и оптимизация приложений... 361 printf("\na2: "); for (Int cnt = 0;cnt < 4;cnt++) printf("%.2f\t", a2[cnt]); _asm { movups xmmO, XMMWORD PTR al cmpeqps xmmO, XMMWORD PTR a2 movups XMMWORD PTR res, xmmO printf("\nResult of comparison:\n\n"); for (int cnt = 0;cnt < 4;cnt++) printf("res[%d] = %f\n", cnt, res[cnt]); for (int cnt = 0;.cnt < 4;cnt++) { if(res[cnt] == 0) { printf("\nSSE-operands are not equals!\n") getchar(); return 0; printf("\nSSE-operands are equals!\n"); getchar(); return 0; Операция сравнения выполняется в ассемблерном блоке и требует всего лишь трех команд ассемблера! Кроме того, для иллюстрации работы с веще- вещественными числами типа real в SSE-расширении я заменил тип перемен- переменных al, a2 и ares с ml28 на float. Собственно сравнение двух 128- битовых величин выполняется командой cmpeqps. Эта команда выполняет попарное сравнение четырех 32-разрядных упакованных чисел и в случае их равенства устанавливает в 1 биты в соответствующих позициях 128-битовой маски-результата. Если соответствующие упакованные 32-разрядные числа не равны, — в соответствующие позиции записываются нули. Окно работающего приложения показано на рис. 10.25.
362 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование Рис. 10.25. Окно приложения, демонстрирующего операцию сравнения массивов вещественных чисел с помощью команды cmpeqps MMX-расширения Кроме проверки чисел на равенство, очень часто возникает необходимость в сравнении величин двух чисел. Следующий пример показывает, как можно сравнивать элементы двух вещественных массивов по принципу "больше- меньше" с использованием ассемблерной команды cmpieps SSE-расшире- ния. Исходный текст консольного приложения C++ .NET приведен в лис- листинге 10.37. // SSE_CMPLEPS_ASM.cpp : Defines the entry point for the console // application. #include "stdafx.h" int _tmain(int argc, _TCHAR* argv[]) { float al[4] = {3.4, 9.17, -4.39, 3.12}; float a2[4] = {12.7, 19.1, -4.68, 3.52} float res [ 4 ] ; printf("LT-EQ comparison 2 packed elements with SSE-ext, asseinbler\n\n") ; printf("al: ") ; for (int cnt = 0;cnt < 4;cnt++) printf("%.2f\t", al[cnt]); printf("\na2: ") ;
Глава 10. Встроенный ассемблер и оптимизация приложений... 363 for (int cnt = 0;cnt < 4;cnt++) ?rintf("%.2f\t", a2[cnt]); asm { movups xmmO, XMMWORD PTR al anpleps xrranO, XMMWORD PTR a2 movups XMMWORD PTR res, xramO printf("\n\nResult of comparison:\n\n"); for (int cnt = 0;cnt < 4;cnt++) { if(res[cnt] i= 0) printfC\nal[%d] <= a2[%d], mask = %.2f\n", cnt, cnt, res[cnt] else printf("\nal[%d] > a2[%d], mask = %.2f\n", cnt,cnt, res[cnt]); } getchar(); return 0; Сравнение элементов массивов, как и в предыдущем примере, сводится к сравнению двух 128-битовых чисел, при этом одна из четырех 32-битовых масок устанавливает биты в " или "О" в зависимости от результата сравне- сравнения 32-разрядных пар упакованных чисел на соответствующих позициях в источнике и приемнике. Если, например, одно из 32-разрядных чисел, со- соответствующих элементу массива ai, не превосходит числа на соответст- соответствующей позиции в 128-битовой переменной, представляющей массив а2, то в соответствующие позиции маски-результата записываются единицы. В противном случае, когда элемент массива ai больше а2, на соответствую- соответствующие позиции в маске записываются нули. Окно работающего приложения показано на рис. 10.26. Рассмотренные примеры демонстрируют параллельное сравнение упакован- упакованных элементов. Существует еще несколько ассемблерных команд SSE- расширения, которые выполняют скалярное сравнение младших пар двой- двойных слов. Результат сравнения определяется установкой соответствующих битов в регистре флагов. Одной из таких команд является comiss. Следующий пример демонстрирует применение этой команды для попар- попарного сравнения элементов двух массивов вещественных чисел. Исходый текст программы приведен в листинге 10.38.
364 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование Рис. 10.26. Окно приложения, демонстрирующего сравнение массивов вещественных чисел на "больше-меньше" с помощью ассемблерных команд SSE-расширения // SSE_COMISS_ASM.cpp : Defines the entry point for the console // application. #include "stdafx.h" int _tmain(int argc, JTCHAR* argv[]) { float al[4] = {12.7, 19.17, -4.68, 3.52}; float a2[4] = {12.7, 19.17, -4.68, 3.52}; bool cres = true; printf("EQ comparison 2 arrays with COMISS \n\n"); printf("al: ">; for (int cnt = 0;cnt < 4;cnt++) printf("%.2f\t", al[cnt]); printf("\na2: "); for (int cnt = 0;cnt < 4;cnt++) printf("%.2f\t", a2[cnt]); asm { lea EAX, al lea EDX, a2
Глава 10. Встроенный ассемблер и оптимизация приложений. 365 next: mov EBX, 0 mov ECX, 4 movss xramO, DWORD PTR [EAX] comiss ximO, DWORD PTR [EDX] no_eq: j ne no_eq add EAX, 4 add , EDX, 4 loop next mov EBX, 1 mov DWORD PTR cres, EBX if (cres)printf("\nEquals!\n"); else printf("\nUnequals!\n"); getchar(); return 0; Сравнение элементов выполняется в ассемблерном блоке. Для выполнения операций в цикле нам понадобится в каждой итерации продвигать указатель ячейки памяти к следующему элементу. Поэтому смысл команд lea lea И add add EAX, EDX, EAX, EDX. al a2 4 4 думаю, понятен. Собственно сравнение операндов выполняется командой comiss " xmmO, DWORD PTR [EDX] При этом в младшей части регистра xmmO находится элемент массива al и сравнивается с элементом массива а2. Текущий адрес элемента из а2 Нахо- Находится в регистре edx. Регистр есх является счетчиком итераций, равным размеру массивов. В зависимости от результата сравнения массивов регистр евх будет содержать 1 или о. Значение 1 свидетельствует о равенстве масси- массивов, значение о — о том, что массивы не равны.
366 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование Окна работающего приложения показано на рис. 10.27—10.28. Рис. 10.27. Окно приложения, демонстрирующего скалярное сравнение с помощью команды comiss для случая равенства массивов Рис. 10.28. Окно приложения, демонстрирующего скалярное сравнение с помощью команды comiss для случая неравенства массивов SSE-расширение включает в себя целый ряд команд, позволяющих выпол- выполнить взаимное преобразование форматов SSE, ММХ и обычного целочис- целочисленного формата. Команды этой группы могут выполнять как скалярные, так и параллельные операции. Рассмотрим применение этих команд на примерах. Первый пример касается параллельного преобразования двух 32-битовых целочисленных значений, находящихся в ММХ-регистре mmO, в два 32- битовых числа с плавающей точкой, помещаемых в младшие два слова SSE- регистра xmmo. Исходный текст приложения показан в листинге 10.39. // MMX_INT_INTO_SSE_FLOAT.срр // application. Defines the entry point for the console
Глава 10. Встроенный ассемблер и оптимизация приложений... 367 #include "stdafx.h" int _tmain(int argc, _TCHAR* argv[]) { int il[2]; float fl[2]; printf("PARALLEL CONV. 2 INTS TO 2 FLOAT WITH SSE-EXT.ASM\n\n"); while (true) { printf("\nEnter first integer il[0]: ") ; scanf("%d", il); printf("\nEnter second integer il[l]: "); scanf("%d", &il[l]); printf("\n"); _asm { movq mmO, MMWORD PTR il cvtpi2ps xmmO, inmO movlps DWORD PTR fl, xmraO emms }; for (int cnt = 0; cnt < 2;cnt++) printf("fl[%d] = %.3f\n", cnt,'fl[cnt]); printf("fl[0] / fl[l] = %.3f\n", fl[O]/fl[l]); }; return 0; Параллельное преобразование двух 32-разрядных вещественных чисел в два 32-разрядные целые выполняется с помощью команды cvtpi2ps. Ассемб- Ассемблерный блок, выполняющий преобразование, содержит команды как SSE-, так и MMX-расширения. Команда movq mmO, MMWORD PTR il пересылает 64-битовое число (два целых) в регистр mmO. Следующая команда cvtpi2ps xmmO, rnmO преобразует два 32-разрядных числа, представленных в упакованном форма- формате в регистре mmO, в два числа с плавающей точкой и помещает результат в
368 Часть III. Встроенный ассемблер Visual C++ .NET 2003 и его использование SSE-регистр xinmo. При этом старшие два двойные слова регистра xmmo не изменяются. Точность результата преобразования зависит от установки со- соответствующих битов в регистре состояния/управления SSE-расширения. Сохранение результата* преобразования в виде вещественного массива из двух 32-разрядных чисел выполняется с помощью команды movlps DWORD PTR fl, xinmO, пересылающей младшие два двойных слова из регистра xmmo в массив fi. Для иллюстрации того, что преобразование целых чисел в вещественные выполнено корректно, оператор printf("fl[0] / fl[l] = %.3f\n", fl[0]/fl[l]) выводит результат деления двух вещественных чисел на экран. Окно работающего приложения показано на рис. 10.29. Рис. 10.29. Окно приложения, демонстрирующего параллельное преобразование двух 32-битовых целых чисел в формат с плавающей точкой при помощи ассемблерных команд SSE-расширения Процедуру обратного преобразования из 32-битового числа с плавающей точкой в целочисленный формат демонстрирует следующий пример, исход- исходный текст которого показан в листинге 10.40. : ДО. Пропорл?.ог!.'-'нис iit.:i.i.ie;"T:.n.:HH; if i:t\-:yc P- SSL-^:-.;.:i:n:j::M»!: ¦¦.'¦ ...¦ <? ПО'^'Л П С; i Н Ь < *Ч фО V- !¦ Л О ¦ ЬА Ы "К // SSE_INTO_MMX_CO^A^.cpp : Defines the entry point for the console // application.
Глава 10. Встроенный ассемблер и оптимизация приложений... 369 #include "stdafx.h" int _tmain(int argc, _TCHAR* argv[]) int float fl[2] = {0,0}; printf("PARALLEL CONV. 2 INTS TO 2 FLOAT WITH SSE-EXT.ASM\n\n"); printf("Enter real fl: "); scanf("%f", &fl[0]); printf("Enter real f2: "); scanf("%f", &fl[l]); _asm { movlps xmmO, QWORD PTR fl cvtps2pi inmO, xramO rnovq QWORD PTR il, mmO einrns }; printf ("\nAfter conversion fl —> il, f2 ~> i2\n\n") ; printf("il = %d\n", il[0]); printf("i2 = %d\n", il[l]); printf("il / i2 = %d\n", il[0] / i getchar(); getchar О; return 0; Параллельное преобразование двух вещественных чисел в два целых выпол- выполняется в ассемблерном блоке командой cvtps2pi mmO, xmmO Вещественные числа из SSE-регистра xmmO преобразуются в целочислен- целочисленный формат и сохраняются в ММХ-регистре mmO. Команды movlps и movq обеспечивают пересылку данных. Следует учитывать, что при использова- использовании ассемблерных команд MMX-расширения последней командой должна быть emms!
370 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование Для иллюстрации корректности преобразования с помощью оператора printf("il / 12 = %d\n", il[0] / il [1]) выводится результат деления двух целых чисел. Окно работающего приложения показано на рис. 10.30. Рис. 10.30. Окно приложения, демонстрирующего параллельное преобразование 32-битовых целых чисел в формат вещественных чисел с помощью ассемблера Кроме рассмотренных нами команд параллельного преобразования SSE- расширение включает и команды скалярного преобразования. С помощью таких команд выполняется преобразование 32-разрядных чисел. Я не буду рассматривать здесь примеры применения этих команд и оставляю это чита- читателям в качестве упражнения. Заканчивая обзор ассемблерных команд SSE-расширения и практических примеров их применения, хочу остановиться на нескольких специфичных, но очень полезных инструкциях. Это команды извлечения квадратного кор- корня и определение максимального/минимального значения пары чисел. SSE- расширение включает команды как параллельных, так и скалярных вычис- вычислений. Рассмотрим пример параллельного извлечения квадратного корня из упако- упакованных чисел с плавающей точкой. Для выполнения этой операции исполь- используется команда sqrtps. Исходный текст программы представлен в листин- листинге 10.41. // PARALLEL_SQRT.срр : Defines the entry point for the console // application.
Глава 10. Встроенный ассемблер и оптимизация приложений... 371 #include "stdafx.h" int _tmain(int argc, JTCHAR* argv[]) { declspec (alignA6)) float fl[4] - {34.78, 23.56, 876.98, 9423.678}; float fsqrt[4]; printf("PARALLEL SQRT CALCULATION WITH SSE-EXT.ASSEMBLER\n\n"); printf("fl: "); for (int cnt - 0;cnt < 4; cnt++) printf("%.3f\t", flfcnt]); printf("\n\n"); _asm { movaps xmmO, XMMWORD PTR fl sqrtps xmmO, xmmO movups XMMWORD PTR fsqrt, xmmO }; for (int cnt = 0;cnt < 4; cnt++) printf("SQRT(%.3f)= %.3f\n", fl[cnt], fsqrt[cnt]); getchar(); return 0; Вычисление квадратного корня выполняется командой sqrtps xmmO, xmmO Операндами этой команды в данном случае выступают два SSE-регистра xmmo и xrami, а результат помещается в регистр xmmO. Приемником результата всегда является один из регистров хгапо, ..., xmm7. В качестве второго опе- операнда может использоваться 128-битовая ячейка памяти. Для достижения большей производительности адрес 128-битовой переменной должен быть выровнен по 16-байтной границе, что и сделано в строке _declspec (alignA6)) float fl[4] = C4.78, 23.56, 876.98, 9423.678} Окно работающего приложения показано на рис. 10.31. Наш последний пример, в котором демонстрируются возможности SSE- расширения, — поиск максимальных и минимальных элементов среди пар упакованных вещественных чисел. Для поиска максимума и минимума ис- используются ассемблерные команды SSE-расширения maxps и minps. Обе команды оперируют с 128-битовыми упакованными операндами, причем
372 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование приемником результата может быть только один из SSE-регистров. Исход- Исходный текст приложения показан в листинге 10.42. Рис. 10.31. Окно приложения, демонстрирующего параллельное извлечение квадратного корня из упакованных вещественных чисел с помощью ассемблера SSE-расширения // SSE_PARALEL_MIN_MAX.cpp : Defines the entry point for the console // application. #include "stdafx.h" int _tmain(int argc, _TCHAR* argv[]) declspec (alignA6)) float fl[4] declspec (alignA6)) float f2[4] {34.78, 23.56, 876.98, 9423.678}; {34.98, 21.37, 980.43, 1755.786}; float fmin [ 4 ] ; float fmax[4] ; int choice; printf("PARALLEL MINIMAX CALCULATION WITH SSE-EXT.ASSEMBLER\n\n"); printf("fl: "); for (int cnt = 0;cnt < 4; cnt++) printf("%.3f\t", fl[cnt]); printf("\n"); printf("f2: ");
Глава 10. Встроенный ассемблер и оптимизация приложений... 373 for (int cnt = 0;cnt < 4; cnt++) printf("%.3f\t", f2[cnt]); while (true) printf("\n\n"); printf("Enter 0 - get MAX elements, 1 - get MIN elements: "); scanf("%d", &choice); printf("\n"); switch(choice) case 0: _asm { movaps xmmO, XMMWORD PTR fl maxps xmmO, XMMWORD PTR f2 movups XMMWORD PTR fmax, xmmO printf("\n\nMAX: "); for (int cnt = 0;cnt < 4; cnt++) printf("%.3f\t", fmax[cnt]); break; case 1: movaps xmmO, XMMWORD PTR fl , minps xmmO, XMMWORD PTR f2 movups XMMWORD PTR fmin, xmmO }; printf("\n\nMIN: "); for (int cnt = 0;cnt < 4; cnt++) printf("%.3f\t", fmin[cnt]); break; default: break; return 0;
374 Часть III. Встроенный ассемблер Visual C++ .NET 2003 и его использование Окно работающего приложения показано на рис. 10.32. Рис. 10.32. Окно приложения, демонстрирующего параллельный поиск максимальных и минимальных элементов в двух массивах вещественных чисел с помощью ассемблера Можно сделать некоторые выводы относительно применения расширений ММХ и SSE технологии SIMD. Применение этих расширений значительно ускоряет работу приложений при обработке больших объемов данных при ограниченных ресурсах времени на обработку, поскольку данные могут об- обрабатываться параллельно в одном цикле. Несмотря на присутствие в Visual C++ .NET 2003, равно как и в других языках программирования собствен- собственных функций для работы с SIMD-технологиями, встроенный ассемблер обеспечивает выигрыш в быстродействии по сравнению с ними, поскольку обладает фундаментальным преимуществом — отсутствием избыточности. Более того, сами собственные функции построены с помощью ассемблера. Операции с упакованными числами расширений ММХ и SSE обладают по- повышенной точностью, и при прочих равных условиях следует отдавать им предпочтение. Прежде чем использовать широкие возможности для оптимизации про- программ, предоставляемые SIMD-технологиями, следует тщательно продумать алгоритм задачи и оценить целесообразность их применения. Из-за ограни- ограниченности объема книги не все возможности SIMD были рассмотрены, но я надеюсь, что читатели извлекли немалую пользу для себя и смогут эффек- эффективно применить эти технологии в своих программах.
Глава 10. Встроенный ассемблер и оптимизация приложений... 375 10.4. Обработка строк с помощью встроенного ассемблера Встроенный ассемблер можно с успехом применить и при обработке строк. Несмотря на то, что среда разработки C++ .NET имеет мощные процедуры обработки строк, использование ассемблера оказывается эффективным и здесь. Дело в том, что часто требуется специфичная обработка строковых переменных, и реализация такой обработки стандартными процедурами ока- оказывается весьма громоздкой и медленной. Вначале рассмотрим наиболее широко используемые типы строк и методы их конвертации. Как и во всех языках высокого уровня, в C++ .NET широко используются строки с завершающим нулем. Для манипуляции с такими строками* в этой среде программирования разработано много самых разнообразных функций. Оптимизация обработки таких строк при помощи ассемблерных процедур была рассмотрена нами в главах 2, 3. Однако в C++ .NET используются и другие типы строк. Сложность мани- манипуляций со строками с завершающим нулем привела разработчиков Micro- Microsoft к необходимости создания класса cstring. Этот класс стал весьма попу- популярным среди программистов. Строка типа cstring представляет собой последовательность символов переменной длины. Символы строки могут быть как 16-битовые (кодировка UNICODE), так и 8-битовые (кодировка ANSI). Для манипуляции со строками используются методы и свойства класса cstring. Этот класс имеет мощные функции для работы со строками, превосходящие по своим возможностям некоторые стандартные функции языка C++, такие как strcat или strcopy. Для инициализации cstring объекта можно использовать оператор cstring: CString s = "Это строка типа CString"; Можно присвоить значение одного объекта cstring другому: CString si = "Это тестовая строка"; CString s2 = si; При такой операции содержимое si копируется в s2. Для конкатенации двух и более строк можно использовать операторы "+" или "+=fm: CString si = "Строка 1"; CString s2 = "Строкой 2"; si += " объединяется "; CString sres= si + "со " + s2; В результате выполнения этой последовательности получим результирую- результирующую строку: Строка 1 объединяется со Строкой 2
376 Часть III. Встроенный ассемблер Visual C++ .NET 2003 и его использование Чтобы манипулировать с отдельными элементами строки cstring, можно использовать функции GetAt и set At этого класса. Первый элемент строки всегда имеет индекс 0. Например, чтобы получить символ строки si с ин- индексом 3, имеющей значение "строка 1", можно выполнить следующий оператор: si.GetAtC) Такого же результата можно добиться, используя оператор " []". Тогда дос- доступ к элементу строки будет выглядеть так же, как и к элементу массива: sl[3] Результатом операции будет символ • о •. Чтобы поместить в позицию с ин- индексом 5 F-й элемент) этой же строки символ 'и1, необходимо выполнить оператор: sl.SetAtE, 'И') Самой мощной функцией класса cstring является функция Format. Она позволяет преобразовать данные других типов в текст и напоминает стан- стандартные функции sprintf и wsprintf. В предыдущих примерах мы приме- применяли эту функцию для вывода элементов массива в поле редактирования Edit. Приведу небольшой фрагмент программного кода: for (int cnt = 0; cnt < size_il; cnt++) { s1.Format("%d", il[cnt]); s_Src = s_Src + " " + si; >; Этот код используется для вывода элементов целочисленного массива ii в поле редактирования Edit. Элемент управления Edit имеет тип cstring, т. к. связан с переменной s_src типа cstring. Здесь же присутствует вспо- вспомогательная переменная si, имеющая такой же тип, которую мы использу- используем для преобразования целочисленного элемента массива в строковый тип. Оператор s_Src = s_Src + " " + si; нам знаком и применяется для вывода преобразованных элементов массива на экран. Как видите, класс cstring во многом упрощает работу со строками (мы рас- рассмотрели только малую часть его возможностей!). Каким же образом можно манипулировать объектами cstring, используя встроенный ассемблер? Лучше всего продемонстрировать это на примере. Рассмотрим следующую задачу: требуется в строке типа cstring заменить все символы пробела сим- символами "+" и вывести результат преобразования на экран.
Глава 10. Встроенный ассемблер и оптимизация приложений... 377 Для решения задачи разработаем приложение на основе диалогового окна и разместим на нем три элемента Edit, кнопку Button и три метки статиче- статического текста Label. Поставим в соответствие элементу Editi переменную si типа cstring, элементу Edit2 — переменную s2 типа cstring и, наконец, элементу Edit3 — переменную lengthsi целого типа. В поле редактирования Editi будет вводиться исходная строка с пробелами, в поле Edit2 будет выведен результат замены пробелов на символы "+", а в поле Edit3 будет отображен размер строки. Вначале рассмотрим фрагмент программного кода для обработки строки, написанный на C++ .NET (листинг 10.43). void CReplacecharinStringDlg::OnBnClickedButtonl() { // TODO: Add your control notification handler code here UpdateData(TRUE); LPSTR Ips2; s_Len = strlen((LPCTSTR)si); s2 = si; Ips2 = s2.GetBufferA28); for (int cnt = 0; cnt < s_Len; cnt++) { if (*lps2 == ' •) *lps2 = •+•; Ips2++; } UpdateData(FALSE); s2.ReleaseBuffer; Для доступа к произвольному элементу строки или массива, как известно, необходимо знать адрес этого массива, его размер и тип элементов, входя- входящих в этот массив. Для строк с завершающим нулем адресом строки являет- является адрес первого элемента. Доступ к элементам строки выполняется через индексирование адреса строки.
378 Часть III. Встроенный ассемблер Visual C++ .NET 2003 и его использование Чтобы получить доступ к элементам строки cstring, можно воспользоваться функцией GetBuffer, передав ей в качестве параметра размер буфера памя- памяти. В данном случае 128 байт вполне достаточно. Результатом выполнения этой функции является указатель на буфер в памяти, что позволяет работать с отдельными элементами так же, как и в обычных функциях обработки строк. Воспользовавшись парой операторов: LPSTR Ips2; Ips2 = s2.GetBufferA28); получим адрес буфера строки. Остается определить размер строки. Особых проблем здесь тоже не возникает, достаточно воспользоваться классической функцией strien и сохранить результат в переменной s_Len: s_Len = strien((LPCTSTR)si); Далее остается выполнить поиск символов пробела в буфере строки и заме- заменить их символом • + '. Это делается при помощи цикла for. После выпол- выполнения всех манипуляций необходимо освободить буфер: s2.ReleaseBuffer; Окно работающего приложения изображено на рис. 10.33. Рис. 10.33. Окно приложения, выполняющего замену символа пробела на символ плюс в строке типа CString Можно оптимизировать предыдущую программу, заменив цикл for ком- компактной ассемблерной процедурой. Исходный текст процедуры на ассемб- ассемблере (назовем ее repiacechar) приведен в листинге 10.44.
Глава 10. Встроенный ассемблер и оптимизация приложений... 379 Листинг 10.44. Функция на ассемблере, выполняющая поиск н замену в стоске типа Си trine* void CReplaceCharinCStringwithBASMDlg::replaceChar(char* psl, int lsl) i X _asm { mov mov eld mov next: scasb je cont: loop jmp change: mov jmp ex: . EDI, psl ECX, lsl AL, ' ' change next ex [EDI-1], '+' cont Процедура принимает в качестве параметров адрес буфера строки и размер строки. Адрес буфера загружается в регистр edi, а размер строки — в ре- регистр есх. Для поиска и замены символов воспользуемся строковой коман- командой scasb, которая сравнивает содержимое регистра al (символ пробела) с текущим элементом строки. Количество итераций определяется размером строки. Так как после сравнения значение адреса было увеличено на 1, то если пробел найден, он заменяется на символ • + • с помощью команды: mov [EDI-1], '+' Исходный текст обработчика нажатия кнопки с учетом сделанных измене- изменений приведен в листинге 10.45. void CReplaceCharinCStringwithBASMDlg::OnBnClickedButtonl() { // TODO: Add your control notification handler code here
380 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование UpdateData(TRUE); LPSTR Ips2; length__sl = strlen( (LPCTSTR) si) ; s2 = si; Ips2 = s2.GetBufferA28); replaceChar(Ips2, length_sl); UpdateData(FALSE); s2.ReleaseBuffer; Обработку строк с помощью встроенного ассемблера особенно выгодно ис- использовать в тех случаях, когда требуются специальные манипуляции с эле- элементами строк или когда сам алгоритм обработки элементов строю"является сложным. Разработка программного кода только с использованием C++ для подобных задач обычно приводит к неоправданному усложнению програм- программы и замедлению быстродействия. Разумное сочетание ассемблера и C++ в этом случае является наилучшим решением. Мы проанализировали возможности встроенного ассемблера Visual C++ .NET 2003 для разработки эффективных алгоритмов. Главное внимание бы- было уделено технике применения встроенного ассемблера на практике при работе с различными типами данных. Хочется подчеркнуть, что изложенный материал далеко не исчерпывает возможностей современных технологий обработки данных, таких как ММХ и SSE, но создает хорошую базу для дальнейшей работы в этом направлении.
Глава 11 Оптимизация мультимедийных приложений с помощью ассемблера Мультимедийные приложения наиболее требовательны к производительно- производительности. Они работают в реальном времени и выдвигают жесткие требования к аппаратному и программному обеспечению. В этой главе мы рассмотрим некоторые методы оптимизации приложений мультимедиа с помощью ас- ассемблера, но вначале сделаем несколько общих замечаний, касающихся улучшения производительности независимо от того, на каком языке напи- написан программный код. Для мультимедийных приложений он должен быть как можно проще. То же самое относится и к структурам данных, используемым в приложениях. По возможности следует избегать преобразований типов данных. Преобразова- Преобразования целых чисел в вещественные и наоборот снижают производительность, поскольку требуют дополнительных команд. Для операций с переменными лучше использовать 32-разрядные данные. Данные, имеющие 8 или 16 разрядов, занимают меньший объем памяти, но для процессоров Pentium наиболее оптимальным является вариант операций с 32-разрядными данными. В мультимедийных приложениях следует избегать операций с плавающей точкой, поскольку целочисленные преобразования выполняются быстрее. Передавать данные в функции следует по ссылке, а не по значению. Кроме того, данные желательно выравнивать на границу двойного слова. Предпоч- Предпочтительно использовать глобальные переменные вместо локальных. Улучшения производительности мультимедийных приложений можно до- добиться за счет оптимизации алгоритмов преобразования векторных данных и использования многопоточности. Концепция многопоточности является очень важным аспектом написания мультимедийных приложений. Ни одно серьезное мультимедийное прило- 13 3ак. 243
382 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование жение не обходится без потоков. Потоки обычно используются для решения следующих задач: ? создания управляющих элементов и меню; ? создания звуковых эффектов; О обновления структур данных; П обновления кадров анимации. Этими задачами применение потоков не ограничивается, существуют и другие варианты их использования. Более подробно работа многопоточных приложений будет рассмотрена в главе 12, сейчас же я приведу пример программы с двумя потоками (основным и вспомогательным), в котором производится масштабирование трехмерного вектора с координатами в мас- массиве ai и коэффициентом масштабирования, равным 4. Кроме этого, вы- вычисляется длина отрезка вектора. Вначале рассмотрим вариант программы с использованием обычных операторов C++ .NET (листинг 11.1). ¦ Листинг 11,1. Применение многопото^ной технологии дня масштайировзния '¦ : и зычислс-иия длимы оскторз I париопт и а C++) ; // MHTHREAD_GRAPHICS.срр : Defines the entry point for the console // application. #include "stdafx.h" #include <windows.h> #include <math.h> int il; // Координаты вектора (х, у, z) int al[4] = {4, 7, -3}; void myFunc(LPVOID kl) { for(il = 0;il < 4; i int _tmain(int argc, _TCHAR* argv[]) { HANDLE mythread;
Глава 11. Оптимизация мультимедийных приложений с помощью ассемблера 383 DWORD mythread_id; double vec_len; printf("CHANGING THE LENGTH OF VECTOR a = (aO, al, a2) (DirectX <P Optimizing Tips)\n"); printf("\nBefore scaling vector a « (%d, %d, %d)\n", al[0], al[l], <P al[2]); vec_len - sqrt((double)(al[0]*al[0]+al[l]*al[l]+al[2]*al[2])); printf("\Length of al « %.2f\n", vec_len); printf("\n\n Starting thread...\n\n"); mythread = CreateThread(NULL,0, (PTHREAD_START_ROUTINE)myFunc, (LPVOID)D), 0, &mythread_id); while(true) { if(WaitForSingleObject(mythread, 0) — WAIT_OBJECT_0) { vec_len - sqrt((double)(al[0]*al[0]+al[l]*al[l]+al[2]*al[2])); break; // Здесь выполняются какие-либо полезные действия CloseHandle(mythread); printf("After scaling vector al = (%d, %d, %d)", al[0], al[l], al[2]); printf("\nLength of al = %.2f\n", vec_len); printf ("\n Thread terminated.. An") ; getchar(); return 0; В этой программе основной процесс использует вспомогательный поток mythread. В этом потоке проводится вычисление новых координат трехмер- трехмерного вектора. Основной поток ожидает завершения вычислений mythread для подсчета длины вектора с помощью onepaToipa vec_len = sqrt((double)(al[0]*al[0]+al[1]*al[l]+al[2]*al[2])) Окно работающего приложения показано на рис. 11.1.
384 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование Рис. 11.1. Окно приложения, демонстрирующего операции с векторами Программу можно улучшить, если оптимизировать некоторые участки кода, связанные с математическими вычислениями. Прежде всего, можно упро- упростить реализацию вычислений в функции потока, если использовать ас- ассемблер. Функция void myFunc(LPVOID kl) for(il = 0;il < 4; i может быть переписана на ассемблере и представлена следующим образом: void rayFunc(LPVOID*) asm { lea ESI, al lea EDI, cl raov ECX, 3 sub ESI, 4 next: add ESI, 4 fild DWORD PTR [ESI] fimul DWORD PTR cl fistp DWORD PTR [ESI] dec ECX jnz next
Глава 11. Оптимизация мультимедийных приложений с помощью ассемблера 385 Для оптимизации лучше всего использовать математический сопроцессор или одно из расширений (ММХ или SSE). Исходный текст модифициро- модифицированного приложения представлен в листинге 11.2. // OPTIMIZING_VECTOR_OPERATIONS.cpp : Defines the entry point for the // console application. #include "stdafx.h" #include <windows.h> #include <math.h> int il; int al[4] = {4, 7, -3}; // size of.vector = sqrt((al-aO)*(al-aO)+ ) const int cl = 4; void myFunc(LPVOID kl) _asiu { lea ESI, al lea EDI, cl mov ECX, 3 sub ESI, 4 next: add ESI, 4 fild DWORD PTR [ESI] fimul DWORD PTR cl fistp DWORD PTR [ESI] dec ECX jnz next } int _tmain(int argc, _TCHAR* argv[] { HANDLE mythread; DWORD mythread_id; double vec len;.
386 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование printf("MOD. VARIANT VECTOR OPERATIONS with a = (aO, al, a2) (DirectX # Tips)\n"); printf("\nBefore scaling vector a - (%d, %d, %d)\n", al[0], al[l], <P l[2]) vec_len = sqrt((double)(al[0]*al[O]+al[l]*al[l]+al[2]*al[2])); printf("\Length of al = %.2f\n", vec_len); printf("\n\n , Starting thread...\n\n"); mythread = CreateThread(NULL,O, (PTHREAD_START_ROUTINE)myFunc, (LPVOID)D), 0, &mythread_id); while(true) { if(WaitForSingleObject(mythread, 0) — WAIT_OBJECT_0) { vec_len = sqrt((double)(al[0]*al[0]+al[1]*al[l]+al[2]*al[2])) break; // Здесь выполняются какие-либо полезные действия CloseHandle(mythread); printf("After scaling vector al = (%d, %d, %d)", al[0], al[l], al[2]); printf("\nLength.of al = %.2f\n", vec_len); printf("\n Thread terminated...\n"); getchar(); return 0; Дальнейшее усовершенствование программного кода можно выполнить при помощи команд ассемблера MMX-расширения. Подобным образом можно оптимизировать программный код для операции масштабирования вектора. Исходный текст приложения показан в листинге 11.3. // OPTIMIZING_VECTOR_OPERATIONS.cpp : Defines the entry point for the // console application.
Глава 11. Оптимизация мультимедийных приложений с помощью ассемблера 387 #include "stdafx.h" #include <windows.h> #include <math.h> int il; int al[4) = {4, int cl[4] = {4,4 7, -3 ,4,4} void myFunc(LPVOID*) i _asm { mov lea sub next: add pxor movd packssdw pxor movd packssdw ECX, ESI, ESI, ESI, mmO, irauO, mmO, Irani, Irani, mml, , 0}; 3 al 4 4 inmO DWORD PTR [ESI] imO inml DWORD PTR cl iranl pmaddwd iranO, mml movd DWORD PTR [ESI], mmO dec ECX jnz next emms int _tmain(int argc, JTCHAR* argv[]) { HANDLE mythread; DWORD mythread_id; double vec len/
388 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование printf("MOD. VARIANT VECTOR OPERATIONS with a = (aO, al, a2) (DirectX # Tips)\n"); printf("\nBefore scaling vector a = (%d, %d, %d)\n", al[0], al[l], <P al[2]); vec_len = sqrt((double)(al[O]*al[O]+al[l]*al[l]+al[2]*al[2])); printf("\Length of al = %.2f\n", vec_len); printf("\n\n Starting thread...\n\n"); mythread = CreateThread(NULL,O, (PTHREAD_START_ROUTINE)myFunc/ (LPVOID)D), 0, &mythread_id); while(true) { if(WaitForSingleObject(mythread, 0) == WAIT_OBJECT_0) { vec_len = sqrt((double)(al[0]*al[0]+al[1]*al[l]+al[2]*al[2])); break; // Здесь выполняются какие-либо полезные действия CloseHandle(mythread); printf("After scaling vector al = (%d, %df %d)", al[0], al[l], al[2]); printf("\nLength of al = %.2f\n", vec_len); printf("\n Thread terminated...\n"); getchar(); return 0; В приведенных ранее примерах применяется оператор If(WaitForSingleObject(mythread, 0) == WAIT_OBJECT_0) В КОТОРОМ КЛЮЧевуЮ РОЛЬ Играет фуНКЦИЯ WIN API WaitForSingleObject. Функция ожидает установления сигнала потоком mythread, и если сигнал не установлен, то немедленно передает управление следующему оператору программы. Такая организация позволяет параллельно выполняться не- нескольким потокам без снижения производительности. Окно работающего приложения показано на рис. 11.2.
Глава 11. Оптимизация мультимедийных приложений с помощью ассемблера 389 Рис. 11.2. Окно приложения, демонстрирующего применение MMX-расширения для операций над векторами Для разработки мультимедийных приложений очень широко используются специально разработанные библиотеки функций DirectX. Сочетание ассемб- ассемблера и функций DirectX позволяет разрабатывать высокопроизводительные приложения. Интерфейс ассемблера с функциями DirectX хоть и имеет свои особенности, но очень похож на обычные вызовы функций.
Глава 12 Оптимизация многопоточных приложений с помощью ассемблера Поток в Win32 является основным элементом выполнения. Приложение (процесс) может содержать несколько независимых потоков, разделяющих его адресное пространство и другие ресурсы. Поток — это независимый элемент внутри процесса. Использование потоков позволяет упростить ра- работу программы и воспользоваться преимуществами параллельной обработ- обработки. Обычные программы выполняются как один поток, что замедляет работу приложений, требующих одновременной обработки данных (поиск файлов, сортировка нескольких массивов данных и т. п.). Следует отметить, что ор- организация потока также требует определенных ресурсов процессора, и по- поэтому использовать многопоточность имеет смысл, когда сам поток выпол- выполняется достаточно долго. Программирование многопоточных приложений достаточно полно описано в литературе, поэтому я не буду дополнительно останавливаться на принци- принципах их организации. Вместо этого я покажу, как можно применить встроен- встроенный ассемблер C++ .NET 2003 для улучшения производительности много- многопоточных приложений. Основное преимущество ассемблера — скорость обработки данных и небольшой размер программного кода — с успехом можно применить для оптимизации приложений с несколькими потоками. Сказанное лучше всего проиллюстрировать примером. Рассмотрим задачу одновременного поиска определенных символов в текстовом файле и под- подсчета их количества. Предположим, необходимо найти количество символов •г1, 't* и 'f в текстовом файле с именем testchar и вывести результат на экран дисплея. Можем предположить, что удобно реализовать выполне- выполнение этой задачи в виде трех независимых потоков. Один поток будет выпол- выполнять поиск символов f r • и подсчет их количества, а второй и третий будут делать то же самое для литер • г' и • f \ Исходный текст программы показан в листинге 12.1.
Глава 12. Оптимизация многопоточных приложений с помощью ассемблера 391 Листинг 12.1. TpoxnoroKosoe приложение дли выполнения подсчета чиелз I символов 1 // THREAD_EXAMPLE_FPU.срр : Defines the entry point for the console // application. ¦include <stdio.h> #include <windows.h> #include <process.h> FILE *fp; char buf[128]; int i, numread; int num_t,num_f, num_r; HANDLE tTimer = NULL; HANDLE rTimer = NULL; HANDLE fTimer = NULL; LARGE_INTEGER liDueTime; void char__t (void*) { _asm { mov EDX, 0 lea ESI, buf mov ECX, 70 mov AL, 't' next_char: cmp AL, BYTE PTR [ESI] jne inc_address inc EDX inc_address: inc ESI dec ECX jnz next_char lea EAX, num_t mov DWORD PTR [EAX], EDX
392 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование tTimer = CreateWaitableTimer(NULL, TRUE, "tTimer"); SetWaitableTimer(tTimer, sliDueTime, 0, NULL, NULL, 0); void char_f(void*) { _asm { mov EDX, 0 lea ESI, buf mov ECX, 70 mov AL, 'f next_char: cmp AL, BYTE PTR [ESI] jne inc_address inc EDX inc_address: inc ESI dec ECX jnz next_char lea EAX, num_f mov DWORD PTR [EAXI, EDX } fTimer = CreateWaitableTimer(NULL, TRUE, "fTimer"); SetWaitableTimer(fTimer, SliDueTime, 0, NULL, NULL, 0); void char_r(void*) { _asm { mov EDX, 0 lea ESI, buf mov ECX, 70 mov AL, 'r1 next_char: cmp AL, BYTE PTR [ESI] jne inc_address inc EDX inc_address: inc ESI
Глава 12. Оптимизация многопоточных приложений с помощью ассемблера 393 dec EGX jnz next_char - lea EAX, num_r mov DWORD PTR [EAX], EDX } rTimer = CreateWaitableTimer(NULL, TRUE, "rTimer")/ ¦ SetWaitableTimer(rTimer, SliDueTime, 0, NULL, NULL, 0); int _tmain(int argc, _TCHAR* argv[]) { printf("OPTIMIZING OF MULTITHREADING APPLICATION WITH ASM DEMO\n\n") if( (fp- = fopen( "d:\\testchar", "r" )) == NULL ) { printf( "The file 'testchar' was not opened\n" ); exit(l); }; numread = fread(buf, sizeof (char), 70, fp); fclose(fp); printf( " First 70 chars :\n\n"); printf("%.70s\n", buf); liDueTime.QuadPart=-10; _beginthread(char_f, 0, NULL); _beginthread(char_t, 0, NULL); _beginthread(char_r, 0, NULL); while (WaitForSingleObject(fTimer, INFINITE) != WAIT_OBJECT_0); CancelWaitableTimer(fTimer); while (WaitForSingleObject(tTimer, INFINITE) != WAIT_OBJECT_0); CancelWaitableTimer(tTimer); while (WaitForSingleObject(rTimer, INFINITE) != WAIT_OBJECT_0); CancelWaitableTimer(rTimer); printf ("\nNumber of ?f characters = %d\n", printf("\nNumber of 't' characters = %d\n", num_t);
394 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование printf("\nNumber of 'r* characters = %d\n", num_r); MessageBox(NULL, "Searching completed!", " FIND CHARS", MB_OK); return @); Каждый из трех потоков порождается функцией joeginthread. В качестве ее первого параметра используются однотипные функции cnar_f, char_t и charr, написанные почти целиком на ассемблере. Для синхронизации рабо- работы потоков используются объекты таймеров ожидания. Для чего это нужно? При запуске потоков в приложении может возникнуть ситуация, когда дан- данные не успевают обрабатываться потоком и приложение не сможет кор- корректно выполняться. Для того чтобы синхронизировать обработку данных приложением и отдельными потоками, необходимо каким-то образом сооб- сообщать приложению об окончании обработки отдельным потоком и готовно- готовности данных. Этот и другие вопросы синхронизации приложений, потоков и нитей отно- относятся к системному программированию и являются весьма сложными. Я не буду подробно рассматривать эти вопросы, остановлюсь лишь на одном из вариантов решения задачи синхронизации с помощью таймера ожидания, а точнее, на его практической реализации. Вначале создается объект таймера ожидания с помощью функции CreateWaitabieTimer. Функция возвращает дескриптор объекта таймера ожидания. Этот дескриптор может использоваться так называемыми функ- функциями ожидания для блокировки или завершения работы процессов и пото- потоков. Возможно, это упрощенное объяснение, но оно помогает понять суть дела. Функция ожидания не передает управления на другие участки про- программного кода до тех пор, пока не будет выполнено определенное условие. Таким условием чаще всего является установка сигнала объектом синхрони- синхронизации. Часто говорят, что объект находится в сигнальном состоянии. В на- нашем случае объектом синхронизации является таймер ожидания. Проследим теперь, как основной процесс взаимодействует с одним из своих потоков в нашем примере: 1. В потоке создается объект таймера ожидания с помощью функции CreateWaitabieTimer. Через определенный интервал времени (например, когда поток завершил обработку данных) объект таймера ожидания пере- переходит в сигнальное состояние. 2. Функция ожидания основного потока или процесса (в нашем случае waitForSingieobject) проверяет объект синхронизации (таймер ожида- ожидания). Если объект перешел в сигнальное состояние, функция ожидания возвращает значение waitobjecto. В случае получения этого значения основной процесс может считать вызываемый поток завершенным и
Глава 12. Оптимизация многопоточных приложений с помощью ассемблера 395 приступить к обработке полученных данных. Кроме того, желательно удалить и сам объект синхронизации, который нам уже не понадобится, с ПОМОЩЬЮ функции CancelWaitableTimer. На практике указанная последовательность выглядит так. Создается объект таймера ожидания в каждом из потоков с помощью функции CreatewaitabieTimer. Функция возвращает дескриптор объекта, который в дальнейшем используется для обращения к объекту таймера ожидания. По- После этого с помощью функции setwaitabieTimer таймер активизируется. Одним из параметров вызова функции является указатель на переменную, содержащую значение интервала времени, через который объект таймера будет установлен в сигнальное состояние. В нашем случае этот интервал, единицей измерения которого является 0.1 микросекунды, записывается в переменную liDueTime. Величина интервала выбрана произвольно и равна 10 миллисекундам. Фрагмент кода для любого из потоков приблизительно одинаков: jnz next fwait }; yTimer = CreateWaitableTimer(NULL, TRUE, "yTimer"); SetWaitableTimer(yTimer, SliDueTime, 0, NULL, NULL, 0); В нашей программе указан интервал времени, равный 10 миллисекундам, определяющий момент завершения функции поиска символа во вспомога- вспомогательном потоке. Хотя это значение выбрано произвольно, следует помнить, что при разработке подобных приложений установка такого параметра оп- определяется быстродействием потока. Может случиться так, что все необхо- необходимые операции не успеют завершиться до того, как будет установлен сиг- сигнал. Не случайно в таких задачах интенсивно используется язык ассемблера. Ассемблерный код, являясь очень быстрым, позволяет работать нескольким потокам с минимальным временем синхронизации! Я не буду анализировать алгоритм поиска символа, реализованный в ас- ассемблерном блоке — он довольно прост, и читатель сумеет без особого труда разобраться в нем. Когда функция ожидания waitForsingieObject вызывается основным пото- потоком или процессом, она проверяет условие установки сигнала. Если условие не выполнено, вызывающий поток или процесс переходит в состояние ожи- ожидания. Ресурсы центрального процессора в этом состоянии почти не ис- используются. Возобновление работы приложения продолжается, если условие
396 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование выполнено. Сразу же после выполнения функции ожидания объект таймера ОЖИДаНИЯ уНИЧТОЖаеТСЯ С ПОМОЩЬЮ ВЫЗОВа ФУНКЦИИ CancelWaitableTimer, принимающей в качестве параметра дескриптор объекта таймера. Вот фрагмент кода основной программы: while (WaitForSingleObject(tTimer, INFINITE) != WAIT_OBJECT_0); CancelWaitableTimer(tTimer) ; Эта программа требует включения библиотеки многопоточности. Например, если компиляция приложения с исходным файлом mythreads.cpp выполня- выполняется из командной строки, то это выглядит так: cl /MT /D "_Х8б_" mythreads.cpp Если компиляция выполняется в среде Visual C++ .NET 2003, то необходи- необходимо установить опцию /мт вручную. Вот последовательность шагов: 1. Выберите в меню Project опцию Properties. 2. Выберите страницу Configuration Properties / C/C++ / Code Generation / Runtime Library. 3. Установите опцию компилятора в /мт. Окно работающего приложения показано на рис. 12.1. Рис. 12.1. Окно приложения, демонстрирующего выполнение трех потоков Рассмотрим еще один пример работы приложения с несколькими потоками. Приложение вычисляет частное от деления квадратного корня из одной пе- переменной на квадратный корень из другой. Обе переменные находятся в
Глава 12. Оптимизация многопоточных приложений с помощью ассемблера 397 двух равных по размеру массивах вещественных чисел на одних и тех же позициях. Реализация вычислений происходит следующим образом: ? в одном потоке вычисляется квадратный корень переменной из первого массива (назовем его f 1); ? в другом потоке вычисляется квадратный корень переменной из второго массива (назовем его f 2); ? основная программа после завершения работы потоков использует полу- полученные данные для дальнейших вычислений (деления). Этот пример во многом похож на предыдущий, но он демонстрирует, как можно обрабатывать числовые величины вещественного типа. Поскольку интенсивность вычислений для операций с плавающей точкой намного вы- выше, чем для символов, то следует внимательно отнестись к выбору величи- величины времени синхронизации объектов таймеров ожидания с основным про- процессом. Исходный текст консольного приложения приведен в листинге 12.2. // THREAD_EXAMPLE_FPU.срр : Defines the entry point for the console // application. // SQRT(f1)/SQRT(f2); #include "stdafx.h" linclude <windows.h> #include <process.h> FILE *fp; float fl[7] = {34.13, 96.03, 234.1, 954.25, 54.103, 3.14, 8.33}; float f2[7] = {67.11, 23.12, 5.87, 76.32, 19.43, 67.11, 5.09}; float fdv[7]; HANDLE xTimer = NULL; HANDLE yTimer = NULL; LARGE_INTEGER liDueTime; void sqrt_x(void*) { _asm { lea ESI, fl
398 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование mov ECX, 7 finit fldz next: , fid DWORD PTR [ESI] fsqrt fstp DWORD PTR [ESI] add ESI, 4 dec ECX jnz next fwait >; xTimer = CreateWaitableTimer(NULL, TRUE, "xTimer"); SetWaitableTimer(xTimer, sliDueTime, 0, NULL, NULL, 0); void sqrt_y(void*) { _asm { lea EDI, f2 mov ECX, 7 finit fldz next: fid DWORD PTR [EDI] fsqrt fstp DWORD PTR [EDI] add EDI, 4 dec ECX jnz next fwait yTimer. = CreateWaitableTimer(NULL, TRUE, "yTimer"); SetWaitableTimer(yTimer, sliDueTime, 0, NULL, NULL, 0); int _tmain(int argc, _TCHAR* argv[])
Глава 12. Оптимизация многопоточных приложений с помощью ассемблера 399 printf("OPTIMIZING OF MULTITHREADING APPLICATION WITH ASM FPU \n\n"); printf("\nfl : "); for (int cnt = 0; cnt <7;cnt++) printf("ft.3f ", fl[cnt]); printf("\nf2 : "); for (int cnt = 0; cnt <7;cnt++) printf{"ft.3f ", f2[cnt]); liDueTime.QuadPart=-10; _beginthread(sqrt_x, 0, NULL); _beginthread(sqrt_y, 0, NULL); while (WaitForSingleObject(xTimer, INFINITE) != WAIT_OBJECT_0); CancelWaitableTimer(xTimer); while (WaitForSingleObject(yTimer, INFINITE) != WAIT_OBJECT_0); CancelWaitableTimer(yTimer); _asm { lea ESI, DWORD PTR fl // lea EDI, DWORD PTR f2 lea EDX, DWORD PTR fdv mov ECX, 7 finit fldz next: fid DWORD PTR [ESI] fid* DWORD PTR [EDI] fdiv fstp DWORD PTR [EDX] add ESI, 4 add EDI, 4 add EDX, 4 dec ECX jnz next fwait
400 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование printf(H\n\nSQRT(fl) : ") ; for (int cnt = 0; cnt <7;cnt++) printf("%.3f ", fl[cnt]); printf C\nSQRT(f 2) : ") ; for (int cnt *= 0; cnt <7;cnt++) printf("%.3f ", f2[cnt]); printf("\n\nSQRT{fl) / SQRT(f2) : "); for (int cnt = 0; cnt <7;cnt++) printf("%.3f ", fdv[cnt]); MessageBox(NULL, "Calculations completed!", " FIND SQRT", MBJDK) ; return 0; Вычисления программы выполняются основным процессом и двумя вспо- вспомогательными потоками. Потоки выполняют вычисления квадратного корня для элементов двух массивов, а основной процесс находит частное от деле- деления полученных величин. Потоки запускаются с помощью операторов _beginthread(sqrt_x, 0, NULL) _beginthread(sqrt_y, 0, NULL) В одном из потоков выполняется функция sqrtx, в другом — sqrty. Эти функции запускают объекты таймеров ожидания и выполняют установки СИГНаЛЬНОГО СОСТОЯНИЯ ДЛЯ фуНКЦИЙ WaitForSingleObject ОСНОВНОГО ПОТО- ка. Функции WaitForSingleObject ожидают завершения работы потоков и переводят таймеры ожидания в неактивное состояние: while (WaitForSingleObject(xTimer, INFINITE) != WAITjDBJECT_0) CancelWaitableTimer(xTimer) while (WaitForSingleObject(yTimer, INFINITE) != WAIT_OBJECT_0) CancelWaitableTimer(yTimer) Результаты работы вспомогательных потоков используются основным про- процессом для дальнейших вычислений в ассемблерном блоке. Вид окна работающего приложения показан на рис. 12.2. Работа приложений в многопоточном режиме — сложная тема и включает много вариантов реализации. Мы рассмотрели один из наиболее часто при- применяемых — с помощью таймеров ожидания. Применение ассемблера в та-
Глава 12. Оптимизация многопоточных приложений с помощью ассемблера 401 ких программах существенным образом увеличивает быстродействие и каче- качество программы в целом. Рис. 12.2. Окно приложения, демонстрирующего выполнение математических операций в двухпотоковом приложении Рассмотренные примеры простых программ демонстрируют технику приме- применения ассемблера и смогут помочь программистам при написании прило- приложений с интенсивными вычислениями, требующими параллельного выпол- выполнения операций.
Глава 13 Встроенный ассемблер C++ .NET и функции времени Windows Значительная часть приложений, работающих в операционных системах Windows, использует функции времени и таймеры. Необходимость в этом возникает всегда, когда дело касается операций в реальном масштабе време- времени, при работе с драйверами устройств, при написании мультимедийных приложений. Должен заметить, что операционные системы семейства Win- dQws не являются системами реального времени. Это означает, что добиться выполнения каких-либо операций, зависящих от точных временных интер- интервалов или связанных с ними, очень трудно. Программисты, сталкивающиеся с написанием приложений, работающих с временными интервалами высо- высокой точности, испытывают немалые трудности. Если дело касается опера- операций при относительно длительных интервалах времени (единицы, десятки секунд, минут и т. д.), то проблем обычно не возникает. Для приложений, оперирующих с десятками и единицами миллисекунд, все намного сложнее. Задержки при выполнении операций обработки аудио- или видеоданных, неточность отсчетов при работе с физическими устройствами приводят к потере и искажениям обрабатываемых данных, что делает приложения нера- неработоспособными . Все проблемы, связанные с временными зависимостями, можно свести к двум категориям: трудность выполнения того или иного алгоритма за определен- определенный интервал времени и невозможность установки самого интервала времени с приемлемой точностью, хотя сам алгоритм вполне работоспособен. В первом случае наилучшим вариантом является применение языка ассемб- ассемблера. Решение подобной задачи на "чистом" C++ возможно далеко не все- всегда. Хорошо спроектированный алгоритм, написанный на ассемблере, все- всегда выполняется быстрее, чем его аналог, закодированный на C++. Поэтому использование ассемблера часто позволяет втиснуть в приемлемые времен- временные рамки как критические участки кода, так и все приложение. Установка точного временного интервала — проблема более сложная. Полу- Получить от операционной системы интервал времени, меньший 50 микросе-
Глава 13. Встроенный ассемблер C++ .NET и функции времени Windows 403 кунд, как утверждают сами разработчики из фирмы Microsoft, проблема почти невыполнимая. Это обусловлено самой структурой операционной системы и временными зависимостями между ее подсистемами. Тем не менее, существует целый ряд решений, позволяющих получить до- достаточно короткие интервалы времени. Ассемблерный код и в этом случае оказывается весьма полезным. Для работы с точными временными интервалами очень часто используется функция WIN API GetTickCount. Функция возвращает число миллисекунд, прошедших с момента запуска Windows. Обобщая, можно сказать, что эта функция вычисляет время, прошедшее между ее последовательными вызова- вызовами. Применение GetTickCount оказывается очень полезным в таких случаях: ? когда требуется вычислить временной интервал между какими-либо дву- двумя событиями. Событиями могут быть начало/окончание выполнения какого-либо фрагмента программного кода или, например, начало/конец приема данных в системах обработки информации; ? когда требуется задать определенный временной интервал выполнения какой-либо задачи. Эффективность использования этой функции значительно возрастает с применением ассемблера при реализации подобных задач. Проиллюстриру- Проиллюстрируем это на примерах. Первый пример демонстрирует применение функции GetTickCount при оп- определении времени выполнения цикла for, в котором вычисляются значе- значения синуса последовательно возрастающих вещественных чисел. Цикл for реализован с помощью операторов C++. Исходный текст приложения при- приведен в листинге 13.1. // GetTickCount_j?lus_ASM.cpp : Defines the entry point for the console // application. #include "stdafx.h" #include <windows.h> #include <math.h> int _tmain(int argc, _TCHAR* argv[]) { float il = 0.0; float ires = 0.0; const float one = 1.0;
404 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование printf("Comparison ASM and C++ speed of execution (pure C++) \n"); DWORD dwStart = GetTickCount(); for (int cnt = 0;cnt < 1000000;cnt++) ires = sin(++il); DWORD dwlnterval = GetTickCount() - dwStart; printf("\nSinus = %.3f \n", ires); printf("Operation is completed through %d ms!\n", (int)dwlnterval); getchar (); return 0; Для вычисления интервала времени в миллисекундах используется простое соотношение, представляющее собой разность значений счетчика после вы- выполнения цикла for и в момент запуска программы: DWORD dwlnterval = GetTickCount() - dwStart Окно работающего приложения показано на рис. 13.1. Рис. 13.1. Окно приложения, показывающего время выполнения операторов цикла for Обратите внимание на время выполнения цикла, нам это понадобится для сравнения с результатами выполнения модифицированного варианта этой программы. Для этого перепишем цикл for на ассемблере. Исходный текст модифици- модифицированного приложения будет выглядеть так, как показано в листинге 13.2. // Time of calculating with ASM.cpp : Defines the entry point for the // console application.
Глава 13. Встроенный ассемблер C++ .NET и функции времени Windows 405 #include "stdafx.h" #include <windows.h> #include <math.h> int _tmain(int argc, _TCHAR* argv[]) { float il = 0.0; float ires = 0.0; printf("Comparison ASM and C++ speed of execution (ASM block) \n"); DWORD dwStart = GetTickCount(); _asm { mov ECX, 1000000 finit next : fldl fadd DWORD PTR il fstp DWORD PTR il fid DWORD PTR il fsin fstp DWORD PTR ires dec ECX jnz next fwait }; DWORD dwlnterval = GetTickCount0 - dwStart; printf("\nSinus = %.3f \n", ires); printf("Operation is completed through %d ms!\n", (int)dwlnterval); getchar(); return 0; Окно работающего приложения показано на рис. 13.2. Сравните время выполнения цикла с показателем предыдущего примера. Ассемблерный блок команд выполняется значительно быстрее. Оба резуль- результата получены на машине с процессором Pentium 4 2,4 ГГц. На компьютерах с другими процессорами и системными параметрами результаты будут дру-
406 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование гими, но закономерность сохранится: ассемблерный вариант цикла for вы- выполняется быстрее! Рис. 13.2. Окно приложения, демонстрирующего производительность цикла, составленного из ассемблерных команд Функция GetTickCount может использоваться для выполнения профамм- ного кода через определенные интервалы времени. Обычно такие профам- мы представляют собой профаммные генераторы, формирователи сигналов определенной формы и симуляторы работы электронных схем в профаммах моделирования. Следующий пример демонстрирует вычисление десяти значений синуса ве- вещественного числа с интервалом времени в 5 миллисекунд. Исходный текст профаммы показан в листинге 13.3. Листинг 13.:?' Бь!числение 1С-ти личсини синуса с mnc-роалсм | и 5 миллисекунд | // SINUS_X_5MS_ASM.cpp : Defines the entry point for the console // application. #include "stdafx.h" #include <windows.h> #include <raath.h> int __tmain(int argc, _TCHAR* argv[]) float fl[10] = {1.5, 4.1, 0.7, 2, 45.12, 21.7, 9.65, 11.3, 0.7, 77} float fsin[10]; DWORD dwStart; printf("Calculation SIN(x) each 5 miliiseconds with ASM\n\n");
Глава 13. Встроенный ассемблер C++ .NET и функции времени Windows 407 for (int cnt = 0;cnt < 10;cnt++) { dwStart » GetTickCount(); while ((GetTickCount() - dwStart)<=5); _asm \ mov lea lea finit next: fldl fadd fstp fid fsin fstp dec jz add add jmp ex: fwait printf("\nfl ECX, ESI, EDI, DWORD DWORD DWORD DWORD ECX ex ESI, EDI, next r 10 DWORD DWORD PTR PTR PTR PTR 4 4 "); PTR fl PTR fsin [ESI] [ESI] [ESI] [EDI] for (int cnt = 0;cnt < 10;cnt++) printf("%.2f ", fl[cnt]); printf (fI\n\nSIN(fl) : ") ; for (int cnt = 0;cnt < 10;cnt++) printf("%.2f ", fsin[cnt]); getchar(); return 0;
408 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование Хочу обратить внимание читателей на очень важный момент. При подсчете временных интервалов (и это касается не только функции GetTickCount) на их точность влияет и время выполнения операторов, осуществляющих под- подсчет. Для относительно длительных (по сравнению со временем выполнения команд) интервалов время выполнения операторов на два порядка ниже, поэтому такие погрешности не оказывают существенного влияния на точ- точность отсчета. Окно приложения показано на рис. 13.3. Рис. 13.3. Окно приложения, выполняющего вычисления синуса числа с интервалом в 5 миллисекунд Для работы с очень малыми интервалами времени (сотни микросекунд и меньше) следует применять специальные приемы и использовать функции WIN API QueryPerformanceCounter И QueryPerformanceFrequency. Рассмот- Рассмотрение способов применения этих функций выходит за рамки книги. Операции, требующие выполнения через определенные фиксированные промежутки времени, можно реализовать с помощью таймера. Таймер пред- представляет собой планируемое событие, которое операционная система фор- формирует через указанные промежутки времени. Точность отсчета системного таймера несколько ниже, чем вычисленная с помощью функции GetTickCount, но она вполне приемлема для многих приложений. Таймер можно использовать одним из двух способов: ? сформировать сообщение wmtimer и написать обработчик для этого со- события; П написать функцию обратного вызова для обработки события таймера. Создать либо изменить системный таймер можно с помощью функции setTimef. После использования таймера его необходимо удалить вызовом функции KiliTimer. Параметры вызова этих функций достаточно хорошо описаны в литературе, и я не буду детально их рассматривать, а сразу пе- перейду к примеру, в котором событие таймера обрабатывается с помощью
Глава 13. Встроенный ассемблер C++ .NET и функции времени Windows 409 обработчика wmtimer. В этом примере системный таймер Windows исполь- используется для выполнения математической операции извлечения квадратного корня из вещественного числа. Для генерации каркаса приложения воспользуемся мастером приложений C++ .NET 2003 и сгенерируем 32-разрядное процедурно-ориентированное приложение. Модифицируем полученный шаблон (изменения показаны жирным шрифтом) так, чтобы воспользоваться функцией системного тай- таймера. Непосредственное вычисление корня выполняется в ассемблерном блоке. Исходный текст приложения показан в листинге 13.4. // GL13_TEST_TIMER.cpp : Defines the entry point for the application. #include "stdafx.h" #include <stdio.h> #include "GL13_TEST_TIMER.h" #define MAX_LOADSTRING 100 , char buf[32]; float fl ¦ 0; float fres = 0; int nTimer; int written — 0; // Global Variables: HINSTANCE hlnst; // current instance TCHAR szTitle[MAX_LOADSTRING]; // The title bar text TCHAR szWindowClass[MAX_LOADSTRING]; // the main window class name // Forward declarations of functions included in this code module: ATOM MyRegisterClass(HINSTANCE hlnstance); BOOL InitInstance(HINSTANCE, int); LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); LRESULT CALLBACK About(HWND, UINT, WPARAM, LPARAM); int APIENTRY _tWinMain(HINSTANCE hlnstance, HINSTANCE hPrevInstance,
410 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование LPTSTR lpQndLine, int nCmdShow) // TODO: Place code here. MSG msg; HACCEL hAccelTable; // Initialize global strings LoadString(hinstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING); LoadString(hinstance, IDC_GL13_TEST_TIMER, szWindowClass, MAX_LOADSTRING); MyRegisterClass(hinstance); // Perform application initialization: if (!Initlnstance (hinstance, nCmdShow)) { return FALSE; } hAccelTable = LoadAccelerators(hinstance, (LPCTSTR)IDC_GL13_TEST_TIMER); // Main message loop: while (GetMessage(&msg, NULL, 0, 0)) { if (ITranslateAccelerator(msg.hwnd, hAccelTable, &msg)) { TranslateMessage(smsg); DispatchMessage(&msg); return (int) msg.wParam; // FUNCTION: MyRegisterClass() // PURPOSE: Registers the window class. // COMMENTS: // This function and its usage are only necessary if you want this code // to be compatible with Win32 systems prior to the 'RegisterClassEx'
Глава 13. Встроенный ассемблер C++ .NETи функции времени Windows 411 II function that was added to Windows 95. It is important to call this // function so that the application will get 'well formed1 small icons // associated with it. ATOM MyRegisterClass(HINSTANCE hlnstance) { WNDCLASSEX wcex; wcex.cbSize - sizeof(WNDCLASSEX); wcex.style = CS_HREDRAW | CS_VREDRAW; wcex.lpfnWndProc = (WNDPROC)WndProc; wcex.cbClsExtra = 0; wcex.cbWndExtra = 0; wcex.hlnstance = hlnstance; wcex.hlcon = Loadlcon(hlnstance, (LPCTSTR)IDI_GL13_TEST_TIMER); wcex.hCursor = LoadCursor(NULL, IDC_ARROW); wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW-2); wcex.lpszMenuName = (LPCTSTR)IDC_GL13_TEST_TIMER; wcex.lpszClassName = szWindowClass; wcex.hlconSm = Loadlcon(wcex.hlnstance, (LPCTSTR)IDI_SMALL); return RegisterClassEx(Swcex); // FUNCTION: Initlnstance(HANDLE, int) // PURPOSE: Saves instance handle and creates main window // COMMENTS: // In this function, we save the instance handle in a global variable and // create and display the main program window. BOOL Initlnstance(HINSTANCE hlnstance, int nCmdShow) { HWND hWnd; hlnst = hlnstance; // Store instance handle in our global variable hWnd - CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hlnstance, NULL);
412 Часть III. Встроенный ассемблер Visual C++ .NET 2003 и его использование if (!hWnd) { return FALSE; ShowWindow(hWnd, nCmdShow) UpdateWindow(hWnd); return TRUE; // FUNCTION: WndProc(HWND, unsigned, WORD, LONG) // PURPOSE: Processes messages for the main window. // WM_COMMAND - process the application menu // WM_PAINT - Paint the main window // WM_DESTROY - post a quit message and return LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { int wmld, wmEvent; PAINTSTRUCT ps; HDC hdc; RECT re; #define TIMER1 1 switch (message) { case WM_COiyiMAND: wmld = LOWORD(wParam); wmEvent = HIWORD(wParam) ; // Parse the menu selections: switch (wmld) { case IDM_ABOUT: DialogBox(hInst, (LPCTSTR)IDD_ABOUTBOX, hWnd, (DLGPROC)About) break;
Глава 13. Встроенный ассемблер C++ .NET и функции времени Windows 413 case IDM_EXIT: DestroyWindow(hWnd); break; , default: return DefWindowProc(hWnd, message, wParam, lParam); } break; case WM_PAINT: hdc = BeginPaint(hWnd, &ps); GetClientRect(hVftid, fire); // TODO: Add any drawing code here... TextOut(hdc, (re.right-rc.left)/4, (re.bottom-rc.top)/2, buf, written); EndPaint(hWnd, &ps); break; case WM_CREATE: nTimer = SetTimer(hWhd, TIMBR1, 5000, NULL); break; case WM_TIMER: switch(wParam) case TIMER1: { _asm { finit fldl fadd DWORD PTR ?1 fstp DWORD PTR fl fid DWORD PTR f1 fsqrt fstp DWORd PTR fres fwait }; written = sprintf(buf/1 Value = %.3f, SQRT - %.3f", fl, fres); hdc > GetDC(hWnd); GetClientRect(hWnd, &rc); InvalidateRect(hWnd, &rc, FALSE); ReleaseDC(hWnd, hdc);
414 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование break; } break; case WM_DESTROY: KillTimer (hWhd, nTimer); PostQuitMessage(O); break; default: return DefWindowProc(hWnd, message, wParam, lParam); } return 0; } //Message handler for about box. LRESULT CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { case WM_INITDIALOG: return TRUE; case WM_COMMAND: if (LOWORD(wParam) — IDOK I I LOWORD(wParam) — IDCANCEL) { EndDialog(hDlg, LOWORD(wParam)); return TRUE; } break; ) return FALSE; Анализ листинга начнем с объявления переменных нашей программы. В на- начале кода определены следующие переменные: ? char buf [32] — символьный буфер для хранения преобразованного ве- вещественного числа в строку, которая далее выводится на экран функцией TextOut; ? float fl — положительное вещественное число, из которого извлекается квадратный корень;
Глава 13. Встроенный ассемблер C++ .NET и функции времени Windows 415 ? float fres — результат вычисления квадратного корня; ? int nTimer — дескриптор нового таймера; ? int written — число символов, записанных в буфер в процессе преобра- преобразования числа в строку. Для создания системного таймера используется функция setTimer: nTimer = SetTimer(hWnd, TIMER1, 5000, NULL) Этот оператор создает системный таймер с идентификатором timeri, гене- генерирующий событие каждые 5 секунд. Дескриптор nTimer используется функцией KiiiTimer для уничтожения таймера по завершению работы при- приложения. Параметр null указывает на отсутствие функции обратного вызова для обработки события (в этом случае используется обработчик wmtimer). Окно работающего приложения показано на рис. 13.4. Рис. 13.4. Окно приложения, демонстрирующего вычисление квадратного корня в обработчике WM TIMER Хочу заметить, что программный код обработчика wmtimer должен быть по возможности как можно более компактным. Это ограничение тем больше, чем меньше интервал времени между срабатываниями таймера. В нашем примере этот интервал равен 5 секундам, что более чем достаточно как для выполнения быстрых математических вычислений, так и для обработки данных перед выводом на экран. Но если интервал времени между срабаты- срабатываниями таймера достаточно мал, например, равен 10 миллисекундам, то здесь начинают влиять факторы аппаратного быстродействия и качества программного кода. Для коротких интервалов времени не стоит писать код обработчика, в кото- котором бы присутствовали вывод на экран или запись обработанных данных в файл. Эти операции требуют достаточно длительных интервалов времени, сопоставимых с интервалом между двумя событиями таймера. Может слу- случиться так, что данные будут еще обрабатываться, когда наступит следующее
416 Часть ///. Встроенный ассемблер Visual C++ .NET2003 и его использование событие. В этом случае приложение будет вести себя непредсказуемым об- образом, и данные будут потеряны. Необходимо учитывать еще один фактор. Сообщение wmtimer формируется системой только в том случае, если в очереди отсутствуют необработанные сообщения. Можно сказать, что все остальные сообщения (за исключением wmpaint) обладают более высоким приоритетом по сравнению с сообще- сообщениями таймера. Можно оптимизировать наше приложение, если постараться перенести часть или все операции вывода данных на экран из обработчика wmtimer, например, в обработчик wmpaint. После анализа исходного текста прило- приложения оказывается, что оператор written = sprintf(buf," Value = %.3f, SQRT = %.3f", fl, fres) может быть исключен из обработчика wmtimer и помещен в wmpaint: case WMJPAINT: hdc = BeginPaint(hWnd, &ps); GetClientRect(hWnd, &rc) ; // TODO: Add any drawing code here... written = sprintf(buf," Value = %.3f, SQRT = %.3f", fl, fres); TextOut(hdc, (rc.right-rc.left)/4, (re.bottom-re.top)/2 , buf, written); EndPaint(hWnd, &ps); break; Наш следующий пример похож на предыдущий, но есть одно существенное отличие — в нем используется функция обратного вызова для обработки события таймера. Исходный текст приложения показан в листинге 13.5. П и с т и н г 13.5. И с п о л ь з о п а м и е фу н к ц и и о Ь р а т н его е ь i з о в а цл я о 6 s ;> «б о т к и события тй // MyTimer_with_ASM.cpp : Defines the entry point for the application. #include "stdafx.h" #include "MyTimer_with_ASM.h" #include <stdio.h> #define MAX LOADSTRING 100
Глава 13. Встроенный ассемблер C++ .NET и функции времени Windows 417 char buf[64]; float fl - 0; float free = 0; int nTimer; int written = 0; // Global Variables: HINSTANCE hlnst; // current instance TCHAR szTitle[MAX_LOADSTRING]; // The title bar text TCHAR szWindowClass[MAX_LOADSTRING]; // the main window class name // Forward declarations of functions included in this code module: ATOM MyRegisterClass(HINSTANCE hinstance); BOOL Initlnstance(HINSTANCE, int); LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); LRESULT CALLBACK About(HWND, UINT, WPARAM, LPARAM); int APIENTRY _tWinMain(HINSTANCE hinstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow) { // TODO: Place code here. MSG msg; HACCEL hAccelTable; // Initialize global strings LoadString(hinstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING); LoadString(hinstance, IDC_MYTIMER_WITH_ASM, szWindowClass, MAX_LOADSTRING); MyRegisterClass(hinstance); // Perform application initialization: if (!Initlnstance (hinstance, nCmdShow)) { return FALSE; } hAccelTable = LoadAccelerators(hinstance, (LPCTSTR)IDC_MYTIMER_WITH_ASM);
418 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование П Main message loop: while (GetMessage(&msg, NULL, 0, 0)) { if ("TranslateAccelerator(msg.hwnd, hAccelTable, &msg)) { TranslateMessage(&msg); DispatchMessage(&msg); return (int) msg.wParam; } // FUNCTION: MyRegisterClass() // PURPOSE: Registers the window class. // COMMENTS: // This function and its usage are only necessary if you want this code // to be compatible with Win32 systems prior to the 'RegisterClassEx' // function that was added to Windows 95. It is important to call this // function so that the application will get 'well formed1 small icons // associated with it. ATOM MyRegisterClass(HINSTANCE hlnstance) { WNDCLASSEX wcex; wcex.cbSize = sizeof(WNDCLASSEX); wcex.style = CS_HREDRAW | CS_VREDRAW; wcex.lpfnWndProc = (WNDPROC)WndProc; wcex.cbClsExtra = 0; wcex.cbWndExtra = 0; wcex.hlnstance = hlnstance; wcex.hlcon = Loadlcon(hlnstance, (LPCTSTR)IDI_MYTIMER_WITH_ASM) wcex.hCursor = LoadCursor(NULL, IDC_ARROW); wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW-1); wcex.lpszMenuName = (LPCTSTR)IDC_MYTIMER_WITH_ASM; wcex.lpszClassName - szWindowClass; wcex.hlconSm = Loadlcon(wcex.hlnstance, (LPCTSTR)IDI_SMALL);
Глава 13. Встроенный ассемблер C++ .NET и функции времени Windows 419 return RegisterClassEx(&wcex); } // FUNCTION: Initlnstance(HANDLE, int) // PURPOSE: Saves instance handle and creates main window // COMMENTS: // In this function, we save the instance handle in a global variable and // create and display the main program window. BOOL Initlnstance(HINSTANCE hlnstance, int nCmdShow) { HWND hWnd; hlnst = hlnstance; // Store instance handle in our global variable hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW, CWJJSEDEFAULT, 0, CWJJSEDEFAULT, 0, NULL, NULL, hlnstance, NULL); if UhWnd) { return FALSE; ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd); return TRUE; // FUNCTION: WndProc(HWND, unsigned, WORD, LONG) // PURPOSE: Processes messages for the main window. // WM_COMMAND - process the application menu // WM_PAINT - Paint the main window // WM_DESTROY - post a quit message and return VOID CALLBACK MyTimerProc(HWND hWnd, // handle to window UINT message, // WMJTIMER message UINT idTimer, // timer identifier DWORD dwTime) // current system time
420 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование { HDC hdc; RECT re; _asm { finit fldl fadd DWORD PTR fl fstp DWORD PTR fl fid DWORD PTR fl fsqrt fstp DWORd PTR fres fwait hdc = GetDC(hWnd); GetClientRect(hWnd, &rc); InvalidateRect(hWnd, &rc, FALSE); ReleaseDC(hWnd, hdc); LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM 1Param) { int wmld, wmEvent; PAINTSTRUCT ps; HDC hdc; RECT re; «define TIMER1 1 switch (message) { case WM_COMMAND: wmld = LOWORD(wParam); wmEvent = HIWORD(wParam); // Parse the menu selections: switch (wmld)
Глава 13. Встроенный ассемблер C++ .NETи функции времени Windows 421 { case IDM_ABOUT: DialogBox(hInst, (LPCTSTR)IDD_ABOUTBOX, hWnd, (DLGPROC)About); break; case IDM_EXIT: DestroyWindow(hWnd); break; default: return DefWindowProc(hWnd, message, wParam, lParam) ; } break; case WM_PAINT: hdc = BeginPaint(hWnd, &ps); GetClientRectfliWhd, fire); // TODO: Add any drawing code here... written » sprintf(buf," Value fl = %.3f, SQRT(fl) = %.3f", fl, fres); TextOut(hdc, (re.right-rc.left)/4, (re.bottom-re.top)/2 , buf, written); EndPaint(hWnd, &ps); break; case WM_CREATE: nTimer = SetTimer(hWhd, TIMER1, 5000, (TIMERPROC) MyTimerProc) ; break; case WM_DESTROY: KillTimer(hWnd, nTimer); PostQuitMessage@); break; default: return DefWindowProc(hWnd, message, wParam, lParam); } return 0; // Messa'1"? гапо • for about box. LRESULT L4TI" .-л About (HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
422 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование { switch (message) { case WM_INITDIALOG: return TRUE; case WM_COMMAND: if (LCWORD(wParam) — IDOK |I LOWORD(wParam) == IDCANCEL) { EndDialog{hDlg, LOWORD(wParam)); return TRUE; } break; .} return FALSE; В этом листинге можно заметить существенные изменения. В функции ус- установки системного таймера nTimer = SetTimer(hWnd, TIMER1, 5000, (TIMERPROC) MyTimerProc) определена функция обратного вызова MyTimerProc. Она выполняет те же действия, что и программный код обработчика wmtimer из предыдущего листинга. Сам обработчик wmtimer из текста программы исключен, по- поскольку в нем больше нет необходимости. Обработка сообщения wmtimer выполняется через функцию обратного вызова MyTimerProc, а не через оче- очередь сообщений приложения. Это позволяет ускорить обработку события таймера. Окно приложения показано на рис. 13.5. Рис. 13.5. Окно приложения, выполняющего обработку события таймера через функцию обратного вызова
Глава 13. Встроенный ассемблер C++ .NETи функции времени Windows 423 Для синхронизированных во времени операций также очень часто исполь- используют таймер ожидания. Таймер ожидания представляет собой объект опера- операционной системы Windows, устанавливающий через определенные моменты времени сигнал, свидетельствующий об истечении этих интервалов времени. Такой сигнал может применяться процессами или потоками для управления операциями в реальном времени. Таймер ожидания используется в работе большинства системных процессов Windows и очень важен для нормального функционирования пользовательских приложений, работа которых связана с синхронизацией во времени. Этот программный объект обеспечивает вы- высокую точность отсчета временных интервалов и допускает многообразие вариантов использования для приложений пользователя. Применение таймера ожидания требует от программиста знания его .работы, а это довольно сложный объект и нуждается в подробном описании. Первое, что нужно сделать при использовании объекта таймера ожидания — СОЗДать его С ПОМОЩЬЮ ВЫЗОВа функции WIN API CreateWaitableTimer. Созданный объект таймера возвращает дескриптор, используемый для даль- дальнейших операций с таймером. Синтаксис функции представлен далее: HANDLE CreateWaitableTimer(LPSECURITY_ATTRIBUTES lpTimerAttributes, BOOL bManualReset, LPCTSTR lpTimerName) где ? lptimerAttributes — указатель на структуру, определяющую дескриптор безопасности. Он определяет, может ли вновь порожденный процесс на- наследовать атрибуты безопасности родительского процесса. По умолчанию значению дескриптора присваивается null; ? bManualReset — определяет тип таймера. Если флаг bManualReset уста- установлен в true, таймер управляется вручную. Это означает, что программа должна специальным образом перезапускать таймер. Если bManualReset установлен в false, таймер является автоматически перезапускаемым; ? lpTimerName — указатель на строку с завершающим нулем, определяю- определяющий имя объекта таймера. Размер имени ограничен значением махратн, имя чувствительно к регистру. Далее необходимо установить необходимые параметры объекта и запустить его. ЭТО ВЫПОЛНЯеТСЯ при ПОМОЩИ ФУНКЦИИ SetWaitableTimer. Синтаксис этой функции: BOOL SetWaitableTimer( HANDLE hTimer, Const LARGE_INTEGER *pDueTime, LONG IPeriod, PTIMERAPCROUTINE pfnCompletionRoutine, LPVOID lpArgToCompletionRoutine, BOOL fResume)
424 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование где ? hTimer — дескриптор объекта таймера ожидания; П pDueTime — определяет момент времени, когда таймер устанавливает сигнал о срабатывании. Этот момент времени представлен в интервалах, равных 100 наносекундам @.1 микросекунды). Например, установка liDueTime.QuadPart = -100000000 определяет время срабатывания через 10 секунд после запуска таймера. Положительное значение этой величи- величины определяет абсолютный интервал времени, т. е. времени, прошедшего с момента запуска операционной системы. Хотя точность срабатывания таймера и является высокой, но она все равно зависит от аппаратных средств компьютера; ? 1 Period определяет период срабатывания таймера в миллисекундах. Если он равен 0, то таймер срабатывает один раз, если больше нуля, то таймер срабатывает периодически; ? параметры pfnCompletionRoutine И lpArgToCompletionRoutine ОПреде- ляют пользовательскую функцию, которая будет вызываться при установ- установке сигнала таймера. Пример использования этих параметров показан в листинге 13.7; П fResume определяет режим энергосбережения и зависит от платформы, на которой выполняется приложение. Полагаем его равным 0. Еще одна функция WIN API переводит объект таймера в неактивное со- состояние. Это функция canceiwaitabieTimer, принимающая в качестве па- параметра дескриптор таймера. Каким образом процесс или поток узнает о том, что таймер сработал и можно выполнять определенные действия? Для этого служат так называемые функ- функции ожидания. Одной из наиболее важных и часто используемых является ФУНКЦИЯ WIN API WaitForSingleObject. Функция WaitForSingleObject BO3- вращает управление приложению в одном из двух случаев: О объект синхронизации (например, таймер ожидания) перешел в сигналь- сигнальное состояние; ? закончилось время ожидания сигнала от объекта (выход по тайм-ауту). Синтаксис этой функции следующий: DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds) где ? hHandle — дескриптор объекта синхронизации; ? dwMilliseconds — величина тайм-аута в миллисекундах. Функция немед- немедленно возвращает управление программе, если интервал времени dwMilliseconds закончился, даже если объект синхронизации не устано-
Глава 13. Встроенный ассемблер C++ .NET и функции времени Windows 425 вил сигнал. Если значение dwMiiiiseconds установлено в 0, функция проверяет состояние объекта синхронизации и немедленно возвращает управление. Если dwMiiiiseconds имеет значение infinite, время ожи- ожидания функции не ограничено. При разработке приложения, использующего таймер ожидания, очень важно определить критические (по времени выполнения) участки программного кода и попытаться их оптимизировать по быстродействию. Чем меньший интервал времени разрешается для выполнения того или иного вычисли- вычислительного алгоритма, тем более жесткие требования по производительности накладываются на этот участок кода. Реализация высокопроизводительных алгоритмов или их отдельных частей на ассемблере при жестких временных ограничениях — часто оказывается единственным способом оптимизации, а высокоуровневые программные методы оптимизации нередко вообще не работают в такой ситуации. Рассмотрим пример, в котором проиллюстрируем вышеизложенное. Наше консольное приложение каждые 0.5 секунды будет вычислять максимальный элемент массива из 7-ми целых чисел. Для заполнения массива произвольны- произвольными целочисленными значениями используется генератор случайных чисел, реализованный с помощью библиотечной функции rand. Для генерации вре- временных интервалов длительностью в 0.5 секунды используется таймер ожида- ожидания. Время перехода таймера в синхронный режим выбрано равным 10 секун- секундам и служит только для иллюстрации возможностей объекта таймера. Исходный текст программы представлен в листинге 13.6. Лис т и и г i 3 6. П о -л -a е >¦¦¦¦ е н и е т а и м е р а с ж и д а н •*.'• * л л > > к > ¦>; ч и г;; е н и,; к-, о м ¦ -,-л >\'. '¦;; с интервлпом и 0.5 сок // WAITABLE_TIMER_USE_WITH_ASM.cpp : Defines the entry point for the // console.application. #include "stdafx.h" #include <windows.h> #include <stdio.h> #include <stdlib.h> #include <time.h> int _tmain(int argc, _TCHAR* argv[]) HANDLE hTimer = NULL; LARGE_INTEGER liDueTime; liDueTime.QuadPart=-100000000;
426 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование int int imax; // Seed the random-number generator with current time so that // the numbers will be different every time we run. srand( (unsigned)time( NULL ) ); // Create a waitable timer. hTimer = CreateWaitableTimer(NULL, FALSE, "WaitableTimer"); if (!hTimer) { printf("CreateWaitableTimer failed (%d)\n", GetLastError()); exit(l); } printf("USING WAITABLE TIMER IN CALCULATION MAXIMUM INT\n\n"); printf("Waiting for 10 seconds...\n"); // Set a timer to wait for 10 seconds. if (!SetWaitableTimer(hTimer, sliDueTime, 500, NULL, NULL, 0)) { printf("SetWaitableTimer failed (%d)\n", GetLastError()); exitA); } // Wait for the timer. while (true) { while (WaitForSingleObject(hTimer, INFINITE) != WAIT_OBJECT_0) printf("\n\nil: "); for(int cnt = 0; cnt < 10;cnt++ ) { il[cnt] = rand() ; printf("%6d ", ilfcnt]);
Глава 13. Встроенный ассемблер C++ .NET и функции времени Windows 427 _asm { lea ESI, il mov ECX, 10 mov EAX, DWORD PTR [ESI] sub ESI, 4 next: add ESI, 4 cmp EAX, [ESI+4] j1 change dec ECX jnz next jmp ex change: xchg EAX, [ESI+4] dec ECX jnz next ex: mov imax, EAX }; printf("\nimax: %6d\n", imax); }; CancelWaitableTimer(hTimer) ; return 0; Проведем анализ исходного текста. Генерация последовательности случай ных чисел выполняется с помощью операторов srand( (unsigned)time( NULL ) ) И for(int cnt = 0; cht < 10;cnt++ ) { il[cnt] = rand(); printf("%6d ", iltcnt]); Заполнение массива ii случайными числами и вывод на экран полученной последовательности выполняется в цикле for.
428 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование Таймер ожидания устанавливается функцией hTimer = CreateWaitableTimer(NULL, FALSE, "WaitableTimer"); Обратите внимание на второй параметр функции. Он установлен в false, a это означает, что таймер будет перезапускаться автоматически. Активизация объекта таймера ожидания выполняется функцией SetWaitableTimer(hTimer, &liDueTime, 500, NULL, NULL, 0) Эта функция использует дескриптор hTimer, полученный при вызове функ- функции CreateWaitableTimer. Переменная liDueTime определяет время активи- активизации таймера, равное 10 секундам: liDueTime.QuadPart=-100000000; Обработка данных проводится в цикле while, содержащем функцию ожида- ожидания WaitForSingleObject С параметром INFINITE! while (true) { while (WaitForSingleObject(hTimer, INFINITE) != WAIT_OBJECT_0); Непосредственное вычисление максимума выполняется в блоке ассемб- ассемблерных команд. Вначале инициализируем регистры esi и есх адресом массива ни значением размерности соответственно. Кроме этого, полага- полагаем максимум равным первому элементу массива и сохраняем это значение в регистре еах: lea ESI, il mov ECX, 10 mov ЕАХ, DWORD PTR [ESI] Поиск максимума выполняется по следующему алгоритму: выполняется сравнение содержимого еах и следующего элемента массива с адресом [esi+4]. Если значение в регистре еах больше или равно значения элемента с этим адресом, итерация повторяется. Если содержимое еах меньше, чем значение в [esi+4], выполняется обмен значений регистра еах и ячейки памяти, после чего начинается новая итерация. Фрагмент кода демонстри- демонстрирует это: next: add ESI, 4 cmp EAX, [ESI+4] j1 change dec ECX
Глава 13. Встроенный ассемблер C++ .NET и функции времени Windows 429 jmp change: xchg dec jnz next ex EAX, [ESI+4] ECX next После завершения всех итераций значение максимума сохраняется в пере- переменной imax С ПОМОЩЬЮ команды mov imax, EAX. По окончании работы приложения таймер ожидания необходимо перевести В неактивное состояние функцией CancelWaitableTimer (hTimer-) , где hTimer — дескриптор объекта таймера ожидания. Хочу сделать важное замечание. Интервал обработки данных установлен равным 0.5 секунды только для демонстрации. Ассемблерный код выполня- выполняется настолько быстро, что этот интервал может быть уменьшен в сотни и тысячи раз! Рассмотрим еще один момент. Если, например, необходимо выполнить более сложные вычисления для больших массивов чисел с плавающей точкой за относительно небольшой отрезок времени, то нагрузка на процессор возрас- возрастет. Корректное выполнение программного кода, разработанного с помощью только операторов языка высокого уровня при тех же требованиях к интерва- интервалу времени, скорее всего станет проблемным. В то же время ассемблерный код будет выполняться быстро, и подобные ограничения не возникнут. Окно работающего приложения показано на рис. 13.6. Рис. 13.6. Окно приложения, демонстрирующего использование таймера ожидания для вычисления максимума в массиве целых чисел
430 Часть III. Встроенный ассемблер Visual C++ .NET 2003 и его использование Можно упростить программу предыдущего примера (см. листинг 13.6), если функцию ожидания waitForSingieobject исключить и вместо нее приме- применить функцию тайм-аута, которую можно указать в четвертом параметре функции setwaitabieTimer. Пятым параметром можно указать адрес струк- структуры или переменной, используемой этой функцией (этот параметр являет- является опциональным). Функцию тайм-аута назовем findmax. В этой функции будет выполняться поиск максимума с помощью блока ассемблерных команд. Параметром этой функции будет целочисленная переменная (назо- (назовем ее cnt), представляющая собой счетчик итераций. Для демонстрации этого варианта программы вычисления максимума будем считать, что выполняется 5 вычислений (cnt=5) с интервалом 0.5 секунды (как и в предыдущем примере). Исходный текст программы показан в листинге 13.7. // WTIMER_CALC_MAX_VAR_2.срр : Defines the entry point for the console // application. #include "stdafx.h" #include <windows.h> #include <stdio.h> #include <stdlib.h> #include <time.h> HANDLE hTimer = NULL; LARGEINTEGER liDueTime; int int imax; int Cnt; void findmax(void) { int cnt; if (Cnt!= 0) { printf("\n\nil: "); for(cnt = 0; cnt < 10;cnt++ )
Глава 13. Встроенный ассемблер C++ .NET и функции времени Windows 431 { il[cnt] - rand(); printf("%6d ", _asm { lea ESI, il mov ECX, 10 mov EAX, DWORD PTR [ESI] sub ESI, 4 next: add ESI, 4 cmp EAX, [ESI+4] j1 change dec ECX jnz next jmp ex change: xchg EAX, [ESI+4] dec ECX jnz next ex: mov imax, EAX }; printf("\nimax: %6d\n", imax) ; Cnt—; } else { CancelWaitableTimer(hTimer); CloseHandle(hTimer); int _tmain(int argc, _TCHAR* argv[]) // Seed the random-number generator with current time so that // the numbers will be different every time we run.
432 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование srand( (unsigned)time( NULL ) ); Cnt = 5; // Display 5 numbers. liDueTime.QuadPart=-100000000; // Create a waitable timer. hTimer = CreateWaitableTimer(NULL, FALSE, "WaitableTimer"); if (!hTimer) { printf("CreateWaitableTimer failed (%d)\n", GetLastError()); exitA); } printf("WAITABLE TIMER IN CALCULATION MAXIMUM INT (VAR 2)\n\n"); printf("Waiting for 10 seconds...\n"); // Set a timer to wait for 10 seconds. if (!SetWaitableTimer(hTimer, SliDueTime, 500, (PTIMERAPCROUTINE)findmax, &Cnt, TRUE)) { printf("SetWaitableTimer failed (%d)\n", GetLastError()); exit(l) ; } while (Cnt != 0) SleepEx(INFINITE, TRUE); return 0; Проведем короткий анализ листинга. Функция findmax указана в качестве ОДНОГО ИЗ параметров ПрИ ВЫЗОВе фуНКЦИИ SetWaitableTimer: SetWaitableTimer(hTimer, SliDueTime, 500, (PTIMERAPCROUTINE)findmax, &Cnt, TRUE) Еще один параметр указывает на адрес счетчика cnt, определяющего коли- количество итераций при вычислении максимума. Сама функция findmax вы- выполняет вычисления в блоке ассемблерных команд. Кроме того, выполняет-
Глава 13. Встроенный ассемблер C++ .NET и функции времени Windows 433 ся декремент счетчика итераций cnt, и, в случае равенства его 0, таймер ожидания деактивируется. В исходном тексте программы появилась функция WIN API sieepEx(infinite, true). Эта функция переводит в состояние ожидания текущий процесс для того, чтобы функция тайм-аута findmax могла выпол- выполниться. После такого объяснения легко понять, как работает цикл while (Cnt != 0) SieepEx(INFINITE, TRUE) В остальном исходный текст программы понятен и не нуждается в объясне- объяснениях. Окно работающего приложения показано на рис. 13.7. Рис. 13.7. Окно приложения, демонстрирующего работу модифицированного варианта программы вычисления максимума с использованием функции тайм-аута Рассмотренные в этой главе принципы применения встроенного ассемблера для выполнения зависящих от времени вычислений не исчерпываются при- приведенными примерами. Эти примеры, надеюсь, помогут читателю освоить и более сложные варианты применения ассемблера для работы в реальном времени.
Глава 14 Ассемблер в задачах системного программирования Windows В этой главе рассматриваются некоторые вопросы оптимизации задач сис- системного программирования в операционных системах Windows. Системное программирование — весьма сложная область, требующая от разработчика хороших знаний функционирования операционной системы. Улучшение показателей работающих программ возможно в том случае, если четко пред- представлять себе возможности операционной системы по оптимизации про- программного обеспечения. В системном программировании решаются задачи по управлению файловой системой, памятью и процессами, межпроцессными коммуникациями, сете- сетевыми соединениями и другие с использованием интерфейса прикладного программного обеспечения, а именно с помощью функций WIN API 32- разрядных операционных систем Windows. Материал главы демонстрирует некоторые аспекты оптимизации подобных задач с помощью языка низкого уровня — ассемблера. Традиционные преимущества ассемблера — компакт- компактность и скорость выполнения программного кода — могут быть полезны и при решении задач системного программирования. Некоторые вопросы оптимизации задач с помощью ассемблера (много- (многопотоковое программирование, таймеры и системные службы) рассматрива- рассматриваются в других главах этой книги. Здесь будут представлены варианты опти- оптимизации для файловых операций и операций управления памятью. Эти опе- операции оказывают решающее влияние на производительность большинства приложений, поэтому повышению эффективности их выполнения и посвя- посвящается материал этой главы. Начнем с файловых операций. Копирование, перемещение, поиск и удале- удаление файлов и каталогов можно выполнять как с использованием библиотеч- библиотечных функций C++ .NET, так и с помощью функций интерфейса WIN API операционной системы. Производительность выполнения во многом зави-
Глава 14. Ассемблер в задачах системного программирования Windows 435 сит от алгоритма реализации файловых операций. Рассмотрим операцию копирования файлов. На производительность этой операции файлов существенное влияние оказы- оказывает размер буфера памяти для считывания/записи, способ организации хра- хранимых данных в памяти, количество пересылаемых или копируемых байт. Очень часто при копировании одного файла в другой требуется преобразо- преобразование данных. Программная реализация такого преобразования оказывает существенное влияние на производительность программы. Встроенный ас- ассемблер позволяет выполнить эффективное преобразование данных с ми- минимальной потерей производительности. Кроме того, ассемблер позволяет написать специфичные алгоритмы обработки и преобразования, которые в C++ реализовать сложно, используя" только библиотечные функции. • Рассмотрим следующий пример. Пусть в исходном текстовом файле необхо- необходимо заменить символы пробела символами плюс и сохранить копию файла под другим именем. Предположим, что в качестве файла-источника исполь- используется текстовый файл с именем readf iie, а в качестве файла-приемника — writefiie. Замену символов выполним с помощью блока ассемблерных команд. Исходный текст консольного приложения приведен в листинге 14.1. : Листинг 14.1. Копирование файлов с заменой символов I // COPY_F_C_ASM.срр : Defines the entry point for the console // application. #include "stdafx.h" #include <stdio.h> int _tmain(int argc, _TCHAR* argv[]) { FILE *fin, *fout; char buf[256]; int bRead, bWritten; if( (fin = fopen( "d:\\readfile", "r" )) == NULL ) { printf( "The file 'readf was not opened\n" ); exit(l); // Open for* write
436 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование if( (fout = fopen( "d:\\writefile", "w+" )) == NULL ) { printf( "The file 'writefile' was not opened\n" ); exit(l); } while {(bRead = fread(buf, sizeof(char), sizeof(buf), fin)) > 0) { _asm { mov ECX, bRead lea ESI, buf mov AL, ' • next_ch: cmp BYTE PTR [ESI], AL je repl inc ESI dec ECX j nz next_ch jmp ex repl: mov [ESI], '+' inc ESI dec ECX j nz next_ch ex: bWritten = fwrite(buf, sizeof(char), bRead, fout); fclose(fin); fclose.(fout); return 0; Ассемблерный блок выполняет преобразование очень просто. В регистр esi загружается адрес буфера памяти, где находятся считанные данные. Регистр есх содержит количество байт, которое необходимо обработать. Заменяемый символ, т. е. пробел, находится в регистре al. В каждой итерации выполня- выполняется сравнение символов в памяти и регистре al. Если символы равны, то
Глава 14. Ассемблер в задачах системного программирования Windows 437 на место пробела в буфере памяти записывается символ плюс, и выполняет- выполняется переход к следующей итерации. Адрес элемента в буфере памяти инкре- ментируется, а счетчик символов — декрементируется: cmp BYTE PTR [ESI], AL je repl inc ESI dec ECX jnz next_ch В случае неравенства символов выполняется переход к следующей итерации одновременно с инкрементом адреса в регистре esi. Содержимое буфера памяти после преобразования сохраняется в новом файле с дескриптором f out: bWritten = fwrite(buf, sizeof(char), bRead, fout) С помощью ассемблера можно создавать весьма замысловатые и сложные алгоритмы обработки данных из файлов, и возможности здесь неограничен- неограниченные. Ни одно приложение не обходится без манипуляций с памятью. Язык C++ содержит функцию maiioc и оператор new. В большинстве случаев програм- программисты обходятся этими средствами. Однако некоторые задачи требуют более гибкого контроля над использованием памяти. В этом случае очень удобной оказывается функция прикладного интерфейса WIN API virtuaiAiioc. Эта функция очень широко применяется и по сравнению с библиотечной функцией maiioc обладает целым рядом серьезных преимуществ. В отличие от maiioc, функция virtuaiAiioc позволяет выделить участок па- памяти, выровненный по границе страницы, которому можно присвоить атри- атрибуты доступа (только чтение, чтение/запись, разрешение выполнения про- программного кода и т. д.). Это позволяет приложению выполнять обработку данных с максимальной скоростью и наиболее подходящим образом. Кроме этого, функция virtuaiAiioc может резервировать память без ее физиче- физического выделения, что снижает нагрузку на операционную систему в целом. Наш второй пример связан с использованием функции распределения памя- памяти virtuaiAiioc в операциях копирования. Само копирование выполняется в блоке ассемблерных команд, причем используются команды строковых примитивов с префиксом повторения rep. Это позволяет выполнить опера- операцию максимально быстро. Исходный текст программы, в которой исполь- используются преимущества как функции virtuaiAiioc, так и ассемблерных команд строковых примитивов, показан в листинге 14.2.
438 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование /I VA_EXAMPLE.срр : Defines the entry point for the console application. #include "stdafx.h" ¦include <windows.h> #include <time.h> int _tmain(int argc, _TCHAR* argv[]) { int* src = NULL; int* dst = NULL; printf(" VirtualAlloc copying with ASM EXAMPLE\n\n"); srand( (unsigned)time( NULL ) ); src = (int*)VirtualAlloc(NULL, 10, MEM_COMMIT, PAGE_READWRITE); int* bsrc = src; dst = (int*) VirtualAlloc (NULL, 10, MEM_COMMIT, PAGE_READWRITE) ; printf("\nsrc : "); for(int cnt - 0; cnt < 10;crit++ ) { *src = rand() ; printf("%d ", *src); src++; asm { mov ESI, bsrc mov EDI, dst mov ECX, 10 eld rep movsd
Глава 14. Ассемблер в задачах системного программирования Windows 439 printf("\n\ndst : "); for(int cnt = 0; cnt < 10;cnt++ ) { printf("%d ", *dst); dst++; } VirtualFree(bsrc, O,MEM_RELEASE); VirtualFree(dst, O,MEM_RELEASE); getchar(); return 0; Хочу обратить внимание на оператор int* bsrc = src Он необходим для правильной установки адреса массива в указателе bsrc. Память для хранения десяти элементов массива случайных целых чисел src выделяется с помощью команды src = (int*JVirtualAlloc(NULL, 10, MEM_COMMIT, PAGE_READWRITE) Точно так же выделяется память для массива приемника dst: dst•- (int* J.VirtualAlloc (NULL, 10, MEM_COMMIT, PAGE_READWRITE) Копирование выполняется с большой скоростью ассемблерными командами mov ESI, bsrc mov EDI, dst mov ECX, 10 eld rep movsd После использования выделенной памяти необходимо ее освободить. Это выполняется с помощью функций VirtualFree(bsrc, 0,MEM_RELEASE) VirtualFree(dst, 0,MEMJRELEASE) Окно работающего приложения показано на рис. 14.1.
440 Часть III. 'Встроенный ассемблер Visual C++ .NET2003 и его использование Рис. 14.1. Окно приложения, выполняющего копирование целых чисел с помощью функции VirtualAlloc Операционные системы Windows поддерживают еще одну, весьма полезную технологию работы с файлами. Для операций используются файлы, отобра- отображаемые на память. Эта технология очень удобна для одновременной обра- обработки файлов несколькими процессами и находит широкое применение. Менеджер виртуальной памяти операционной системы позволяет программе работать с файлом так, как будто он загружен в оперативную память ком- компьютера. Для работы с файлом, отображенным на память, необходимо вы- выполнить следующие шаги: 1. Открыть файл С ПОМОЩЬЮ ВЫЗОВа CreateFile. 2. Передать дескрИПТОр файла функции WIN API CreateFileMapping. 3. Получить указатель на буфер памяти, где находится файл, с помощью функции MapviewOf File. Этот указатель является обычным, т. е. с ним можно производить различные операции, допустимые для указателей. 4. По завершению работы с файлом необходимо вызвать функцию UnmapViewOfFile. 5. Удалить дескриптор объекта отображения файла и закрыть дескриптор файла С ПОМОЩЬЮ функции CloseHandle. Следующий пример демонстрирует применение отображения файла на па- память. В зависимости от значения опции выбора A или 0) выполняется пре- преобразование алфавитных символов текстового файла testmap к верхнему или нижнему регистру. Эти преобразования выполняются, как и в преды- предыдущем листинге, с использованием встроенного ассемблера, что обеспечива- обеспечивает хорошую производительность. Исходный текст приложения показан в листинге 14.3.
Глава 14. Ассемблер в задачах системного программирования Windows 441 II FILE_MAPPING_EXAMPLE.срр : Defines the entry point for the console // application. #include "stdafx.h" #include <windows.h> int _tmain(int argc, _TCHAR* argv[]) { HANDLE fin; HANDLE map_fin; char* mapBase = NULL; int fSize; int choice = 0; printf<" USING FILE MAPPING WITH ASM OPTIMIZING\n\n"); printf("Enter 1 - convert to upper, 0 -convert to lower:"); scanf("%d", fichoice); fin - CreateFile("d:\\testmap", GENERIC_WRITE|GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL); if (fin == INVALID_HANDLE_VALUE) { printf("Cannot open file\n"); exit(l); } fSize = GetFileSize(fin, NULL); map_fin = CreateFileMapping(fin, NULL, PAGE_READWRITE, 0, 0, NULL); if (!map_fin) { printf("Cannot open mapping\n");
442 Часть III. Встроенный ассемблер Visual C++ .NET 2003 и его использование getchar(); exitB); . mapBase = (char*)MapViewOfFile(map_fin, FILE_MAP_WRITE, 0, 0, 0); if (!mapBase) { printf("Cannot get the map pointer\n"); getchar(); exit(l); char* dmapBase = mapBase; switch(choice){ case 1: _asm { mov ECX, fSize mov EDI, dmapBase next_char: mov AL, BYTE PTR [EDI] cmp AL, 96 jg high_check jmp next high_check: crop AL, 122 jg next sub AL, 32 mov BYTE PTR [EDI], AL next: add EDI,1 dec ECX jnz next_char } break; case 0: asm {
Глава 14. Ассемблер в задачах системного программирования Windows 443 nextl_char mov mov mov cmp jg jmp highl_check: nextl: break; default: break; cmp jg add mov add dec jnz ECX, fSize EDI, dmapBase AL, BYTE PTR [EDI] AL, 64 highl_check nextl AL, 90 next AL, 32 BYTE PTR [EDI], AL EDI, 1 ECX nextl_char printf("\n NEW CONTENT OF FILE: \n\n"); for (int cnt = 0; cnt < fSize;cnt++) printf ("%c,", *mapBase++); UnmapViewOfFile(mapBase); CloseHandle(map_fin); CloseHandle(fin); getchar{); return 0; Окно работающего приложения, преобразующего символы к верхнему реги- регистру, показано на рис. 14.2, а к нижнему — на рис. 14.3. Мы закончили рассмотрение возможностей ассемблера по оптимизации не- некоторых задач системного программирования. В этой главе были проанали- проанализированы наиболее важные элементы системного программирования и их оптимизация с помощью ассемблера.
444 Часть III. Встроенный ассемблер Visual C++ .NET 2003 и его использование Рис. 14.2. Преобразование символов, содержащихся в текстовом файле, к верхнему регистру ::::? ZF-1 ¦*!? *!W::ik?:™::!!E \ Рис. 14.3. Преобразование символов, содержащихся в текстовом файле, к нижнему регистру
Глава 15 Оптимизация процедурно-ориентированных приложений и системных служб Материал этой главы посвящен принципам использования встроенного ас- ассемблера C++ .NET 2003 в процедурно-ориентированных приложениях Windows и системных службах. Применение ассемблера в каждом из этих типов задач имеет свои особенности, которые и будут рассмотрены. Начнем с классического Windows-приложения процедурно-ориентирован- процедурно-ориентированного типа. Пусть работающая программа должна отображать в рабочей области окна приложения разность двух произвольно выбранных целых чисел. Каркас такого приложения можно легко построить с помощью Мастера приложе- приложений Visual C++ .NET. Предположим, что результат вычитания должен ото- отображаться на экране при щелчке левой кнопки мыши в окне приложения. Исходный текст программы показан в листинге 15.1. ,,, , .. , .. нисляющее ; разность двух целых чисел // WIN_CLASSIC.cpp : Defines the entry point for the application. #include "stdafx.h" ¦include "WIN_CLASSIC.h" #include <stdio.h> #define MAXJLOADSTRING 100 // Global Variables: HINSTANCE hlnst; // current instance TCHAR szTitle[MAX_LOADSTRING]; // The title bar text TCHAR szWindowClass[MAX LOADSTRING]; // the main window class name
446 Часть ///. Встроенный ассемблер Visual C++, NET2003 и его использование 11 Forward declarations of functions included in this code module: ATOM MyRegisterClass(HINSTANCE hinstance); BOOL InitInstance(HINSTANCE, int); LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); LRESULT CALLBACK About(HWND, UINT, WPARAM, LPARAM); int APIENTRY _tWinMain(HINSTANCE hinstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow) { // TODO: Place code here. MSG msg; HACCEL hAccelTable; // Initialize global strings LoadString(hinstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING); LoadString(hinstance, IDC_WIN_CLASSIC, szWindowClass, MAX_LOADSTRING); MyRegisterClass(hinstance); // Perform application initialization: if (!Initlnstance (hinstance, nCmdShow)) { return FALSE; } hAccelTable = LoadAccelerators(hinstance, (LPCTSTR)IDC_WIN_CLASSIC); // Main message loop: while (GetMessage(&msg, NULL, 0, 0)) { if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg)) { TranslateMessage(&msg);
Глава 15. Оптимизация процедурно-ориентированных приложений... 447 DispatchMessage(&msg); } } return (int) msg.wParam; } // FUNCTION: MyRegisterClass() // PURPOSE: Registers the window class. // COMMENTS: // This function and its usage are only necessary if you want this code // to be compatible with Win32 systems prior to the •RegisterClassEx' // function that was added to Windows 95. It is important to call this // function so that the application will get 'well formed1 small icons // associated with it. ATOM MyRegisterClass(HINSTANCE hlnstance) { WNDCLASSEX wcex; wcex.cbSize = sizeof(WNDCLASSEX); wcex.Style = CS_HREDRAW I CS_VREDRAW; wcex.lpfnWndProc = (WNDPROC)WndProc; wcex.cbClsExtra = 0; wcex.cbWndExt ra = 0; wcex.hlnstance = hlnstance; wcex.hlcon = Loadlcon(hlnstance, (LPCTSTR) IDI_WIN_CLASSIC). ; wcex.hCursor = LoadCursor(NULL, IDC_ARROW); wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW-1); wcex.lpszMenuName = (LPCTSTR)IDC_WIN_CLASSIC; wcex.lpszClassName = szWindowClass; wcex.hleonSm = Loadlcon(wcex.hlnstance, (LPCTSTR)IDI SMALL);
448 Часть III. Встроенный ассемблер Visual C++ .NET 2003 и его использование return RegisterClassEx(&wcex); } // FUNCTION: Initlnstance(HANDLE, int) // PURPOSE: Saves instance handle and creates main window // COMMENTS: // In this function, we save the instance handle in a global variable and // create and display the main program window. BOOL Initlnstance(HINSTANCE hlnstance, int nCmdShow) { HWND hWnd; hlnst = hlnstance; // Store instance handle in our global variable hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CWJJSEDEFAULT, 0, NULL, NULL, hlnstance, NULL); if (IhWnd) { return FALSE; ShowWindow(hWnd, nCmdShow) UpdateWindow(hWnd); return TRUE; // FUNCTION: WndProc(HWND, unsigned, WORD, LONG) // PURPOSE: Processes messages for the main window. // WM_COMMAND - process the application menu // WM_PAINT - Paint the main window // WM DESTROY - post a quit message and return
Глава 15. Оптимизация процедурно-ориентированных приложений... 449 LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { int wmld, wmEvent; PAINTSTRUCT ps; HDC hdc; RECT re; int il, i2, ires; char buf[32] - "il - i2 - "; switch (message) { case WM_COMMAND: wmld = LOWORD(wParam); wmEvent =* HIWORD(wParam); // Parse the menu selections: switch (wmld) { case IDM_ABOUT: DialogBox(hlnst, (LPCTSTR)IDD_ABOUTBOX/ hWnd, (DLGPROC)About); break; ( case IDM_EXIT: DestroyWindow(hWnd); break; default: return DefWindowProc(hWnd, message, wParam, lParam); } break; case WM_PAINT: hdc = BeginPaint(hWnd, &ps); // TODO: Add any drawing code here... EndPaint(hWnd, &ps); break;
450 Часть ///. Встроенный ассемблер Visual C++ .NET2003 и его использование II Added code case WM_LBUTTONDOWN: 11 = 45; 12 - 98; hdc = GetDC(hWnd); GetClientRect(hWnd, &rc); _asm { mov EAX, il sub EAX, i2 mov ires, EAX } sprintf(&buf[9],"%dfl, ires); TextOut(hdc, (rc.right-rc.left)/3, (re.bottom-re.topI2, buf,12); InvalidateRect(hWnd, &rc, FALSE); ReleaseDC(hWnd, hdc); break; case WM_DESTROY: PostQuitMessage@); break; default: return DefWindowProc(hWnd, message, wParam, lParam) ; } return 0; // Message handler for about box. LRESULT CALIiBACK About (HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { case WM_INITDIALOG: return TRUE; case WM_COMMAND: if (LOWORD(wParam) = IDOK || LOWORD(wParam) — IDCANCEL) { EndDialog(hDlg, LOWORD(wParam));
Глава 15. Оптимизация процедурно-ориентированных приложений... 451 return TRUE.; } break; } return FALSE; Поскольку мы имеем дело с классическим процедурно-ориентированным приложением Windows, для вывода результата на экран используется обра- обработчик события wmlbuttondown: case WM_LBUTTONDOWN: 11 - 45; 12 = 98; hdc = GetDC(hWnd); GetClientRect(hWnd, &rc) ; _asm { mov EAX, il sub EAX, i2 mov ires, EAX } sprintf(&buf[9],"%d", ires); TextOut(hdc, (rc.right-rc.left)/3, (re.bottom-re.top)/2, buf,12); InvalidateRect(hWnd, &rc, FALSE); ReleaseDC(hWnd, hdc) ; break; Программный код обработчика нажатия левой кнопки поместим, например, после обработчика wmpaint в функции обратного вызова. Проанализируем этот программный код. Первые два оператора присваивают переменным (il и i2) какие-либо цело- целочисленные значения. В нашем случае il = 4 5, ±2 = 98. Сами переменные объявлены в функции обратного вызова wndProc. Здесь же объявлены вспомогательные переменные re, ires и buf. Для того чтобы отобразить результат вычитания в окне приложения, вначале требуется получить контекст устройства рисования. Это выполняется опера- оператором hdc = GetDC(hWnd) Далее получаем координаты клиентской области окна приложения с помо- помощью оператора GetClientRect(hWnd, &rc)
452 Часть III. Встроенный ассемблер Visual C++ .NET 2003 и его использование Эти координаты нам понадобятся при позиционировании и выводе текста на экран. Блок ассемблерных команд выполняет в этом обработчике основную рабо- работу — вычисляет разность двух чисел. Результат помещается в переменную ires. Для вывода на экран значение целочисленной переменной ires нужно пре- преобразовать в текстовую строку. Для этого подходит функция sprintf. Функ- Функция определена в заголовочном файле stdio.h, поэтому он должен быть включен в исходный текст программы. Функция sprintf имеет вид: sprintf(&buf[9], "%d", ires) После преобразования целочисленной переменной ires в строку символов (переменная buf) можно вывести результат в клиентскую область окна. Это выполняется с помощью функции TextOut(hdc, (rc.right-rc.left)/3, (re.bottom-re.top)/2, buf,12) Наконец, нужно сообщить операционной системе о необходимости перери- перерисовки клиентской области окна приложения. Это выполняет функция InvalidateRect(hWnd, &rc, FALSE); Перед выходом из обработчика необходимо освободить контекст устройства: ReleaseDC(hWnd, hdc) ; Применение ассемблерных блоков и команд в процедурно-ориентирован- процедурно-ориентированном приложении выполняется довольно просто. Для этого нужно поместить программный код в обработчик какого-либо события.. Следует учитывать и тот факт, что для вывода результатов математических вычислений на экран или на принтер необходимо преобразовать их в символьный тип. Функция Textout и подобные ей оперируют с символьными данными, поэтому и возникает необходимость такого преобразования. Окно работающего приложения показано на рис. 15.1. I i U* Рис. 15.1. Окно процедурно-ориентированного приложения Windows, показывающего результат вычитания двух целых чисел
Глава 15. Оптимизация процедурно-ориентированных приложений... 453 Язык ассемблера является эффективным средством оптимизации при разра- разработке системных служб (system services) Windows. Системные службы Windows — это наиболее таинственная часть операционной системы и, в то же время, одна из наиболее важных составляющих ОС. Можно кратко опре- определить службу как программу, работающую вне контекста пользователя. Системные службы поддерживаются только в операционных системах Windows NT/2000/XP/2003. В Windows службы используются для решения важнейших задач, связанных с управлением и мониторингом операционной системы, обеспечивают доступ к драйверам файловой и дисковой системы компьютера, являются вспомогательным звеном для доступа к аппаратным ресурсам компьютера. Большинство служб весьма критичны по отношению ко времени исполне- исполнения, особенно если работают совместно с аппаратными ресурсами компью- компьютера. Применение ассемблера для написания критических участков кода та- таких служб — часто единственный способ обеспечить корректную работу в жестких временных интервалах. Предположим, какая-либо программа принимает поток данных с USB- или СОМ-порта персонального компьютера и записывает их в файл для после- последующей обработки. Системная служба в определенные интервалы времени считывает данные из этого файла и обрабатывает их в фоновом режиме. Пусть (как это часто бывает) поток данных, записанных в файл с USB- порта, представляет собой последовательность целых чисел. Обработка таких данных службой может заключаться в определении максимального значения из поступивших на данный момент времени данных и записанных в файл. В системах измерения и обработки данных в промышленности, научных ис- исследованиях подобная конфигурация программных средств встречается очень часто. Программа-драйвер считывает данные из физического устройства и выполняет их предварительную обработку. После предварительной обра- обработки данные обычно сохраняются в файле. Последующие манипуляции с данными выполняет другая программа, и во многих случаях она реализована в виде системной службы. Драйвер устройства работает в реальном масштабе времени, число выборок данных в секунду может досигать нескольких тысяч или даже десятков тысяч. Для хранения и первичной обработки данных в этом случае может потребоваться большой объем памяти и значительная доля процессорного времени. Если количество поступивших байт незначительно или интервал времени ограничен, то использование оперативной памяти вполне оправ- оправдано. Но только для этого случая. Очень удобно было бы хранить данные в файле, объемы накопителей на жестких дисках позволяют это делать, но здесь возникает другая проблема — скорость обработки данных из файла приложением высокого уровня. Если программа-драйвер и сможет запи- записывать данные на диск очень быстро (что вполне достижимо), то вы- выполнение высокоуровневой обработки, анализа и графического представле-
454 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование ния результатов в реальном времени может оказаться весьма сложной проблемой. Избыточность кода системной службы при обработке приводит к полной неработоспособности всей системы сбора и анализа данных. Программный код службы Windows для таких случаев должен быть предель- предельно компактным и быстрым. Кроме того, необходимо учитывать, что опера- операционные системы Windows NT/2000/XP/2003 не являются системами реаль- реального времени, и обработка данных может занять намного больше времени, чем планирует разработчик. Здесь опять выручает ассемблер. Хорошо напи- написанные фрагмент кода или функция способны обеспечить на критических участках нужную производительность. Я рассмотрю два примера разработки системных служб Windows, в которых для оптимизации быстродействия используется ассемблерный код. Учиты- Учитывая сложность и важность темы, нам понадобятся некоторые теоретические сведения, касающиеся базовых принципов разработки системных служб. Рассмотрим только основные аспекты для лучшего понимания приведенных в книге примеров и для написания собственных. С точки зрения разработчика программного обеспечения, службы — это программы, обычно консольные, определенным образом связанные с дис- диспетчером служб SCM (Service Control Manager) операционной системы. Диспетчер служб представляет собой внутренний механизм Windows, вы- выполняющий управление службами и драйверами устройств. Взаимодействие SCM с пользователем осуществляется посредством Services Control Panel (Windows NT 4.0) или одного из модулей консоли ММС (Microsoft Manage- Management Console) операционных систем Windows 2000/XP/2003. Как и любое другое консольное приложение, служба обладает функцией main, которая может обеспечивать работу нескольких служб. В наших при- примерах будут рассмотрены приложения для одной службы. Консольное при- приложение службы должно содержать точку входа службы, именуемую обычно servicestart или serviceMain. С точкой входа службы связан текстовый идентификатор службы. Для взаимодействия службы с системой необходимо зарегистрировать функ- функцию-обработчик управления службой. Сама служба обычно выполняется в фоновом режиме и требует для работы отдельного программного потока. Раз- Разработку программного кода системной службы можно выполнить либо вруч- вручную, либо воспользоваться Мастером приложений Visual C++ .NET 2003. Вне зависимости от того, какими программными средствами создается служба, необходимо выполнить следующие действия: 1. Определить точку входа main о, которая будет выполнять регистрацию службы в SCM. При этом диспетчеру служб сообщаются точка входа (servicestart) службы и ее имя. Эти действия выполняет функция StartServiceCtrlDispatcher.
Глава 15. Оптимизация процедурно-ориентированных приложений... 455 2. Передать управление функции servicestart для регистрации обработчи- обработчика управления службой. Регистрация выполняется с помощью вызова функции RegisterServiceCtrlHandler. Кроме ТОГО, функция Servicestart, как правило, запускает на выполнение основной поток службы с ПОМОЩЬЮ ОДНОЙ ИЗ функций beginthread, beginthreadex ИЛИ CreateThread. 3. Создать обработчик управления службы. Теперь можно перейти к практическим примерам. Наш первый пример мо- моделирует работу небольшой системы сбора и обработки данных. В этом примере, к сожалению, я не смогу проиллюстрировать работу в реальном времени, цель этой демонстрации — показать, как можно использовать язык ассемблера в службе Windows, обрабатывающей данные из файла. Систем- Системная служба выполняет простую задачу — поиск максимального значения среди целых чисел, записанных в файл с именем test. В качестве программы-драйвера выступит тестовая программа, которая будет записывать пять случайных целых чисел в файл test каждый раз при нажа- нажатии клавиши < Enter> и выводить содержимое этого файла на экран. Кроме этого, на экран будет выводиться значение максимума, прочитанного из файла testmax. В нашей "виртуальной" системе обработки данных будет запущена систем- системная служба (назовем ее Fiiewriter), которая с интервалом в 30 секунд будет считывать содержимое файла test, выполнять поиск максимума и записы- записывать полученное значение в файл testmax. Мы рассмотрим, как выполняется поиск максимума с помощью блока ас- ассемблерных команд системной службы Fiiewriter. Для разработки службы разработаем проект консольного приложения без генерации файлов исход- исходных текстов (Empty Project), в который включим наш файл (назовем его filewriter.cpp) с исходным текстом (листинг 15.2). ¦include <windows.h> #include <stdio.h> #include <string.h> #include <time.h> ¦include <process.h> SERVICE_STATUS ServicelStatus; SERVICE_STATUS^HANDLE ServicelStatusHandle; int modtime=30; // интервал = ЗОсекунд
456 Часть III. Встроенный ассемблер Visual C++ .NET 2003 и его использование HANDLE thread; BOOL manual=FALSE; int rx[256] ; int imax; FILE* fp; void findmax(void *) time_t timeval; while (ServicelStatus.dwCurrentState!=SERVICE_STOPPED) if (ServicelStatus.dwCurrentState!=SEKvTCE_PAUSED) time Ftimeval) ; if ((timeval%modtizne)=0| | manual) if (>(fp = fopen("d:\\test", "a+b"))) exit(l) ; int fres = fseek(fp, 0, SEEK_END); int fsize = ftell(fp); fres = fseek(fp, 0, SEEK_SET); fread(&rx, sizeof(int), fsize, fp); f close (fp) ; asm { mov ECX, fsize shr ECX, 2 lea ESI, rx mov EAX, DWORD PTR [ESI] again: add ESI, 4 cmp EAX, DWORD PTR [ESI] jge next_int xchg EAX, DWORD PTR [ESI] next_int: dec ECX jnz again mov imax, EAX
Глава 15. Оптимизация процедурно-ориентированных приложений... 457 if (! (fp = fopen("d:\\testxnax", "w+Ь"))) exit(l); fwrite(&imax, sizeof (int) , 1, fp) ; f close (fp) ; } manual=FALSE; } SleepE00); VOID stdcall CtrlHandler (DWORD Opcode) { DWORD status; switch(Opcode) { case SERVICE_CONTROL_PAUSE: // Do whatever it takes to pause here. ServicelStatus.dwCurrentState = SERVTCEJPAUSED; break; case SERVICE J3ONTROL_CONTINUE: // Do whatever it takes to continue here. ServicelStatus.dwCurrentState = SERVICE_RUNNING; manual=TRUE; break; case SERVICE_CONTROL_STOP: // Do whatever it takes to stop here. ServicelStatus.dwWin32ExitCode = 0; ServicelStatus.dwCurrentState = SERVICE_STOPPED; ServicelStatus.dwCheckPoint = 0; ServicelStatus.dwWaitHint = 0; if-iiSetServiceStatus (ServicelStatusHandle, &ServicelStatus) status = GetLastError();
458 Часть III. Встроенный ассемблер Visual C++ .NET 2003 и его использование return; case SERVICE_CONTROL_INTERROGATE: break; default: // Send current status. if (!SetServiceStatus (ServicelStatusHandle, &ServicelStatus) status = GetLastError(); return; void stdcall ServicelStart (DWORD argc, LPTSTR *argv) { DWORD status; DWORD specificError; if (argol) modtime=atoi (argv[l]) ; if (modtime=-0) modtime=l; ServicelStatus.dwServiceType = SERVICE_WIN32; ServicelStatus.dwCurrentState = SERVICE_START_PENDING; ServicelStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP I SERVICE_ACCE PT_PAUSE_CONTINUE; ServicelStatus.dwWin32ExitCode = 0; ServicelStatus.dwServiceSpecificExitCode = 0; ' ServicelStatus.dwCheckPoint = 0; ServicelStatus.dwWaitHint = 0; ServicelStatusHandle = RegisterServiceCtrlHandler(TEXT("FileWriter") CtrlHandler); if (ServicelStatusHandle — (SERVICE_STATUS_HANDLEH) return; // Initialization code goes here. status=NO_ERROR; // Handle error condition
Глава 15. Оптимизация процедурно-ориентированных приложений... 459_ if (status != NO_ERROR) { ServicelStatus.dwCurrentState = SERVICE_STOPPED; ServicelStatus.dwCheckPoint = 0; ServicelStatus.dwWaitHint = 0; ServicelStatus.dwWin32ExitCode * status; ServicelStatus.dwServiceSpecificExitCode = specificError; SetServiceStatus (ServicelStatusHandle, SServicelStatus); return; } // Initialization complete - report running status. ServicelStatus.dwCurrentState - SERVICE_RUNNING; ServicelStatus.dwCheckPoint = 0; ServicelStatus.dwWaitHint = 0; if (!SetServiceStatus (ServicelStatusHandle, &ServicelStatus)) status = GetLastError(); // This is where the service does its work. thread- (HANDLE) Jseginthread(findmax,0,NULL); return; } void main(int argc, char *argv[]) { SERVICE_TABLE_ENTRY DispatchTable[] > { { TEXT("FileWriter"), ServioelStart }, { NOLL, NOLL } >; if (argc>l && !stricrrp(argv[l], "delete")) { SC_HANDLE scm=6penSCManager(NULL,NULL,SC_MANAGER_CREATE_SERVICE); if (!scm) { printf("Can't open SCM\n");
460 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование exit(l); } SC_HANDLE svc=OpenService(son,"FileWriter",DELETE); if (!svc) { printf("Can't open service\n"); exitB); } if (!DeleteService(svc)) { printf ("Can't delete serviceW); exitC); } printf("Service deletedXn"); CloseServiceHandle(svc); CloseServiceHandle(scm); exit(O); } if (argol && !stricmp(argv[l], "setup")) { char pname[1024]; pname [0] = ""; GetModuleFileName(NULL,pname+1,1023); strcat(pname,"\""); SC_HANDLE scm=OpenSCManager(NULL,NULL,SC_MANAGER_CREATE_SERVICE),svc; if (!scm) { printf("Can't open SCM\n"); exit(l); } if(!(svc=CreateService(scm,"FileWriter","FileWriter", SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS, SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL, pname,NULL,NULL, NULL,NULL,NULL)))
Глава 15. Оптимизация процедурно-ориентированных приложений...461 printf("Registration error!\n"); exitB); } printf("Successfully registered\n"); CloseServiceHandle(svc); CloseServiceHandle(scm) ; exit(O); if (!StartServioeCtrlDispatcher( DispatchTable)) Как работает системная служба FiieWriter? При вызове функции Servicestart (точка входа в системную службу) запускается основной поток с функцией f indmax, выполняющий всю работу службы: thread= (HANDLE)_beginthread(findmax,0,NULL) С интервалом времени в 30 секунд, определяемым переменной modtime, происходит вызов ассемблерного блока команд функции f indmax: asm { mov shr lea mov again: add cmp jge xchg next int dec jnz mov ECX, ECX, ESI, EAX, ESI, EAX, next_ EAX, : ECX again imax, fsize 2 rx DWORD PTR 4 DWORD PTR int DWORD PTR EAX [ESI] [ESI] [ESI] Этот фрагмент кода выполняет поиск максимального элемента в буфере па- памяти с адресом, определяемым регистром esi. В esi находится адрес буфера целых чисел гх, куда считываются данные из файла test. Полученное зна-
462 Часть HI. Встроенный ассемблер Visual C++ .NET2003 и его использование чение максимума сохраняется в переменной imax. Затем с помощью опера- операторов if (!(fp = fopen("d:V\testmax", "w+b"))) exit(l); fwrite(&imax, sizeof(int), 1, fp); fclose(fp); значение максимума из переменной imax записывается в файл testmax. Операция вычисления максимума повторяется каждые 30 секунд, хотя мож- можно задать и другой интервал в переменной modtime. Несколько слов о запуске системной службы FiieWriter. Первое, что нуж- нужно сделать, — проинсталлировать службу в операционной системе. Для этого из командной строки необходимо выполнить оператор FiieWriter setup Для удаления системной службы необходимо выполнить команду FiieWriter delete Процесс инсталляции системной службы показан на рис. 15.2. Рис. 15.2. Инсталляция системной службы FiieWriter в Windows Проинсталлированную службу FiieWriter можно запустить из окна управ- управления системными службами (рис. 15.3). Тестовая Программа ДЛЯ проверки работы СИСТеМНОЙ службы FiieWriter записывает в двоичный файл с именем test пять произвольных целых чи- чисел. В этой же программе определяется максимум из записанных в файле элементов. Программа также открывает файл с именем testmax, созданный системной службой FiieWriter, и проверяет значение записанного макси- максимума. Файл testmax будет содержать значение максимума, полученное с помощью системной службы перед очередным запуском тестовой програм-
Глава 15. Оптимизация процедурно-ориентированных приложений... 463 мы. Исходный текст тестовой консольной программы представлен в лис- листинге 15.3. Рис. 15.3. Запуск службы FileWriter ;ти«г 15.3. Тестовая программа для проэс-пии работы системной службы // FILE__WRITE_ADD_EXM.cpp : Defines the entry point for the console // application. «include "stdafx.h" #include .<stdlib.h> «include <time.h> int _tmain(int argc, _TCHAR* argv[]) { FILE* fp; int ix[25]; int rx[256]; int imax; int imaxl = 0; printf("SYSTEM SERVICE FILEWRITERS' TEST APPLICATION\n\n") srand( (unsigned)time( NULL ) );
464 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование for (int cnt = 0;cnt < 5;cnt++) ix[cnt] = rand(); if (!(fp = fopen("d:\\test", "a+b"))) { printf("Cannot open file!\n"); exitA); } fwrite(&ix, sizeof(int), 5, fp); int fres = fseek(fp, 0, SEEK_END); int fsize = ftell(fp); printf("Size of file TEST = %d Bytes\n\n", fsize); fres = fseek(fp, 0, SEEK_SET) ; fread(&rx, sizeof(int), fsize, fp); for (int cnt = 0;cnt < fsize/4;cnt++) printf("%d ", rxfcnt]); fclose(fp); _asm { mov -ECX, fsize shr ECX, 2 lea ESI, rx mov EAX, DWORD PTR [ESI] again: add ESI, 4 crop EAX, DWORD PTR [ESI] j ge next_int xchg EAX, DWORD PTR [ESI] next_int: dec ECX jnz again mov imax, EAX }; printf("\nMaximum = %d\n", imax); if (!(fp = fopen("d:\\testmax", "a+b"))) { printf("Cannot open file!\n");
Глава 15. Оптимизация процедурно-ориентированных приложений... 465 exit(l); } fread(&imaxl, sizeof(int), 1, fp); printf("\nFile TESTMAX contains = %d ", imaxl); getchar(); return 0; } Окно работающего приложения показано на рис. 15.4. Рис. 15.4. Окно приложения, тестирующего системную службу Второй пример применения ассемблерного кода в системной службе более сложный. Предположим, необходимо подсчитывать количество слов в тек- текстовом файле через определенные интервалы времени и отображать эту ин- информацию на экране дисплея. Текстовая информация записывается или удаляется из файла в произвольные моменты времени. Здесь, как и в пре- предыдущем примере, моделируется ситуация обработки информации в реаль- реальном времени. Для отслеживания изменений в файле лучше всего подходит реализация программы как системной службы. В этот раз воспользуемся Мастером приложений Visual C++ .NET и созда- создадим каркас службы. Будьте очень внимательны при выполнении последую- последующих шагов. Выберем тип приложения Windows Service (рис. 15.5). Назовем проект cwservice. После того как шаблон системной службы соз- создан, изменим установленное Мастером имя службы cwserviceWinService на cwservice, оставив остальные опции проекта без изменения (рис. 15.6).
466 Часть III. Встроенный ассемблер Visual C++ .NET 2003 и его использование Рис. 15.5. Выбор типа приложения Windows Service Ш CWServfce - Microiell Visual C++ lieiign] - CWSkv«iceWinSeryiceit f Oesiinl* t.WbcrviccWm1 To add cofliponenSs to your cbss, Фщ them from the SeryerEyp'wer or Tootoox and use me Properties window to set their properties. To create methods anc events for your class, clcfc here to mt,?h to шее view True -vet False ¦¦Co True False ¦': "! \_Г ¦¦".''.' : ' f Рис. 15.6. Установка имени службы I! I X!
Глава 15. Оптимизация процедурно-ориентированных приложений... 467 Включим в проект инсталлятор для нашей службы. Это необходимо для ин- инсталляции/деинсталляции, а также для установки опций запуска службы. Добавить инсталлятор можно в поле конструктора проекта, если после щелчка правой кнопки мыши выбрать опцию Add Installer (рис. 15.7). Ш CWSenrice * Mkrasoft f Hue» C++ (design] - Prepctliirtaler Ji [Design]* ProjcctIn*taM<*f.h [Design]' 4s4Vif t* Init.ilk'f 1 {"¦ .--.: Рис. 15.7. Включение инсталлятора в проект Далее необходимо присвоить имя cwservice полю DisplayName инсталлято- инсталлятора службы (рис. 15.8). Хочу заметить, что в общем случае имена, присвоенные полям DisplayName и ServiceName, могут отличаться. Поле StartType оставим без изменений (служба запускается вручную). Поле Account инсталлятора процесса должно содержать запись Local system (рис. 15.9). Последующие шаги связаны с модификацией программного кода службы. Вспомним, что наша служба должна выполнять периодический подсчет ко- количества слов в текстовом файле. Для выполнения периодических операций воспользуемся компонентом Timer панели Toolbox. Поместим компонент на рабочее поле конструктора. Установим поля свойств так, как показано на рис. 15.10.-.
468 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование Рис. 15.8. Установка свойства DisplayName ':'¦¦ -.Г ¦¦¦¦ : >:-¦¦ :'¦[> i Рис. 15.9. Установка опции Account
Глава 15. Оптимизация процедурно-ориентированных приложений. 469 '¦¦ W4: i-vit *¦ W:<:-,...>..<:.iifi.':;-,i-.jii j* Рис. 15.10. Установка свойств таймера i:: :¦:¦)! Рис. 15.11. Установка опций Event Log
470 Часть III. Встроенный ассемблер Visual C++ .NET 2003 и его использование Как видно из рис. 15.10, интервал срабатывания таймера задан равным 10 секунд, и в исходном состоянии таймер отключен. Кроме того, поместим на рабочее поле компонент EventLog. Он нам понадобится для вывода ин- информации о количестве слов в файле. Этот компонент связывает нашу службу с журналом регистрации событий Windows. Установим свойства компонента EventLog, как показано на рис. 15.11. Сделаю некоторые пояснения относительно установки опций EventLog. Ис- Источником событий, подлежащих регистрации, является наша служба cwservice, а сами события будут регистрироваться в файле журнала регист- регистрации событий. Для того чтобы наша служба cwservice выполняла что-либо полезное, не- необходимо в обработчике события onstart активизировать таймер. После этого обработчик события таймера onTimer сможет выполнять необходимые действия. В случае остановки службы необходимо остановить и таймер. Это делается в обработчике события onstop. Далее представлен исходный текст класса cwservicewinService, определяющего поведение нашей службы (листинг 15.4). tpragma once using namespace System; using namespace System:'.Collections; using namespace System::ServiceProcess; using namespace System::ComponentModel; #include <windows.h> tinclude <stdio.h> using namespace System::Runtime::InteropServices; [Dlllmportrcw.dll")] extern "C" int countwords(char* path); int cntWords; char buf[64]; int ibuf; namespace CWService
Глава 15. Оптимизация процедурно-ориентированных приложений... 471 III <suramary> /// Summary for CWServiceWinService . /// </suimary> /// /// WARNING: If you change the name of this class, you will need to /// change the ///'Resource File Name1 property for the managed resource compiler tool ///associated with all .resx files this class depends on. Otherwise, ///the designers will not be able to interact properly with localized ///resources associated with this form. public gc class CWServiceWinService : <P public System::ServiceProcess::ServiceBase { public: CWServiceWinService() { InitializeComponent(); } /// <summary> /// Clean up any resources being used. /// </summary> void Dispose(bool disposing) { if (disposing && components) { components->Dispose(); } super::Dispose(disposing); protected: /// <summary> ///- Set things in motion so your service can do its work. /// </summary> void OnStart(String* args[]) { // TODO: Add code here to start your service.
472 Часть HI. Встроенный ассемблер Visual C++ .NET2003 и его использование timerl->Enabled = true; /// <summary> /// Stop this service. /// </sumnaary> void OnStop() { // TODO: Add code here to perform any tear-down necessary // to stop your service. timerl->Enabled = false; } private: System::Diagnostics::EventLog * eventLogl; private: System::Timers::Timer * timerl; private: /// <summary> /// Required designer variable. /// </summary> System::ComponentModel::Container *components; /// <summary> /// Required method for Designer support - do not modify /// the contents of this method with the code editor. /// </summary> void InitializeComponent(void) { this->eventLogl = new System::Diagnostics::EventLog(); this->timerl = new System::Timers::Timer(); ( try_cast<System::ComponentModel::ISupportlnitialize * ><p (this->eventLogl))->BeginInit(); ( try_cast<System::ComponentModel::ISupportlnitialize * ><P (this->timerl))->BeginInit(); // // eventLogl // this->eventLogl->Source = S"CWService";
Глава 15. Оптимизация процедурно-ориентированных приложений... 473 II timerl this->timerl->Interval = 10000; this->timerl->Elapsed += new <P System:rTimers::ElapsedEventHandler(this, timerl_Elapsed); // // CWServiceWinService // this->CanPauseAndContinue = true; this->ServiceName = S"CWService"; ( try__cast<System: :ComponentModel: : ISupportlnitialize * ><P (this->eventLogl))->EndInit(); ' ( try_cast<System::ComponentModel::ISupportlnitialize * ><? (this->timerl))->EndInit(); private: System::Void timerl_Elapsed(System::Object * sender, System::Timers::ElapsedEventArgs * e) ibuf = sprintf(buf, "%s", "Number of words in file = cntWords = countwords ("d: Wtestfile") ; sprintf(buf + ibuf, "%d", cntWords); eventLogl->WriteEntry("CWService", (LPSTR)buf); "); Даже при беглом взгляде на листинг возникает вопрос: где же функция, вы- выполняющая обработку файла? Эту функцию я поместил в библиотеку дина- динамической компоновки cw.dll и назвал countwords. Функция написана на ассемблере и выполняет подсчет слов в файле. Подключение DLL к службе выполнено с помощью строк: using namespace System::Runtime::InteropServices; [DllImport("cw.dll")] extern "O"- int countwords (char* path)
474 Часть III. Встроенный ассемблер Visual C++ .NET2003 и его использование Подсчет слов и вывод результата в журнал регистрации выполняются в об- обработчике события таймера: ibuf = sprintf(buf, "%s", "Number of words in file = "); cntWords = countwords ("d: Wtestfile"); sprintf(buf + ibuf, "%d", cntWords); eventLogl->WriteEntry("CWService", (LPSTR)buf); Для подсчета слов выбран небольшой текстовый файл с именем testfiie. Количество слов, как было сказано, вычисляется функцией countwords из библиотеки cw.dii, а затем запоминается в переменной cntWords. Отформа- Отформатированная строка с результатом отображается в журнале приложений. Исходный текст программного кода библиотеки динамической компоновки cw.dll представлен в листинге 15.5. 15.5. Исходной текст ow.dll // cw.cpp : Defines the entry point for the DLL application. #include "stdafx.h" #include <windows.h> BOOL APIENTRY DllMain{ HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { return TRUE; } extern "C" declspec(dllexport) int countwords(char* path) { HANDLE hln; DWORD bytesRead, lenFile; int cntWords = 0; char buf[2048]; hln = Creat,eFile(path,GENERIC_READ,O, NULL, OPEN_EXISTING, 0, NULL); lenFile = GetFileSize (hln, NULL);
Глава 15. Оптимизация процедурно-ориентированных приложений... 475 ReadFile(hin, buf, lenFile CloseHandle(hin); _asm { lea mov dec inc check_space: inc dec jz cmp je cmp je cmp je jmp next_char: inc dec jz cmp je cmp je cmp je jmp inc cntWords: inc jmp ex: mov ESI, buf EDX, bytesRead ESI EDX ESI EDX ex BYTE PTR [ESI], check^space BYTE PTR [ESI], check_space BYTE PTR [ESI], check_space next_char ESI EDX ex BYTE PTR [ESI], inc_cntWords ¦ BYTE PTR [ESI], inc_cntWords BYTE PTR [ESI], inc_cntWords next char cntWords check_space EAX, cntWords , &bytesRead,NULL); i i Oxd Oxa i i Oxd Oxa
476 Часть HI. Встроенный ассемблер Visual C++ .NET2003 и его использование Функция countwords принимает в качестве параметра указатель на строку символов, представляющую собой имя текстового файла. Содержимое файла считывается в буфер памяти размером 2 Кбайт. Следует иметь в виду, что размер текстового файла не должен превышать размер буфера памяти, а сам размер буфера может быть выбран произвольно. Далее ведется поиск и выделение отдельных слов, находящихся в буфере памяти. Алгоритм поиска основан на предположении, что слова отделяются друг от друга символом пробела или возврата каретки, что чаще всего и бы- бывает на практике. Думаю, читатель сможет самостоятельно проанализиро- проанализировать листинг с исходным текстом DLL. Хочу сказать несколько слов о запуске системной службы cwservice. Ин- Инсталляция службы выполняется с командной строки: cwservice.exe -install а деинсталляция — с помощью команды cwservice.exe -install /u После инсталляции служба запускается и останавливается, как обычно, с использованием ММС-консоли. Работу службы cwservice можно наблю- наблюдать, если просматривать журнал регистрации событий (рис. 15.12). Рис. 15.12. Сообщение системной службы CWService
Глава 15. Оптимизация процедурно-ориентированных приложений... 477 Содержимое самого текстового файла testf iie показано на рис. 15.13. Рис. 15.13. Содержимое файла testf lie Мы рассмотрели примеры использования ассемблера для программирования процедурно-ориентированных приложений и системных служб Windows. Несмотря на сложность этих примеров, хочется надеяться, что они принес- принесли читателям пользу.
Заключение Автор попытался дать в книге обширную информацию по применению ас- ассемблера для программирования приложений в среде Microsoft Visual C++ .NET 2003. В конце хотелось бы сделать некоторые дополнительные замеча- замечания, касающиеся материала книги. Несмотря на широкий диапазон осве- освещаемых вопросов, часть из них осталась незатронутой. Хочется надеяться, что даже в таком объеме книга принесет пользу читателю. Так сложилось, что отечественные программисты большую часть ассемблер- ассемблерных программ пишут на макроассемблере MASM фирмы Microsoft. Учиты- Учитывая и то, что в среде программирования C++ .NET используется тот же макроассемблер, примеры книги разработаны с использованием синтаксиса MASM. Это ни в коей мере не означает, что нельзя использовать и другие автономные компиляторы ассемблера. Автор рекомендует обратить внима- внимание и на другие инструменты разработки на языке ассемблера, например, на очень неплохой ассемблер NASM. С помощью ассемблера можно добиться значительного повышения произ- производительности приложений на C++ .NET, причем намного меньшей ценой, чем в случае применения других методов оптимизации. На ассемблере можно писать и полнофункциональные графические приложения, тем более, что в настоящее время существуют очень мощные средства разработ- разработки, такие как MASM 32 или AsmStudio. Они позволяют создавать приложения на языке ассемблера довольно быстро и качественно. При всех достоинствах автономных пакетов разработки на ассемблере следует отметить тот факт, что время разработки крупных при- приложений с их помощью требует намного больше времени, чем с использо- использованием языков высокого уровня. Автономные компиляторы ассемблера очень эффективны для написания от- отдельных программных модулей с последующим их включением в приложе- приложения, написанные на языках высокого уровня, в частности, на C++ .NET. К сожалению, фирма Microsoft и другие известные производители программ- программного обеспечения для разработки в среде Windows больше не развивают авто- автономные компиляторы ассемблера и прекратили разработки в этом направлении. Альтернативой, причем довольно неплохой, являются продукты третьих фирм и независимых разработчиков, например, тот же NASM. Одной из причин отказа крупных фирм от разработок компиляторов ассемблера является то, что ассемб- ассемблер стал частью среды программирования в языках высокого уровня. Встроенный ассемблер языков высокого уровня хоть и не является самостоя- самостоятельным средством разработки, но весьма эффективен для написания быст- быстрых и производительных программ. Автор надеется, что приведенные приме- примеры использования встроенного ассемблера Visual C++ .NET смогли убедить читателей в необходимости применения этого языка в своих программах. Хочется верить, что эта книга станет настольной для многих программи- программистов — как опытных, так и начинающих.
Приложения
Приложение 1 Инструкции процессоров 80x86 Это приложение является справочником по системе команд семейства про- процессоров Intel. В справочник включены команды, используемые процессо- процессорами 80386 и более поздними. Для описания форматов команд используется ряд аббревиатур, представленных в табл. П1.1. Сами команды описаны в табл. П1.2. Таблица П1.1. Аббревиатуры для описания команд Обозначение Краткое описание reg reg8, regl6, reg32 асе mem mem8, memi6, mem32 immed immed8, immedl6, immed32 Один из 8-, 16- или 32-разрядных регистров из списка: АН, AL, ВН, BL, СН, CL, АХ, ВХ, СХ, DX, SI, DI, ВР, SP, ЕАХ, ЕВХ, ЕСХ, EDX, ESI, EDI, EBP, ESP Регистр общего назначения, определяемый количеством битов AL, АХ ИЛИ ЕАХ Операнд в памяти Операнд в памяти, определяемый количеством битов Непосредственный операнд Непосредственный операнд с определенным количеством битов label Код операции ааа aad aam aas Метка Операнды Таблица П1.2. Система команд Функция ASCII-коррекция после сложения ASCII-коррекция перед делением ASCII-коррекция после умножения ASCII-коррекция после вычитания
482 Приложения Таблица П1.2 (продолжение) Код операции adc add and bsf, bsr bt, btc, btr, bts call Операнды reg, reg mem, reg reg, mem reg, immed mem, immed ace, immed reg, reg mem,.reg reg, mem reg, immed mem, immed ace, immed reg, reg mem, reg reg, mem reg, immed mem, immed ace, immed regl6, regl6 regl6, meml6 reg32, reg32 reg32, mem32 regl6, immed8 regl6, regl6 meml6, immed8 meml6, regl6 label reg meml6 mem32 Функция Сложение с переносом Сложение Логическое "И" Сканирование битов Проверка битов Вызов процедуры
Приложение 1. Инструкции процессоров 80x86 483 Таблица П1.2 (продолжение) Код операции cbw cdq clc eld cli cmc emp emps, empsb, empsw, cropsd cwd daa das dec div idiv imul in inc int iret jCONDITION jmp lahf Ids, lesrifs, lgs, lss Операнды mem, mem reg mem reg mem reg mem reg mem ace, immed reg mem label label Функция Преобразование байта в слово Преобразование двойного слова в учетверенное Сброс флага переноса Сброс флага направления Сброс флага прерывания Инвертирование флага переноса Сравнение операндов Сравнение строк Преобразование слова в двойное слово Десятичная коррекция после сложения Десятичная коррекция после вычитания Декремент Деление без знака Деление целых чисел со знаком Умножение целых чисел со знаком Ввод из порта Инкремент Генерирование программного прерывания Возврат из прерывания Переход, если выполнено условие CONDITION Безусловный переход Загрузка флагов в АН Загрузка дальнего указателя
484 Приложения Таблица П1.2 (продолжение) Код операции lea lods, lodsb, lodsw, lodsd loop loope, loopz 'loopne, loopz mov movs, movsb, movsw, movsd mul neg пор not or Операнды reg, mem mem label label label reg, reg mem, reg reg, mem reg, immed mem, immed mem, mem reg mem reg mem reg mem reg, reg mem, reg reg, mem reg, immed mem, immed ace, immed Функция Загрузка текущего адреса Загрузка строки в аккумулятор Цикл: декремент регистра СХ и переход на метку, если сх больше 0 Цикл, если равно 0: декремент регистра сх и переход на метку, если сх больше 0 и флаг нуля установлен Цикл, если не равно 0: декремент регистра сх и переход на метку, если сх больше 0 и флаг нуля сброшен Пересылка операндов Пересылка строк Умножение целых чисел без знака Изменение знака операнда Отсутствие операции: не производит никаких действий, используется для задержек во временных циклах Логическая функция "НЕ", инвертирует каж- каждый бит операнда Логическое "ИЛИ"
Приложение 1. Инструкции процессоров 80x86 485 Таблица П1.2 (продолжение) Код операции out POP рора, popad popf, popfd push pusha, pushad pushf, pushfd rcl rcr rep repCONDITION ret retn Операнды imraed, ace DX, ace regl6 reg32 menu 6 mem32 regie reg32 meml 6 mem32 reg, immed8 feg, CL mem, immed8 mem, CL reg, immed8 reg, CL mem, immed8 mem, CL immed8 Функция Вывод в порт Извлечение операнда из стека Извлечение из стека всех регистров общего назначения (рора — 16-разрядных, popad — 32-разрядных) Извлечение флагов из стека Помещение операнда в стек Помещение в стек всех регистров Помещение регистра флагов в стек Циклический сдвиг операнда влево через флаг переноса Циклический сдвиг операнда вправо через флаг переноса Повторение команды строковых примитивов, используя регистр СХ как счетчик Повторение команды строковых примитивов по условию Возврат из процедуры Возврат из процедуры с восстановлением стека. Непосредственный операнд опреде- определяет значение, которое должно быть добав- добавлено к регистру-указателю стека
486 Приложения Таблица П1.2 (продолжение) Код операции rol ror sahf sal sar sbb seas, scasb, scasw, scasd setCONDITION shl Операнды reg, iramed8 reg, CL mem, immed8 mem, CL reg, immed8 reg, CL mem, immed8 mem, CL reg, immed8 reg, CL mem, immed8 mem, CL reg, immed8 reg, CL mem, immed8 mem, CL reg, reg mem, reg reg, mem reg, immed mem, immed mem reg8 mem8 reg, immed8 reg, CL mem, immed8 mem, CL Функция Циклический сдвиг влево Циклический сдвиг вправо Загрузка регистра флагов из регистра АН Арифметический сдвиг влево Арифметический сдвиг вправо Вычитание с заемом Сканирование строки путем сравнения значе- значения элементов со значением в аккумуляторе Установка операнда по условию: если за- заданное условие истинно, то байт-получатель устанавливается в 1, если ложно — в 0 Логический сдвиг влево
Приложение 1. Инструкции процессоров 80x86 487 Таблица П1.2 (продолжение) Код операции shr stc std sti stos, stosb, stosw, stosd sub test wait xchg xlat, xlatb Операнды reg, irnmed8 reg, CL mem, immed8 mem, CL mem reg, reg mem, reg reg, mem reg, immed mem, immed ace, immed reg, reg mem, reg reg, mem reg, immed mem, immed ace, immed reg, reg mem, reg reg, mem mem Функция Логический сдвиг вправо Установка флага переноса Установка флага направления Установка флага прерывания Сохранение содержимого аккумулятора в ячейке памяти, принадлежащей буферу строки Вычитание Проверка отдельных битов операнда- получателя с соответствующими битами операнда-приемника: выполняет операцию логического "И" и устанавливает флаги в соответствии с результатом Приостановка работы процессора Обмен содержимого операнда-отправителя и операнда-получателя Использование значения в регистре AL как индекса таблицы, на которую указывает со- содержимое регистра вх
488 Приложения Таблица П1.2 (окончание) Код операции хог Операнды reg, reg mem, reg reg, mem reg, immed mem, immed ace, immed Функция Логическое исключающее "ИЛИ"
Приложение 2 Описание CD На прилагающемся к книге CD записаны примеры программ. Примеры раз- размещены в каталогах Chaptern, где п — номер главы. Каталоги содержат файлы проектов и исходные тексты программ, разработанные в среде Microsoft Visual C++ .NET 2003. Для компиляции исходных текстов программ необходимо, чтобы на персо- персональном компьютере было установлено следующее программное обеспечение: ? макроассемблер MASM версии 6.14 и выше. Можно использовать сво- свободно распространяемый пакет MASM32 версий 7 или 8; ? среда разработки Microsoft Visual C++ .NET 2003. Желательно также ус- установить последние пакеты обновлений.
Список литературы 1. Жуков А. В., Авдюхин А. А. Ассемблер. — СПб.: БХВ-Петербург, 2002. 2. Вильяме А. Системное программирование в Windows 2000 для профес- профессионалов. — СПб.: Питер, 2001. 3. Ирвин К. Язык ассемблера для процессоров Intel. 3-е изд./Пер. с англ. — М.: Издательский дом "Вильяме", 2002. 4. Магда Ю. С. Ассемблер. Разработка и оптимизация Windows-приложе- Windows-приложений. — БХВ-Петербург, 2003. 5. Оберг Р. Дж., Торстейнсон П. Архитектура .NET и программирование с помощью Visual C++: Пер. с англ. — М.: Издательский дом "Вильяме", 2002. 6. Паппас К., Мюррей У. Эффективная работа: Visual C++ .NET. — СПб.: Питер, 2002. 7. Саймон P. Windows 2000. API. Энциклопедия программиста: Пер. с англ. — Киев; СПб.: ООО "ДиаСофтЮП", 2002. 8. Харт Дж. М. Системное программирование в среде Win 32. 2-е изд./Пер. с англ. — М.: Издательский дом "Вильяме", 2001. 9. Шилдт Г. Полный справочник по С. 4-е изд./Пер. с англ. — М.: Изда- Издательский дом "Вильяме", 2002.
Предметный указатель в ВТВ44 С COFF 161 О OMF 161 Адрес процедуры 16 Алгоритм 12 Алгоритмизация 10 Арифметика: с насыщением 57 с циклическим переполнением 57 База данных 10 Библиотека: динамической компоновки 234, 247 импорта 248 статическая 248 Библиотека импорта 252 В Векторизация 306 Встроенные средства оптимизации 9 Д Декларация 162 Декорирование имени 160 Дескриптор, тсонтекста устройства отображения 182 Динамическая загрузка 242 Диспетчер служб SCM 454 Драйвер устройства 10 и Интерфейс пользователя 10 к Команды: безусловных переходов 44, 48 строковые 88, 92, 101 строковых примитивов 52, 87, 89, 142 условных переходов 39 Компилятор 9 Конкатенация 94, 96 Кэширование 14 м Массивы данных 87 Многопоточность 16, 381, 390 о Объединение 198 Объем памяти 10 Оконная процедура 190
492 Предметный указатель Операнд 18 Оператор 16 получения адреса 293 присваивания 135 раскрытия ссылки 293 сравнения 131 условного перехода 131 Операции с плавающей точкой 15, 62, 344 Оптимизация 9, 12, 25, 62, 83, 271, 306 п Подпрограмма 16, 107 Поток 390 Потоковое расширение SSE 58 Приложение: клиент-серверное 10 процедурно-ориентированное 21 сетевое 10 Программа реального времени 10 Профайлер 14 Процедура 16, 107 Разворачивание цикла 32, 34 Системная: служба 10, 453 шина 11 Скалярный режим вычислений 59 Собственные функции 57, 60, 308, 346 Сопроцессор 27, 62, 171, 290 Стек 17, 109 Строка с завершающим нулем 88, 176,375 Структура 197 Таймер ожидания 394, 423 у Указатель 176 Ф Файл описания экспортируемых функций 252 Флаг: направления 88 состояния 72 Функция: обратного вызова 181, 190 ожидания 394 Ц Цикл 15