Федеральное агентство по образованию ТОМСКИЙ ГОСУДАРСТВЕННЫЙ УНИВЕРСИТЕТ Факультет информатики Кафедра прикладной информатики ДОПУСТИТЬ К ЗАЩИТЕ В ГАК зав. кафедрой прикладной информатики, доктор технических наук, профессор _____________ С.П. Сущенко «____» июня 2006 г. Хряпова Надежда Петровна Современные методы оптимизации программной реализации алгоритма арифметического кодирования. Дипломная работа Научный руководитель, асс. каф. АСУ, ТУСУР А.В. Солонский Исполнитель, студ. гр. 1412 Н.П. Хряпова Электронная версия работы помещена в электронную библиотеку. Файл Администратор Томск – 2006 Реферат. Дипломная работа 35с., 12 рис., 10 табл., 12 источников. АРИФМЕТИЧЕСКОЕ КОДИРОВАНИЕ, ПРОФИЛИРОВАНИЕ, INTEL VTUNE. МЕТОДЫ ОПТИМИЗАЦИИ, Объекты исследования – алгоритм арифметического кодирования, программная реализация алгоритма арифметического кодирования, методы оптимизации. Цель работы - исследование методов оптимизации программных реализаций актуальных алгоритмов. Методы исследования - теоретический и экспериментальный. Основные результаты – синтез методов оптимизации для программных реализаций алгоритма арифметического кодирования. 2 Оглавление. Введение………………………………………………………………………………………….4 1. Описание предметной области……………………………………………………………...5 1.1. Обзор метода арифметического кодирования……………………………………….5 1.1.1. Идея метода арифметического кодирования…………………………………..5 1.1.2. Детали реализации метода………………………………………………………7 1.2. Понятие оптимизации программ………………………………………………….……9 1.2.1. Цели и задачи профилировки………………………………………………….10 1.2.2. Основные правила оптимизации………………………………………………11 1.2.3. Алгоритмические приемы оптимизации…………………………………...…12 1.2.4. Машинно-зависимые приемы оптимизации………………………………….15 1.3. Стратегия оптимизации……………………………………………………………….15 1.3.1. Критерий оптимизации……………………………………….………..………15 1.3.2. Факторы, влияющие на быстродействие……………………….………..……15 1.3.3. Процесс оптимизации…………………………………………….……...……..16 1.3.3.1. Создание программной реализации……………………….…….…...…16 1.3.3.2. Анализ области применения……………………………….…….….…..16 1.3.3.3. Профилирование и анализ узких мест………………………..….……..16 1.3.3.4. Оптимизация кода и архитектуры…………………………….….….….17 1.3.3.5. Циклическое тестирование……………………………………..……….17 1.3.3.6. Интеграция локальных улучшений в адаптивную оптимизированную систему………………..............…………………17 1.3.3.7.Краш – тестирование…………………………………………..………...…17 2. Методы оптимизации программы арифметического сжатия……………………………18 2.1. Методы оптимизации реализованного кода арифметического кодирования....................................................................................................................18 2.1.1. Создание арифметического кодека……………………………………...….…18 2.1.1.1.Реализация арифметического кодека (руководство программиста)…................................................................….18 2.1.1.2.Функциональные возможности программы (руководство пользователя)...........................................................................19 2.1.2. Профилирование программы и выбор методов оптимизации ….………..…19 2.1.3. Сравнительный анализ времени работы оригинального и оптимизированного кода……………………………………….………...…….24 2.2. Методы оптимизации референсного кода арифметического сжатия........................25 2.2.1. Обзор референсной программы арифметического кодирования....................26 2.2.2. Профилирование программы и выбор методов оптимизации........................27 2.2.3. Сравнительный анализ времени выполнения референсного и оптимизированного кода.....................................................................................30 Заключение ……………………………………………………………………………………..32 Список использованных источников………………………………………………………….33 Приложение А. Доказательство декодирующего неравенства ……………………………..34 3 Введение. Современное общество использует цифровой вид представления информации во многих сферах жизнедеятельности. Большой объем информации требует большой протяженности и пропускной способности каналов передачи данных. На данный момент развития информационной инфраструктуры, существующие каналы не справляются с требуемым траффиком. Следовательно, задача сжатия данных является актуальной во многих приложениях обработки и передачи информации. Все стандарты сжатия данных в той или иной мере используют алгоритмы энтропийного кодирования. Под энтропийным кодированием понимается кодирование, при котором энтропия сжатых данных совпадает с энтропией исходного источника и по сжатым данным можно полностью восстановить исходную информацию. Такой подход принято называть сжатием без потерь. Это сжатие, например, применяется в дистрибутивах программных продуктов. Это наиболее изученная область сжатия данных. Она является весьма важной, поскольку большинство методов компрессии самых разных типов цифровой информации часто используют на определенных стадиях алгоритмы сжатия без потерь. Можно сказать, что компрессия без потерь является экстремальным случаем сжатия, при котором энтропия данных остается неизменной. Одним из наиболее современных и передовых энтропийных алгоритмов сжатия является семейство алгоритмов арифметического сжатия. Они позволяют достичь коэффициента сжатия, наиболее близкого к максимальному. Ценой за высокие показатели сжатия является трудоёмкость алгоритмов, и, следовательно, их высокая требовательность к вычислительным ресурсам. Арифметическое кодирование используется для сжатия контекстных последовательностей данных в последних стандартах сжатия видеоизображений JPEG2000, H.264 (MPEG-4 p.10, AVC) итд. Растущие объемы данных и развитие сервисов информационных цифровых ресурсов также накладывают дальнейшие ограничения на работу алгоритмов - работа в реальном времени. Телефония, телевидение, видеоконференции требуют обработки информации наиболее быстрым способом. Такие требования послужили толчком к развитию направления в разработке программного обеспечения, которое сейчас востребовано на рынке софтверных услуг, – оптимизация программ. Множество компиляторов, операционных систем, типов процессоров и периферийных устройств, наконец, множество DSP устройств накладывают специфические условия на процесс оптимизации программ. Так называемая, универсальная оптимизация (алгоритмическая), хорошо описанная в научной литературе дает недостаточные результаты, все больше становится востребованная контекстная оптимизация, под определенный тип процессора, кэша, памяти, набор инструкций итд. Настоящая работа посвящена исследованию методов оптимизации программы классической теории арифметического кодирования с использованием приемов алгоритмической оптимизации, а также с использованием особенностей компилятора MSVC .net и набора инструкций Pentium 4 (SSE2), что в свете вышеописанного материала и обозначенных ограничений является актуальной задачей. 4 1. Описание предметной области Избыточность – центральное понятие в теории сжатия информации. Любые данные с избыточной информацией можно сжать, данные, в которых нет избыточности, сжать нельзя. Значение термина избыточность лучше пояснить с помощью понятия энтропии, описанного в [1, глава 2]. В теории информации под энтропией символа а, имеющего вероятность P, подразумевается количество информации, содержащейся в а, которая равна − P log 2 P . Если символы некоторого алфавита с символами от a1 до a n имеют вероятности от P1 до Pn соответственно, то энтропия всего алфавита равна n ∑ − P log i 2 Pi . Для строки символов алфавита энтропия определяется аналогично. С помощью понятия энтропии теория информации показывает, как вычислять вероятности строк символов алфавита, и предсказывает ее наилучшее сжатие, то есть наименьшее в среднем число бит, необходимое для представления этой строки символов. i 1.1 Обзор метода арифметического кодирования Арифметическое кодирование известно сегодня как один из наиболее эффективных методов сжатия данных, который применим для большого класса источников информации. Основная идея арифметического кодирования была сформулирована Элайесом еще в начале 60-х годов. Однако, первый шаг в направлении практической реализации этой идеи был сделан независимо Риссаненом и Паско в 1976 г. Преимущество арифметического кода по отношению к другим методам заключается в том, что он позволяет достичь произвольно низкой избыточности на символ источника . В последние годы задача кодирования источников с большими алфавитами становится все более актуальной. С точки зрения воздействия на скорость кодирования алфавит из 2 8 = 256 символов, который широко используется при сжатии компьютерных файлов, может считаться уже достаточно большим. 1.1.1 Идея метода арифметического кодирования Этот метод основан на идее преобразования входного потока в одно дробное число[2,3]. Результатом работы арифметического компрессора получается число, меньшее 1 и большее либо равное 0. Из этого числа можно однозначно восстановить последовательность символов, из которых оно было построено. Особенностью арифметического кодирования является то, что для отображения последовательности символов в потоки цифр используются относительные частоты символов. Алгоритм читает входной файл символ за символом и добавляет биты к сжатому файлу. На первом шаге следует вычислить или, по крайней мере, оценить частоты возникновения каждого символа алфавита, но, если программа может получить хорошие оценки из другого источника, первый проход можно опустить. Каждому символу ставится в соответствие интервал в промежутке от 0 до 1 и частота его появления во входном файле. Длина интервала для символа равна частоте его появления в сообщении. Часто встречающимся буквам ставятся в соответствие большие интервалы. На отображение затрачивается меньше десятичных цифр, чем на отображение редко встречающихся букв. Положение интервала вероятности каждого символа не имеет значения. Важно только то, чтобы и кодер, и декодер располагали символы по одинаковым правилам. Идея арифметического кодирования заключается в следующих шагах: 5 1. Задать «текущий интервал» [0;1). 2. Повторить следующие действия для каждого символа s входного файла. Разделить текущий интервал на части пропорционально вероятностям каждого символа. Выбрать подынтервал, соответствующий символу s, и назначить его новым текущим интервалом. 3. Когда весь входной файл будет обработан, выходом алгоритма объявляется любая точка, которая однозначно определяет текущий интервал. Формулы, организующие классическую модель арифметического кодирования, приведены в [4, глава 1]. Процесс кодирования начинается инициализацией двух переменных Low и High и присвоением им 0 и 1, соответственно. Они определяют интервал [Low, High). По мере поступления и обработки символов, переменные Low и High начинают сближаться, уменьшая интервал. На каждом шаге переменные пересчитываются по правилу: Range = High − Low (1), High = Low + Range * HighRange( X ) (2), Low = Low + Range * LowRange( X ) (3), где Range обозначает длину интервала символа Х, HighRange(X) и LowRange(X) обозначают верхний и нижний конец области символа Х. Когда обработаны все символы входного файла, на выход алгоритма поступает число, принадлежащее получившемуся интервалу. Декодер работает в обратном порядке. Определяя, в каком интервале находится число, декодер распознает символ алфавита. Далее удаляется эффект этого символа из кода с помощью преобразования: Code = Code − LowRange( X ) Range (4), где LowRange(X) обозначают нижний конец области символа Х, Range обозначает длину интервала символа Х. В алгоритме декодер останавливается при обнаружении специального символа EOF (конец файла), но если размер входного файла известен, то можно его кодировать без этого символа. Таким образом, при обработке числа после определения символа, в интервал которого оно попадает, этот символ выдается как раскодированный, а его влияние на число устраняется действиями, обратными действиям при кодировании. В качестве примера рассмотрим кодирование и декодирование строки «BILL GATES». Интервалы вероятностей символов приведены в таблице 1.1. Шаги кодирования и декодирования - в таблицах 1..2 и 1.3 соответственно. Таблица 1.1. Интервалы вероятностей для символов сообщения. Символ Пробел А В Е G Вероятность 1/10 1/10 1/10 1/10 1/10 6 Интервал [0.00, 0.10) [0.10, 0.20) [0.20, 0.30) [0.30, 0.40) [0.40, 0.50) I L S T 1/10 2/10 1/10 1/10 [0.50, 0.60) [0.60, 0.80) [0.80, 0.90) [0.90, 1.00) Таблица 1.2. Шаги алгоритма арифметического кодирования при обработке сообщения. Очередной Символ Нижняя Граница 0.0 0.2 0.25 0.256 0.2572 0.25720 0.257216 0.2572164 0.25721676 0.257216772 0.2572167752 В I L L Пробел G А Т Е S Верхняя Граница 1.0 0.3 0.26 0.258 0.2576 0.25724 0.257220 0.2572168 0.2572168 0.257216776 0.2572167756 Таким образом, число 0.2572167752 однозначно кодирует сообщение “BILL GATES”. Таблица 1. 3. Шаги работы алгоритма арифметического декодирования. Число Символ Нижняя Граница Верхняя Граница Интервал 0.2572167752 0.572167752 0.72167752 0.6083876 0.041938 0.41938 0.1938 0.938 0.38 0.8 0.0 В I L L Пробел G А Т Е S 0.2 0.5 0.6 0.6 0.0 0.4 0.2 0.9 0.3 0.8 0.3 0.6 0.8 0.8 0.1 0.5 0.3 1.0 0.4 0.9 0.1 0.1 0.2 0.2 0.1 0.1 0.1 0.1 0.1 0.1 1.1.2 Детали реализации метода В описанном выше алгоритме предполагается, что в переменных Low и High хранятся числа с неограниченной точностью. Любое практическое применение арифметического кодирования основывается на операциях с целыми числами. Использование целочисленной арифметики приведено в [1, 4]. Ранее было отмечено, что при работе алгоритм отслеживает верхнюю и нижнюю границы возможного выходного кода. После старта нижняя граница установлена в 0.0, а верхняя — в 1.0. Первое упрощение для работы с целочисленной арифметикой — это заменить 1 на 0.999... Это упрощает и кодирование, и декодирование. 7 Будем располагать в целочисленных регистрах дробные части значений границ, выравнивая их влево. Использование при кодировании целочисленных регистров приведено в таблице 1.4. Как только самые левые цифры Low и High становятся одинаковыми, они уже не меняются в дальнейшем. Поэтому можно выдвигать эти числа за пределы переменных Low и High и сохранять их в выходном файле. После сдвига цифр мы будем справа дописывать 0 в переменную Low, а в переменную High – цифру 9. На каждом шаге кодирования и декодирования Low и High обновляются, приближаясь друг к другу, и при совпадении старших цифр переменные Low, High и Code сдвигаются на одну позицию влево. В переменную Code справа записывается следующая цифра из закодированного файла. Скорректируем вышеописанные формулы: Range = High − Low + 1 (5), High = Low + Range * HighRange( X ) − 1 (6), Low = Low + Range * LowRange( X ) (7), В описанном раннее алгоритме текущий символ для декодирования возможно определить, просто просматривая таблицу интервалов вероятностей символов в данном сообщении. В целочисленной реализации интервал вероятностей определяется не как диапазон от 0.0 до 1.0, а как разница между текущими значениями верхней и нижней границ. Целочисленная реализация арифметического декодирования использует четыре шага для декодирования каждого символа. На первом шаге определяется текущий интервал вероятностей. На втором шаге текущее значение регистра кода масштабируется на основе значений текущего интервала вероятностей, регистров кода и границ. На третьем шаге на основе масштабированного значения регистра кода определяется очередной символ восстановленного сообщения. На четвертом шаге код декодированного символа удаляется из входного потока. Для того чтобы декодеру распознать символ, на каждом шаге вычисляется переменная: Index = number * (Code − Low + 1) − 1 High − Low + 1 (8), где number – количество символов в файле. Переменная Index округляется до ближайшего целого. Для удобства работы декодера заводится специальный массив CumFreq (накопленные частоты). Значение CumFreq для символа равно сумме частот всех предыдущих символов. Путем сравнения значений вычисленной переменной и массива CumFreq определяется символ, в интервал которого попадает переменная Index. Доказательство верного распознавания символов приведено в приложении А. Таблица 1.4. Кодирование с использованием целочисленной арифметики. Начальное состояние Кодируем 'В' (0.2-0.3) Выдвигаем 2 Кодируем 'I' (0.5-0.6) Выдвигаем 5 Верхняя Граница 99999 29999 99999 59999 99999 Нижняя Граница 00000 20000 00000 50000 00000 8 Интервал 100000 100000 100000 Текущий код сообщения .2 .2 .25 Кодируем 'L' (0.6-0.8) Кодируем 'L' (0.6-0.8) Выдвигаем 7 Кодируем ' ' (0.0-0.1) Выдвигаем 2 Кодируем 'G' (0.4-0.5) Выдвигаем 1 Кодируем 'А' (0.1-0.2) Выдвигаем 6 Кодируем 'Т' (0.9-1.0) Выдвигаем 7 Кодируем 'Е' (0.3-0.4) Выдвигаем 7 Кодируем 'S' (0.8-0.9) Выдвигаем 5 Выдвигаем 2 Выдвигаем 0 79999 75999 59999 23999 39999 19999 99999 67999 79999 79999 99999 75999 59999 55999 59999 60000 72000 20000 20000 00000 16000 60000 64000 40000 76000 60000 72000 20000 52000 20000 20000 40000 40000 40000 40000 40000 40000 40000 .25 .25 .257 .257 .2572 .2572 .25721 .25721 .257216 .257216 .2572167 .2572167 .25721677 .25721677 .257216775 .2572167752 .25721677520 Для алгоритма арифметического кодирования возможна ситуация зацикливания. В процессе работы алгоритма может возникнуть случай, когда, например, верхняя граница станет равна 70000, а нижняя — 69999. Интервал между ними так мал, что все последующие итерации не изменят значений границ, а поскольку старшие цифры границ не совпадают, то алгоритм не будет выдавать цифр кода, то есть зациклится. Для того чтобы избежать этой ситуации, немного изменим алгоритм. В [5] приведен метод устранения ситуации зацикливания. Если две старшие цифры границ не совпадают, то, если они отличаются на единицу, необходимо проверить следующие по значимости цифры границ. Если они равны 0 у верхней границы и 9 у нижней, значит, алгоритм на пути к зацикливанию и надо принимать меры. Необходимо осуществить следующие действия: удалить вторые по значимости цифры границ и остальные менее значимые сдвинуть влево по описанным правилам. Старшие цифры останутся на месте. Затем увеличить специальный счетчик, чтобы запомнить событие о выброшенных цифрах. Нужно продолжать те же действия до тех пор, пока старшие цифры границ не совпадут. Затем в код сообщения помещается совпавшая старшая цифра, а за ней равное счетчику количество девяток или нулей, в зависимости от того, к цифре верхней или нижней границы сошлись старшие цифры. Для алгоритма арифметического кодирования возможна ситуация отрицательного переполнения. Предположим, что Low и High настолько близки друг к дpугу, что опеpация кодирования пpиводит их одному целому числу. В этом случае дальнейшее кодирование продолжать невозможно. Следовательно, кодировщик должен следить за тем, чтобы интервал [Low; High) всегда был достаточно широк. Простейшим способом для этого является обеспечение ширины интервала не меньшей максимального значения суммы всех накапливаемых частот. 1.2 Понятие оптимизации программ Оптимизация программы - это улучшение какой-либо характеристики программы, называемой критерием оптимизации. Оптимизация программ в основном выполняется по двум основным критериям: быстродействие и объему используемых данных. В данной работе основным и единственным критерием возьмем увеличение быстродействия, так 9 как описанный выше метод вычислительным ресурсам. арифметического кодирования требователен к 1.2.1 Цели и задачи профилировки Производительность приложения определяется самым узким его участком, поэтому в первую очередь нужно определить части программы, на которых будет выполняться оптимизация. Процесс оптимизации следует начать с профилировки программы. Согласно [6, глава 1] профилировкой называют измерение производительности как всей программы, так и отдельных ее фрагментов, с целью нахождения «горячих точек» - тех участков программы, на выполнение которых расходуется наибольшее количество времени. При этом важно отметить, что ликвидация не самых «горячих» точек программы, практически не увеличивает ее быстродействия. Основная цель профилировки – это исследование характера поведения приложения во всех его точках. В зависимости от степени детализации в качестве «точки» рассматривается как отдельная машинная команда, так и целая конструкция высокого языка - функция, цикл, процедура. Сложная программа состоит из большого числа функций. Как утверждалось ранее, нет смысла оптимизировать их все – трудоемкость такого подхода будет выше выгод, полученных от оптимизации программы целиком. Для начала необходимо локализовать участки кода с максимальной вычислительной трудоемкостью. Участки программы, которые в наибольшей степени влияют на ее производительность, в силу наиболее частого выполнения или своей ресурсоемкости называются критическим кодом. В поиске критического кода программы используют профайлеры (профилировщики) – специальные программы, которые измеряют временные затраты на выполнение участков кода программы. Профилировщики представляют возможности для оптимизации программ. К таким программам относятся Intel VTune, AMD Code Analyst, profile.exe и множество других. Наиболее мощным из них на сегодняшний день является пакет от Intel, который кроме собственно профилировщика включает в себя программные эмуляторы всех процессоров Intel, а так же содержит огромное количество статей (более 3000 страниц) для формирования советов по профилировке и оптимизации программ. В данной работе использован профилировщик Intel VTune v.7.2. Эта программа позволяет измерить время обработки каждой команды и вывести полную статистику о состоянии процессора при выполнении каждой команды. Одной из важнейших возможностей VTune является поиск «горячих точек», то есть тех участков программы, которые при минимальном объеме выполняются большую часть времени работы программы. В отличие от остальных профилировщиков, VTune не только показывает эти точки в программе, но и объясняет причину их появления. VTune использует относительную меру времени – такты процессора (Clockticks), так как это наиболее точное время, которое можно измерить с наименьшей погрешностью. Большинство современных профилировщиков поддерживают следующий набор базовых операций: • • • • • определение общего времени исполнения каждой точки программы; определение удельного времени исполнения каждой точки программы; определение причины и/или источника конфликтов и пенальти; определение количества вызовов той или иной точки программы; определение степени покрытия программы. В многозадачной среде (операционная система Windows) никакая программа не владеет всеми ресурсами системы единолично и вынуждена делить их с остальными задачами. Поэтому скорость выполнения профилируемой программы непостоянна и находится в тесной зависимости от "окружающей среды". На практике разброс результатов измерений обычно достигает 10%-15%, если же параллельно с 10 профилировкой исполняются интенсивно нагружающие систему задачи, то разброс будет значительно выше. Существую два подхода к измерению результатов времени выполнения. Данные методы приведены в [6]. Первый предполагает осуществлять в каждом сеансе профилировки несколько контрольных прогонов и затем выбирать замер с наименьшим временем выполнения. Измерения производительности - это не совсем обычные инструментальные измерения и типовые правила метрологии здесь неуместны. Процессор никогда не ошибается и каждый полученный результат точен, но в той или иной степени искажен побочными эффектами. Никакие побочные эффекты не приводят к тому, что программа начинает исполняться быстрее, поэтому прогон с минимальным временем исполнения и представляет собой измерение, в минимальной степени испорченное побочными эффектами. Второй способ вычисляет наиболее типичное время выполнения, как время выполнения в реальных условиях. Такое время носит название среднеминимального времени исполнения. Осуществляется N повторов программы, затем отбрасывается N/3 максимальных и N/3 минимальных результатов замеров, а для оставшихся N/3 замеров находится среднее арифметическое. Величина N варьируется в зависимости от конкретной ситуации, но обычно хватает 9-12 повторов, так как большее количество уже не увеличивает точности результатов. 1.2.2 Основные правила оптимизации В [6] приведен ряд правил оптимизации: Прежде чем приступать к оптимизации, необходимо иметь надежно 1. работающий неоптимизированный вариант. Основной прирост оптимизации дает не учет особенностей системы, а 2. алгоритмическая оптимизация. Обнаружив профилировщиком узкие места необходимо произвести 3. оптимизацию в рамках языка высокого уровня. Убедившись, что все возможное для увеличения быстродействия сделано, а результат неудовлетворителен, стоит обратиться к ассемблерному листингу. Возможны ситуации, где в неудовлетворительной производительности кода виноваты процессор или подсистема памяти, а не компилятор. Лишь после анализа листинга следует приступать к ассемблерной оптимизации. Исключением к вышеописанным пунктам можно считать операции с векторизируемыми данными, которые на данный момент весьма слабо конвертируются компилятором в SIMD инструкции. Прежде чем приступать к оптимизации, необходимо убедится, что количество прогонов программы достаточно велико для маскировки накладных расходов первоначальной загрузки. Оптимизация начинается с выделения профилировщиком критического кода и анализа его неоптимальности. Причем каждое внесенное изменение необходимо проверять профилировщиком. После завершения оптимизации локального фрагмента программы, необходимо выполнить контрольную профилировку всей программы целиком на предмет обнаружения новых появившихся «горячих точек». Проводя оптимизацию, не следует забывать о ее цели. Фактически идеал недостижим, поэтому оптимизацию следует завершать когда: 1. Производительность программы признана удовлетворяющей; 2. В программе отсутствуют «горячие точки», то есть количество инструкций равномерно распределено по все программе, и дальнейшая оптимизация потребует переписывания большого количества кода; 3. Сложность алгоритма настолько высока, что не представляется возможным дальнейшая оптимизация без значительных временных затрат; 11 4. Критическая зависимость от платформы, когда дальнейшая машинно-зависимая оптимизация приведет к потере совместимости с одной из целевых платформ. Ко всем нижесказанным методам оптимизации алгоритма арифметического кодирования предъявляются следующие требования: оптимизация должна быть по возможности максимально машинно1. независимой и переносимой на другие платформы (операционные системы) без существенных потерь эффективности. оптимизация не должна увеличивать трудоемкость разработки (в том 2. числе тестирования) приложения более чем на 10-15%. оптимизирующий алгоритм должен давать выигрыш не менее чем на 203. 25% в скорости выполнения. оптимизация не должна допускать безболезненное внесение изменений. 4. 1.2.3 Алгоритмические приемы оптимизации Приемы оптимизации программы можно разделить на алгоритмические и машинно-зависимые способы. Данные способы приведены в [7, 8]. В случае использования алгоритмических приемов оптимизации используются различные математические и логические методы для улучшения параметров алгоритма. Такой способ оптимизации невозможно автоматизировать, успешность его применения зависит от программиста. Способность программиста к алгоритмической оптимизации программы зависит от его понимания предметной области: владения им базовых концепций применяемых алгоритмов и особенностей предметной области программы. В первую очередь это замена алгоритмов на более быстродействующие. Часто бывает, что более простой алгоритм показывает низкую производительность по сравнению с более сложными. Тогда, возможна замена эквивалентных алгоритмов, например, замена Дискретного Преобразования Фурье на Быстрое Преобразование Фурье, замена пузырьковой сортировки массива на шелл-сортировку или быструю сортировку. В некоторых случаях возможна оптимизация программы за счет снижение точности. В зависимости от особенностей предметной области возможно уменьшить разрядность представления чисел или перейти от выполнения операций с числами с плавающей запятой к целым числам или числам с фиксированной запятой. На практике используется весьма широкий набор машинно-независимых оптимизирующих преобразований, что связано с большим разнообразием неоптимальностей. К ним относятся: • разгрузка участков повторяемости, • упрощение действий, • чистка программы, • экономия памяти и оптимизация работы с памятью, • реализация действий, • сокращение программы и другие методы. Разгрузка участков повторяемости. Это такой способ оптимизации, который состоит в вынесении вычислений из многократно исполняемых участков программы на участки программы, редко исполняемые. К этому виду преобразования относятся различные чистки зон, тел циклов и тел рекурсивных процедур, когда инвариантные по результату выполнения выражения, исполняемые при каждом прохождении участка повторяемости, выносятся из него. Если размещение осуществляется перед входом в участок повторяемости, то эту ситуацию называют чисткой вверх, если же за выходом из участка повторяемости, то чисткой вниз. 12 Упрощение действий. Этот способ оптимизации ориентирован на улучшение программы за счет замены групп (как правило, удаленных друг от друга) вычислений на группу вычислений, дающий тот же результат с точки зрения всей программы, но имеющих меньшую сложность. Удаление индуктивных переменных К таким преобразованиям относят удаление индуктивных переменных, что означает замену нескольких индуктивных переменных цикла одной индуктивной переменной, а также удаление индуктивных выражений из цикла. Замена сложных операций на более простые Особо важным преобразованием из этой группы является понижение силы операций, заменяющее в индуктивных вычислениях сложные операции на более простые. Операция деления или возведения в степень заменяется умножением, а умножение сложением. Пример: for (int i=1; i<n; i++) a[i] = a[i] +c*i; преобразуется в: int t = c; for (int i=1; i<n; i++) { a[i]=a[i]+t; t= t+c; } Исключение избыточных выражений – это замена вхождений выражений на переменную, значение которой совпадает со значением выражения. Во многих случаях ряд вычислений будет содержать идентичные подвыражения. Избыточность может возникнуть как в пользовательском коде, так и в адресных вычислениях, сгенерированных компилятором. Компилятор может сохранить однажды вычисленное подвыражение, а затем использовать этот результат в других вычислениях. Исключение общих подвыражений есть важное преобразование, и выполняется почти универсально. Однако здесь нужно следить за ценой исключения. Если запоминание промежуточных значений вызывает дополнительный разброс адресов памяти, такое преобразование понижает эффективность выполнения программы. Реализация действий. Это способ повышения быстродействия программы за счет выполнения определенных ее вычислений на этапе трансляции. Набор преобразований данного типа включает в себя следующие оптимизации: • подстановка или свертка констант, • распроцедуривание (открытая подстановка тела процедуры на место ее вызова), • втягивание констант, когда выражения, имеющие тождественно константные значения, заменяются на эти значения. Типичные программы содержат много констант и, втягивая их на протяжении программы, компилятор может выполнить существенный объем предвычислений. Более важно то, что втягивание констант открывает путь к другим оптимизациям. В дополнение к очевидным возможностям, таким как исключение мертвого кода, передача констант влияет на оптимизацию циклов, поскольку константы часто появляются в их теле. Пример: 13 int z = 2;................ int n=32*z; int c = 3; for (int i=1; i<n; i++) a[i] = a[i] +c; после втягивания констант: for (int i=1; i<64; i++) a[i] = a[i] +3; Чистка программы. Данный способ повышает качество программы за счет удаления из нее ненужных объектов и конструкций. Набор преобразований этого типа включает в себя следующие оптимизации: • удаление идентичных операторов, • удаление из программы операторов, недостижимых по управлению от начального, • удаление несущественных операторов, то есть операторов не влияющих на результат программы, • удаление процедур, к которым нет обращений, • удаление неиспользуемых переменных и другие. Существует много оптимизаций, предназначенные для выявления и удаления избыточности. Это относится, например, к удалению кода, инвариантного к циклу. Здесь исключаются недостижимые или бессмысленные операции. Вычисление недостижимо, если оно никогда не выполняется. Недостижимый код создается программистом или другим преобразованием. Например, если уже известно, что условное предложение истинно или ложно, одна ветвь условного предложения никогда не будет выполняться и соответствующий код можно исключить. Другим источником недостижимого кода является цикл, который не выполняет ни одной итерации. Экономия памяти и оптимизация работы с памятью Улучшения быстродействия возможно за счет уменьшения объема памяти, отводимой под информационные объекты программы в каждом ее исполнении. Например, замена стека локальных переменных или параметров, вовлекаемых в рекурсию, одинарной переменной. Когда интенсивный обмен с памятью неизбежен, то есть необходима оптимизация обработки больших массивов данных и потоковых алгоритмов, используются следующие рекомендации: • • • • • • • разворот циклов, читающих память; устранение зависимости по данным; чтение данных с шагом не меньшим 32 байт; группирование операции чтения памяти с операциями записи; комбинирование вычислений с доступом к памяти; отправление контроллеру памяти нескольких запросов одновременно; обработка данных двойными словами. Для достижения наивысшей производительности следует проектировать алгоритм программы так, чтобы все интенсивно обрабатываемые блоки данных целиком умещались в сверхоперативной памяти первого или второго уровней (кэше). В противном случае обмен с памятью существенно замедлит работу процессора. 14 1.2.4 Машинно-зависимые приемы оптимизации Машинно-зависимые используют особенности устройства и работы конкретной системы. Ярким примером машинно-зависимой оптимизации является векторизация операций, т.е. использование потоковых расширений процессора, таких как MMX (MultiMedia eXtensions), SSE (Streaming SIMD Extensions) и т.п. Машино-зависимую оптимизацию можно выполнять двумя различными способами. Первый способ основан на понимании работы кодогенератора компилятора, его алгоритма и рекомендуется для приложений, в которых компилятор выбирается в начале проекта и в дальнейшем не меняется. При использовании такого способа преобразуется исходный код программы, написанный на языке высокого уровня. Для тех проектов, в которых заранее не известен компилятор (OpenSource проекты, кроссплатформенные приложения) применятся другой способ, основанный на замещении ресурсоемких участков кода ассемблерными вставками. При такой оптимизации ухудшается переносимость кода на другие платформы. Машинно-зависимые способы оптимизации довольно хорошо автоматизируются и большую часть их выполняют оптимизирующие компиляторы. Однако всегда остаются моменты в программе, которые можно оптимизировать вручную. 1.3 Стратегия оптимизации 1.3.1 Критерии оптимизации Классическое определение понятия оптимизация заключает в себе нахождения решения системы дифференциальных неравенств с достижением минимума и/или максимума одного или нескольких критериев. В задачах математического программирования в качестве исследуемых критериев обычно принимаются ресурсы для решения задач (время, стоимость материалов и процессов). В задачах реального программирования под ресурсами задачи понимаются вычислительные ресурсы компьютера. Чаще всего, наиболее критичными выделяются время выполнения программы и объём памяти, требуемый для её функционирования. В связи с научно-техническим прогрессом, последнее требование в последнее время потеряло своё актуальность, в то время как необходимость в быстром выполнении программ значительно увеличилась. Сложность разрабатываемых программных комплексов растёт с опережением графика увеличения быстродействия процессоров. Таким образом, основным оптимизационным критерием в данной работе будет являться быстродействие. 1.3.2 Факторы, влияющие на быстродействие Следующим шагом после выделения оптимизационного критерия необходимо выделить спектр переменных величин, имеющих значимое влияние на время выполнения алгоритма. Очевидно, что он будет ограничен количеством организационных единиц, вовлеченных в процесс выполнения программы: программная реализация алгоритма, аппаратная часть, массив входных данных. Подробнее опишем каждую из трех составляющих. Программная реализация алгоритма 15 Программная реализация алгоритма состоит из синтаксических конструкций, описывающих работу алгоритма в терминах и категориях выбранного средства реализации. Выберем мерой определения степени оптимизации программной реализации количество тактов процессора (-ов) необходимое для обработки некоторой стандартной входной последовательности. Различные средства реализации (языки программирования и компиляторы) предлагают различную степень автоматизации процессов оптимизации, и в методических целях лучшим выбором будет язык низкой степени абстракции – С, asm и наиболее популярный компилятор Microsoft Visual C++ версии 6 и 7. Аппаратные данные В общем случае для выполнения программы необходим некоторый процессор общего или специального назначения, способный выполнять последовательность машинных команд, сгенерированных компилятором. Список устройств подходящих под вышеописанное определение довольно широк: телефоны, телевизоры, видеомагнитофоны, холодильники. Опять же в методических целях, ограничим этот список самым универсальным устройством – персональным компьютером. Наибольшим влиянием на производительность являются тактовая частота процессора, набор используемых инструкций, размер и быстродействие кэша и памяти. Входные данные Очевидно, что от количества и типа входных данных будет зависеть производительность алгоритма, поэтому, необходимо тщательным образом исследовать возможность обработки различных типов данных построенным алгоритмом и выбрать некоторые, наиболее характерные последовательности данных, которые и будут использоваться в тестах при оптимизации программы. 1.3.3 Процесс оптимизации Согласно изученной литературе [6, 9] в целях исследования будем использовать нижеописанную стратегию оптимизации. 1.3.3.1 Создание программной реализации Процесс оптимизации начинается с создания программной реализации алгоритма. Первым делом необходимо выбрать язык реализации и компилятор. На данный момент не существует альтернатив языку С/С++, так как он предоставляет большие возможности для отладки и построения рабочих конструкций на любом уровне абстракции с использованием языка assembler. 1.3.3.2 Анализ области применения Далее необходимо рассмотреть всё множество входных данных и построить классифицированную модель. Это необходимо для составления репрезентационного набора наборов тестовых данных. Необходимо составить статистику использования того или иного набора данных, ведь не секрет, что алгоритм будет себя вести по разному в зависимости от типа обрабатываемых данных и оптимизация может не быть универсальной, то есть улучшение в одном приведет к ухудшению в другом. В связи с этим будет логично ввести адаптивную подстройку под тип обрабатываемых данных или предоставить эту возможность пользователю. 16 1.3.3.3 Профилирование и анализ узких мест Следующим важнейшим этапом является анализ работы приложения на подобранном тестовом материале. Цель этого этапа выявление узких мест в архитектуре программной реализации. Профилирование, возможно, проводить с помощью простых временных замеров критических участков кода, а также с использованием современных инструментальных средств, которые не только помогают выявить узкие места, но и получить развернутое объяснение причин возникновения проблем. 1.3.3.4 Оптимизация кода и архитектуры Данный этап является наименее формализованным и наиболее творческим. После выявления узких мест необходимо принять решение, каким образом от них избавиться. Наиболее простым случаем представляется исправление явно неэффективного кода, но на практике с усложнением алгоритмов, явность неэффективного кода зачастую неявна. Сложные зависимости между структурами данных и операциями, выполняемыми над ними, приводят к сравнению работы инженера-оптимизатора с игрой в бирюльки, где надо постепенно распутывать сложный характер зависимостей, не повредив при этом рабочей функциональности. Если полученные результаты не удовлетворяют поставленным целям, то, возможно, требуется изменение архитектуры, заново группировать операции по функциональным группам с целью улучшить быстродействие. 1.3.3.5 Циклическое тестирование После выполнения очередного этапа или подхода в изменении кода необходимо проводить оценочные тесты быстродействия и корректности проведенных изменений. В системе контроля версий записывать все изменения, ибо неудачный подход в одной архитектуре может пригодиться после перестроения структуры системы. Неудачные попытки так же важны, как и удачные. 1.3.3.6 Интеграция локальных оптимизированную систему улучшений в адаптивную Как уже было сказано выше, результатом первого этапа оптимизации должен быть набор удачных-неудачных подходов, которые необходимо скомпоновать в одну систему, которая будет показывать на некотором заданном множестве тестовых данных наилучший в терминах поставленных целей результат. 1.3.3.7 Краш - тестирование Заключительным этапом является, так называемое, краш - тестирование. Насколько бы хорошо не была проведена оптимизация, наличие ошибок в продукте может привести к отрицательной динамике продаж продукта, поэтому наиглавнейшим критерием оптимизации является безошибочность и стабильность работы продукта. 17 2. Оптимизация программы арифметического сжатия 2.1 Методы оптимизации реализованного кода арифметического кодирования 2.1.1 Создание арифметического кодека В данной программе реализован арифметический кодек, использующий строгую вероятностную модель. Строгой моделью является та, где частоты символов текста в точности соответствуют предписаниям модели. 2.1.1.1 Реализация арифметического кодека (руководство программиста) Арифметический кодек был реализован на языке С в среде Microsoft Visual Studio v.7.0. В процессе разработки проекта Compress было создано 13 файлов. В приложении были использованы следующие типы данных: • • • • typedef struct SYMBOL { unsigned int code; // код символа int amount; // частота символа int CumFreg; // накопленная частота } symbol_t; typedef struct SYMBOL_TABLE { unsigned int table_type; // размер алфавита unsigned int dif_num; // число различных символов файла unsigned int number; // размер файла struct SYMBOL * symbols; // указатель на массив символов } symbol_table_t; typedef struct ENCODER { unsigned int low; // нижняя граница unsigned int high; // верхняя граница unsigned int value; // код } encoder_t ; typedef struct DECODER { unsigned int low; // нижняя граница unsigned int high; // верхняя граница unsigned int code; // выровненный код unsigned int value; // код } decoder_t; Список функций: • • • • • • • • • • process_input_parameters(…) – проверяет корректный ввод параметров в командной строке; process_reading(…) – чтение символов входного файла в буфер; process_writing (…) – запись из буфера в файл; enc_file(…) - создает заголовок сжатого файла: записывает размер кодируемого файла, число различных символов входного файла, размер алфавита и статистику символов; create_symbol_table(…) - создание экземпляра типа данных и его инициализация; symbols_in_file(…) – создает статистику символов кодируемого файла, вычисление массива накопленных частот; free_symbol_table(…) – освобождение памяти, выделяемой под экземмляр типа данных symbol_table_t; create_enc(…) - создание экземпляра типа данных encoder_t и его инициализация; encode_symbol(…) – вычисление нижней и верхней границы интервала для заданного символа; remember_number(…) – осуществляет сборку и запись кода в сжатый файл; 18 • • • • • • • • • • • encode(…) – реализует кодирование файла; free_enc(…) – освобождение памяти; create_dec(…) - создание экземпляра типа данных decoder_t и его инициализация; find_sym(…) – нахождение кода символа по вычисляемой переменной index путем сравнения ее с массивом накопленных частот; decode_symbol(…) - вычисление границ интервала для распознанного символа; read_code(…) – чтение кода из закодированного файла; decoder(…) – реализует декодирование файла; free_dec(…) – освобождение памяти; top_value(…) – возвращает наибольшее значение верхней границы; number_of_digit(…) – возвращает цифру заданного разряда; shift_number_of_digit(…) – удаление цифры у числа заданного разряда; 2.1.1.2 Функциональные возможности программы (руководство пользователя) Для работы арифметического кодера достаточно скопировать файл compress.exe на жесткий диск компьютера в папку, содержащую сжимаемые файлы. Для кодирования файла в командной строке вводятся следующие параметры: compress.exe –i <имя сжимаемого файла> -o <имя сжатого файла> -e. Для декодирования необходимо указать следующее: compress.exe –i <имя сжатого файла> -o <имя разжатого файла> -d. В процессе работы алгоритма могут выдаваться следующие сообщения об ошибках: • Unknown parameter - неверно задан параметр в командной строке, • Not enough parameters – недостаточное число параметров, • Read error – ошибка при чтении input файла , • Invalid output filename – ошибка чтения и/или записи output файла, • Encoding file doesn’t exist – не существует сжатый файл при попытке декодирования. 2.1.2 Профилирование программы Выявление критических участков кода VTune автоматически запускает профилируемое приложение и начинает собирать информацию о времени его выполнения в каждой точке программы, первоначально представляя информацию об относительном времени выполнении всех модулей системы. Модуль compress.exe занял основную долю времени выполнения приложения. Для определения наиболее «горячих» участков важно оценить вклад каждой функции программы в общее время выполнения. Распределение времени выполнения функций для процесса кодирования внутри модуля compress.exe представлен на рис. 2.1. В число «горячих точек» процесса кодирования входят четыре вычислительноемких функции, осуществляющие сдвиг числа, вычисление цифры заданного разряда, кодирование символа и сжатие файла. Данный предварительный результат необходимо подвергнуть проверке на надежность. Если время выполнения некоторой точки программы не постоянно, а варьируется в тех или иных пределах (например, в зависимости от рода обрабатываемых данных), то трактовка результатов профилировки становится неоднозначной, а сам результат является ненадежным. Для более достоверного анализа требуется проверить, является ли «горячая точка» «плавающей». Результаты профилировки процесса кодирования и декодирования 19 различных типов компьютерных данных представлены в таблицах 2.1 и 2.2 соответственно. Рисунок 2.1 - Профилирование модуля compress.exe Таблица 2.1. Результаты профилировки процесса кодирования. Имя файла Test.doc Test.txt Test.mdb Test.bmp Video_test.avi (несжатое видео) audio_test.avi (несжатый звук) Имя функции Number_of_digit Encode_ symbol Shift_number Encode Number_of_digit Encode_ symbol Shift_number Encode Number_of_digit Encode_ symbol Shift_number Encode Number_of_digit Encode_ symbol Shift_number Encode Number_of_digit Encode_ symbol Shift_number Encode Number_of_digit Encode_ symbol Shift_number Encode Время выполнения (%) 54.96 12.21 15.49 11.42 56.25 5.68 16.48 13.64 44.93 16.67 12.32 13.77 58.77 10.12 13.49 9.95 57.33 8.52 15.33 12.23 56.06 7.51 16.15 14.18 Таблица 2.2. Результаты профилировки процесса декодирования. Имя файла Test.doc Имя функции Number_of_digit 20 Время выполнения (%) 42.64 Test.txt Test.mdb Test.bmp Video_test.avi (несжатое видео) audio_test.avi (несжатый звук) Find_sym Decode_ symbol Shift_number Decode Number_of_digit Find_sym Decode_ symbol Shift_number Decode Number_of_digit Find_sym Decode_ symbol Shift_number Decode Number_of_digit Find_sym Decode_ symbol Shift_number Decode Number_of_digit Find_sym Decode_ symbol Shift_number Decode Number_of_digit Find_sym Decode_ symbol Shift_number Decode 20.02 6.73 20.13 7.44 27.82 48.33 5.65 10.67 4.6 36.11 19.71 13.09 17.09 7.37 45.64 14.8 7.87 20.56 8.17 44.06 18.99 6.94 19.39 7.29 43.44 24.21 5.38 18.01 6.09 Как видно из таблицы, полученные результаты не являются надежными. Для каждой функции необходимо проанализировать ее внутреннее распределение времени и обосновать затраты времени ее выполнения. Анализ узких мест и выбор методов оптимизации Для выяснения причины значительных затрат времени на выполнение функции от number_of_digit использован встроенный анализатор кода. На рис. 2.2 слева исследуемого кода приведено количество затраченного процессорного времени на обработку команд. Трудоемкими являются операции деления и взятия остатка. Рисунок 2.2 - Анализатор кода функции number_of_digit. 21 Целочисленное деление - очень «дорогостоящая» операция, даже на старших моделях процессоров Intel Pentium занимающая до сорока и более тактов. Процесс деления поддается оптимизации. Данный прием рассмотрен в [6, глава 4]. Если делитель кратен степени двойки, то инструкцию деления можно заменить более быстродействующей инструкцией побитового сдвига, выполняющейся за один такт. Если же делитель отличен от степени двойки, то имеет смысл заменить деление умножением. Существует множество формул подобных преобразований, например: N a / b = 2 * aN b 2 (9), где N – разрядность числа. Если делитель – константа, то операция деления выполнится за пять тактов: 2 N a константное выражение, вычисляемое на этапе компиляции, выражение N вычисляется 2 битовым сдвигом за один такт, еще четыре такта расходуется на умножение. Вычисление остатка происходит ничуть не быстрее деления, так как на машинном уровне оно посредством деления и осуществляется. Остаток от деления можно вычислять посредством умножения и битовых сдвигов, но не для всех делителей. Делитель обязательно должен быть кратен k * 2 t , где k и t – некоторые числа. Тогда остаток будет можно вычислить по следующей формуле: a %b = a%k * 2 t = a − (( 2N a * N ) & −2 t ) * k k 2 (10). Современные компиляторы не используют эту формулу, если k не равно 2. Данная формула приведена в [6, глава 4]. Температуру точки можно оценивать не только по времени ее выполнения, но и по частоте вызова. В оптимизации нуждаются «горячие точки» функций, имеющих наибольшее количество вызовов. К числу часто вызываемых относятся функции shift_number и number_of_digit. На рис. 2.3 приведено количество затраченного процессорного времени на вызов функций. Кроме внутренней оптимизации, часто вызываемые функции в большинстве случаев имеет смысл «инлайнить» (от английского in-line), то есть непосредственно вставить их код в тело вызываемых функций, что сэкономит некое количество времени. На рис. 2.4 показано, что основная доля времени внутри функции shift_number расходуется на вычисление делителя и возврат вычисленного значения. Поэтому для значения разряда, равного единице, необходимо «инлайнить» функцию, что сэкономит время на вызов функции и возврат сдвинутого числа. Рисунок 2.3 - Анализатор кода вызовов функций shift_number и number_of_digit. 22 Для функций кодирования и декодирования основные затраты времени связаны с вызовами процедур сдвига, вычислений цифр и границ. Использование метода inline – функций сэкономит время вызова функций. Функции encode_symbol и decode_symbol являются симметричными, обеспечивают вычисление границ для заданного символа. Как показано на рис. 2.5 данные функции не являются столь критичными к оптимизации, и наиболее «горячая точка» cвязана с вычислением ширины интервала и вычислений границ. На каждом шаге декодирования происходит вызов функции find_sym, которая распознает закодированный символ. На рис. 2.6 изображено распределение процессорного времени выполнения инструкций функции. Значительные затраты времени связаны с использованием ветвлений внутри цикла. Суперконвейерные процессоры, которыми являются все старшие представители серии Intel x86, крайне болезненно относятся к ветвлениям. При нормальном ходе исполнения программы в то время, пока обрабатывается текущий код, блок упреждающей выборки успевает считать и декодировать следующую партию инструкций, не допуская простоя шины памяти. Ветвления же очищают конвейер, который у поздних моделей микропроцессоров Pentium очень длинный, и потребуется не один десяток тактов на его заполнение, что приводит к простоям. Поэтому программа, критичная к производительности, должна содержать минимум ветвлений. Рисунок 2.4 - Анализатор кода функции shift_number. Рисунок 2.5 - Анализатор кода функции encode_symbol 23 Рисунок 2.6 - Анализатор кода функции find_sym. Примененные методы оптимизации с учётом специфики задачи • • • • • • • • Сокращение операций целочисленного деления путем замены операцией умножения; Использование оптимизирующей формулы для вычисления остатка; Ликвидация ветвлений; Использование inline – метода для функции; Удаление избыточных переменных и лишних присвоений; Упрощение вычислений для границ интервала; Замена операций целочисленного деления и взятия остатка на операции умножения и вычитания; Упрощение арифметических вычислений ширины интервала в цикле кодирования и декодирования. 2.1.4 Сравнительный анализ работы оригинального и оптимизированного кода Тест на скорость сжатия производился на системе: CPU Pentium-4 1700 МГц, RAM 512 МБ, OS Windows XP. Результаты работы оригинального и оптимизированного кода представлены в таблицах 2.3 и 2.4. Время выполнения вычислено при помощи библиотечной функции clock(). Измерения произведены согласно методу наименьшего времени выполнения. Заключительное тестирование показало, что в ходе оптимизации не была нарушена функциональность приложения. 24 Таблица 2.3. Сравнение усовершенствованного кода. времени кодирования оригинального Имя файла Размер исходног о файла, кб Размер сжатого файла, кб Коэффи циент сжатия Время Время выполне выполне ния ния оригина оптимиз льного ированн кода, мс ого кода, мс 1112 391 1172 391 1162 371 Test.doc Test.bmp Video_test.avi (несжатое видео) audio_test.avi (несжатый звук) Test.txt Test.mdb 1396 1403 1428 989 1040 946 1,41 1,35 1,51 1052 969 1,09 1111 341 3,26 244 2284 164 683 1,49 3,34 180 891 70 390 2,57 2,28 и Коэффиц иент ускорени я 2,84 2,99 3,13 Таблица 2.4. Сравнение времени декодирования оригинального и усовершенствованного кода. Имя файла Размер разжатог о файла, кб Размер сжатого файла, кб Время выполнен ия оптимизир ованного кода, мс 801 781 891 Коэффиц иент ускорени я 989 1040 946 Время выполнен ия оригиналь ного кода, мс 1872 1932 1772 Test.doc Test.bmp Video_test.avi (несжатое видео) audio_test.avi (несжатый звук) Test.txt Test.mdb 1396 1403 1428 1052 969 1832 791 2,32 244 2284 164 683 490 1502 290 711 1,67 2,11 2,34 2,47 1,99 Таким образом, в среднем в 3 раза ускорен арифметический кодер, в 2 раза декодер. Достигнутые результаты показывают, что первоначальный код содержал избыточные и неоптимальные операции. Работа с данным кодом носила методический и исследовательский характер. 2.2 Оптимизация референсного кода арифметического сжатия 25 2.2.1 Обзор референсной программы арифметического кодирования Авторами референсного кода являются Уиттен, Нил и Клири, листинг кода приведен в [10]. В данной программе реализован арифметический кодек, состоящий из двух частей: арифметического кодека и вероятностной модели. В листинге приведены фиксированная и адаптивная вероятностные модели. Фиксированная модель задает частоты символов, приближенные к общим для английского текста. Данная фиксированная модель ограничена в использовании и на практике она уступает адаптивной модели по эффективности сжатия. Клиpи и Уиттен показали, что при общих условиях использование строгой не даст общего лучшего сжатия по сравнению с адаптивным кодированием. В порядке выделения результатов арифметического кодирования, модель будет pассматpиваться как строгая. В [10] исследуется эффективность сжатия приведенной программы. Пpи кодировании текста арифметическим методом, количество битов в закодированной строке pавно энтpопии этого текста относительно использованной для кодиpования модели. Три фактора вызывают ухудшение этой характеристики: • расходы на завершение текста; • использование арифметики небесконечной точности; • масштабирование счетчиков, сумма которых не превышает Max_frequency. Пpи завершении процесса кодирования кодируется уникальный завершающий символ и посылаются вслед достаточное количество битов для гарантии того, что закодированная строка попадет в итоговый рабочий интервал. Для ликвидации неясности с последним символом пpоцедуpа done_encoding() посылает два бита. В случае, когда перед кодированием поток битов должен блокироваться в 8-битовые символы, будет необходимо завершить работу к концу блока. Такое комбинирование может дополнительно потребовать 9 битов. Затраты при использовании арифметики конечной точности проявляются в сокращении остатков при делении. Это видно при сравнении с теоретической энтропией, которая выводит частоты из счетчиков точно также масштабируемых при кодировании. Здесь затраты составляют порядка 10 −4 битов/символ. Дополнительные затраты на масштабирование счетчиков для коротких текстов (меньших 214 байт) отсутствуют, для текстов в 10 5 − 10 6 байтов экспериментально подсчитанные накладные расходы составляют менее 0.25% от кодиpуемой стpоки. Адаптивная модель пpи угрозе превышения общей суммой накопленных частот значение Max_frequency, уменьшает все счетчики. Это приводит к тому, что взвешивать последние события тяжелее, чем более ранние. Показатели имеют тенденцию прослеживать изменения во входной последовательности, которые могут быть очень полезными. Это в общем случае зависит от источника, известны случаи, когда ограничение счетчиков до 6-7 битов давало лучшие результаты, чем повышение точности арифметики. Результаты сжатия, достигнутые пpогpаммой ваpьиpуются от 4.8-5.3 битов/символ для коpотких английских текстов и до 4.5-4.7 битов/символ для длинных. Хотя существуют и адаптивные техники Хаффмана, они все же испытывают недостаток концептуальной пpостоты, свойственной арифметическому кодированию. При сравнении они оказываются более медленными. Hапpимеp, в [10] пpиводитcя таблица 2.5 сравнения хаpактеpистик референсного алгоритма с характеристиками пpогpаммы compact UNIXa, которая pеализует адаптивное кодиpование Хаффмана с пpименением аналогичной модели для исходных тестовых данных. 26 Таблица 2.5 Сpавнение адаптивных кодиpований Хаффмана и аpифметического Текстовые файлы Си-пpогpаммы Алфавит Ассиметpичные показатели Аpифметическое кодиpование Вывод Кодиро Декоди (байты) вание рование (мкс) (мкс) 57718 214 262 62991 73501 12092 230 313 143 288 406 170 Кодиpование Хаффмана Вывод Кодиро Декоди (байты) вание рование (мкс) (мкс) 57781 550 414 63731 76950 16257 596 822 215 441 606 132 2.2.2 Профилирование программы и выбор методов оптимизации Общее время выполнения модуля референсного алгоритма, вычисленное профилировщиком Intel VTune v.7.2, составляет 1102 такта. Распределение тактового времени внутри модуля приведено на рисунке 2.7 Промежуточные замеры времени выполнения осуществляются при помощи реализованной функции read_tsc(), которая является счетчиком точного времени и осуществляет меньший разброс значений, чем библиотечная функция clock(). Листинг функции приведен на рисунке 2.8. Рисунок 2.7 - Тактовое время выполнения функций Первым шагом осуществлено выравнивание используемых массивов перекодировки символов, частот и накопленных частот по границам кэш-линейки. Проанализируем узкие места функции update_model(). Около 70% времени выполнения расходует цикл по корректировке массива накопленных частот. Участок кода приведен на рисунке 2.9. Данную операцию возможно заменить с помощью использования SIMD команд MMX, SSE2 (в зависимости от поколения выполняющего процессора [11]), приведенных на рисунке 2.9. Также изменения типа данных массива cum_freq с 4 байтного беззнакового на 2 байтный беззнаковый дает возможность на одном такте проводить в два раза больше сложений (восемь против четырех для инструкций SSE2, и 4 вместо двух для MMX). Использована возможность процессора Pentium 4 упреждающей загрузки данных из оперативной памяти в кэш c помощью инструкции prefetcht0 . Для цикла масштабирования счетчиков осуществлен четырехкратный разворот цикла. Время выполнения оптимизированного кода update_model() составило 190 тактов. 27 void read_tsc(unsigned long long * time_stamp) { unsigned int *tsc_l, *tsc_h; tsc_l = (unsigned int *) time_stamp; tsc_h = tsc_l + 1; __asm{ rdtsc mov ecx, tsc_l mov [ecx], eax mov ecx, tsc_h mov [ecx], edx } } Рисунок 2.8 - Функция read_tsc() Рисунок 2.9 - Участок SIMD-оптимизируемого кода функции update_model() if(i_4x) __asm{ mov lea movdqu lea LINE_LOOP: ecx, i_4x edi, one_4 xmm7, [edi] edi, cum_freq prefetcht0 movdqu xmm0, movdqu xmm1, movdqu xmm2, movdqu xmm3, paddw xmm0, paddw xmm1, paddw xmm2, paddw xmm3, [edi+64] [edi] [edi+16] [edi+32] [edi+48] xmm7 xmm7 xmm7 xmm7 movdqu movdqu movdqu movdqu [edi], xmm0 [edi+16], xmm1 [edi+32], xmm2 [edi+48], xmm3 add sub jnz edi, 64 ecx, 1 LINE_LOOP } Рисунок 2.9 - SIMD-оптимизированный код Узким местом функции output_bit () является ветвление. На рисунке 2.10 приведен временной анализ кода output_bit и временной анализ сгенерированного кода. 28 Рисунок 2.10 - Анализатор кода функции output_bit Произведена замена условного перехода арифметической операцией buffer |= (bit<<7). Время выполнения операции составило 15 тактов. Анализатор оптимизированного кода приведен на рисунке 2.11. Прирост быстродействия составил 35 тактов. Рисунок 2.11 - Анализатор кода функции output_bit() На тестовом материале проанализировано количество попаданий в каждое последовательное ветвление функций decode_symbol(), encode_symbol(). Для ускорения работы функций изменен порядок последовательных проверочных условий по вероятности попадания в ветвление: 1. (low >= HALF), 2. (high < HALF), 3. (low >= FIRST_QTR && high < THIRD_QTR). В результате анализа профилировщиком референсного алгоритма выявлено неоптимальное использование операций чтения и записи в файл символа (рис.2.9). Для устранения неоптимальности введены объекты промежуточного буфера для входных и выходных последовательностей, находящиеся в оперативной памяти и обмен данными с которыми занимает меньшее количество времени. Файловые операции чтения производятся один раз в начале работы алгоритма. Файловые операции записи производятся один раз в конце работы алгоритма. Операция buffer = getc (in), расходующая 16 тактов при кодировании и 15 при декодировании, заменена buffer = *(buf_in+(buf_counter++)), аналогично для putc (buffer, out). В функции инициализации объекта buf_init() в качестве параметра возвращается размер входного файла. Это позволяет выполнить упрощение: убрать проверку конца файла в функции input bit(). Функции output_bit(), output_bit_plus_follow(), input_bit() были переведены в макросы. Улучшение быстродействия вызвано устpанением pасходов по вызову пpоцедуp. 29 После реализованных методов оптимизации замер профилировщика показал время выполнения приложения в 941 такт. Профилирование результирующего кода приведено на рисунке 2.12. Рисунок 2.12 - Тактовое время выполнения функций Таким образом, применен следующий комплекс оптимизирующих методов: • Изменение типов данных, выравнивание данных по границам кэш-линейки • Изменение архитектуры программы • Устранение избыточных ветвлений • SIMD код (MMX, SSE2) • Замена ветвлений безусловными операциями • Кэш оптимизация для SSE2 (использование предзагрузки данных) • Введение объектов промежуточного буфера для ускорения операций с входным и выходными последовательностями • Изменение порядка последовательных проверочных условий по вероятности попадания в ветвление • Устранение избыточных вызовов операторов call, ret, снижение количества операций со стеком (замена часто вызываемых функций макросами) 2.2.3 Сравнительный анализ времени выполнения референсного и оптимизированного кода Тест на скорость сжатия производился на системе: CPU Pentium-4 1700 МГц, RAM 512 МБ, OS Windows XP. Результаты работы референсного и оптимизированного кода представлены в таблицах 2.6. Измерения произведены согласно методу наименьшего времени выполнения. Таблица 2.6. Сравнение усовершенствованного кода. Имя файла Test.doc Test.bmp Video_test.avi (несжатое видео) audio_test.avi (несжатый звук) Размер файла Размер сжатого файла времени кодирования Характеристики референсного кода 1396 1403 1428 822 898 732 Кодиро вание, (мс) 590 560 480 1052 881 600 30 Декодир ование, (мс) 631 601 501 651 оригинального Характеристики оптимизированного кода Кодиров Декодиров ание, ание, (мс) (мс) 450 561 440 531 390 431 450 561 и Test.txt Test.mdb 244 2284 152 572 90 510 31 100 571 70 430 80 501 Заключение. В рамках настоящей работы были проведено изучение алгоритма арифметического кодирования и методов оптимизации приложений. Был реализован арифметический кодек. Хорошие показатели сжатия достигаются при использовании файлов с асимптотическим распределением частот встречающихся символов, то есть когда частота встречаемости символов во входном сообщении сильно отличается друг от друга. Реализованный арифметический кодек послужил объектом для исследования методов оптимизации, примененных в рамках улучшения быстродействия программы арифметического кодирования. Применением стратегии оптимизации для программной реализации алгоритма арифметического сжатия достигнуты следующие результаты на тестовом материале: увеличение быстродействия для процесса кодирования в среднем в 3 раза, уменьшение времени декодирования в среднем в 2 раза. Практическая ценность работы заключается в оптимизации референсного алгоритма арифметического кодирования. На основании полученных результатов исследования методов оптимизации достигнуто увеличение быстродействия на тестовом материале в среднем на 20–25 %. Оптимизированный вариант программной реализации алгоритма c некоторыми модификациями вошёл в состав JPEG2000 кодека компании MainConcept. 32 Список использованных источников. 1. Вернер М. Основы кодирования. М.: Техносфера, 2004. - с.75 – 80. 2. В.Н.Потапов. Арифметическое кодирование вероятностных источников. Институт математики им. С.Л.Соболева СО РАН.: Новосибирск, 2001. - 12 с. 3. Захаров М.В. Об одной реализации арифметического кода. [Электронный ресурс]. 1998. – Режим доступак ресурсу: http://compression.ru/download/articles/ar/zakharov_1993_arith_pdf.rar , свободный. 4. Сэломон Д. Сжатие данных, изображений и звука. – М.: Техносфера, 2004. - с.62 – 81. 5. Мастрюков Д. Алгоритмы сжатия информации. Часть 2. Арифметическое кодирование // Монитор. - 1994. - № 1. – с. 20 – 23. 6. Касперски К. Техника оптимизации программ. Эффективное использование памяти. – СПб.: БХВ-Петербург, 2003. - с.1-93, 407 – 456. 7. Касьянов В.Н. Оптимизирующие преобразования программ. – М.: Наука, 1988. с.16-20. 8. Яковлев А. Старые идеи об оптимизации программ. [Электронный ресурс]. -1996. – Режим доступа к ресурсу: http://alex.ability.ru/optimize.html, свободный. 9. Isensee P. C++ optimization Strategies and Technigues. [Электронный ресурс]. – 1993.Режим доступа к ресурсу: http://www.tantalon.com/pete/cppopt/main.htm , свободный. 10. Witten I., Neal R., Cleary J. Arithmetic Coding For Data Compression// Communications of the ACM. -1987. - № 6. –с.520-540. 11. Рей Дункан. Оптимизация программ на ассемблере. [Электронный ресурс]. – Режим доступа к ресурсу: http://www.helloworld.ru/texts/comp/lang/asm/opt/index.htm 12. Арифметическое кодирование [Электронный ресурс]. – Режим доступа к ресурсу: http://algolist.manual.ru, свободный. 33 Приложение А. Доказательство декодирующего неравенства. Доказательство приведено в [12]. Пpовеpим верность определения пpоцедуpой decode_symbol() следующего символа. Из кода функции видно, что должен использовать code для поиска символа, сократившего при кодировании рабочий интервал так, что он продолжает включать в себя code. Number = Cum_freq[0] – это нормализованный множитель. Строка нахождения переменной cum, значение которой определяет вычисленную накопленную частоту, в функции определяет такой символ, для которого ⎡ (Code − Low + 1) * number − 1⎤ CumFreq[ symbol ] <= ⎢ ⎥ < CumFreq[ symbol − 1] High − Low + 1 ⎣ ⎦ где "[ ]" обозначает операцию взятия целой части - деление с отбрасыванием дробной части. Другими словами: CumFreq[ symbol ] <= (Code − Low + 1) * number − 1 + ε < CumFreq[ symbol − 1] Range где Range = High − Low + 1 , 0 <= ε <= (А.1), Range − 1 . Range Последнее неравенство выражения (A.1) происходит из факта, что CumFreq[symbol-1] должно быть целым. Затем необходимо показать, что Low' <= Code <= High', где Low' и High' есть обновленные значения для Low и High как опpеделено ниже. High = Low + Range * HighRange( X ) − 1 (A.2), Low = Low + Range * LowRange( X ) (A.3), ⎡ Range * CumFreq[ symbol ] ⎤ Low′ = Low + ⎢ ⎥⎦ <= number ⎣ ⎡ Range ⎤ ⎡ (Code − Low + 1) * number − 1 ⎤ *⎢ <= Low + ⎢ − ε ⎥ <= / из _ выражения _ А.1 / <= Range ⎣ number ⎥⎦ ⎣ ⎦ <= Code + 1 − 1 number Поэтому Low' <= Code, так как Code, Low', number положительны. 34 ⎡ Range * CumFreq[ symbol − 1] ⎤ High ′ = Low + ⎢ ⎥ − 1 >= number ⎣ ⎦ ⎤ ⎡ Range ⎤ ⎡ (Code − Low + 1) * number − 1 >= Low + ⎢ *⎢ + 1 − ε ⎥ − 1 <= / из _ выражения _ А.1 / >= ⎥ Range ⎣ number ⎦ ⎣ ⎦ Range ⎡ Range − 1⎤ 1 * ⎢− +1− = Code number ⎣ Range Range ⎥⎦ Получили, что: >= Code + ⎡ ( High − Low + 1) * CumFreq[ symbol ] ⎤ Low + ⎢ ⎥⎦ <= Code <= number ⎣ ⎡ ( High − Low + 1) * CumFreq[ symbol − 1] ⎤ <= Low + ⎢ ⎥⎦ number ⎣ Таким обpазом, что Code лежит внутpи нового интеpвала, вычисляемого пpоцедуpой decode_symbol(). Это опpеделенно гаpантиpует коppектность опpеделения каждого символа опеpацией декодиpования. 35