К. Касперски - Техника оптимизации программ, Эффективное использование памяти (1127752), страница 86
Текст из файла (страница 86)
Измерять производительность еше проше — достаточно засечь время выполнения функции и... Правда, тут есть одно "но". Если уж мы взялись оценивать именно качество кодогенерации, а не быстродействие компьютера, следует учесть, и по возможности свести к нулю, все посторонние эффекты. Во-первых, к моменту вызова функции все обрабатываемые ей данные должны целиком находиться в кэше первого уровня, иначе неповоротливость памяти сотрет все различия в произволительности тестируемого кола. Во-вторых, размер обрабатываемых данных должен быть лостаточно велик для того, чтобы замаскировать накладные расхолы на вызов функции, передачу аргументов, снятие показаний со счетчика производительности и т. д.
Все нижеследующие примеры обрабатывают 4000 элементов типа 'ле, — это лает стабильный и хорошо воспроизволимый результат, т. к. "насышение" наступает уже на 1000 элементах, после чего накладные расходы уже не играют сколь-нибудь заметной роли, Машинная оптимизация Сравнительный анализ основных компиляторов Давайте рассмотрим три довольно полярных алгоритма: копирование блока памяти, поиск минимума орели множества чисел и пузырьковую сортирпвку. Предложенный выбор совсем не случаен. Операции копирования (равно как сравнения и поиска в памяти) программисты всегда предпочитали реализовывать на чистом ассемблере, ибо микропроцессоры семейства (пге! х8б поддерживают специальные машинные инструкции, изначально ориентированные на эти цели. В частности, копирование памяти осуществляется командой ввг ночев своеобразным аналогом функции срт.
К сожалению, эквивалентные ей (команде вез моча) конструкции в языках С/С++ отсутствуют. Можно, конечно, вызвать функцию срт, но это будет нечестно — мы же договорились библиотечные функции не использовать! (тем более что вт за редкими исключениями пишется отнюдь не на С, а на ассемблере). На уровне чистого языка существует лишь один путь решения данной задачи — поэлементное копирование массива в цикле. Проблема в том, что компиляторы еше не научились понимать физический смысл компилируемой программы, и лаже такой очевидный алгоритм копирования ни один из известных мне оптимизаторов "ни в жизнь" не распознает. Компилятор дословно переведет программу на язык машинного кода, сохранив при этом и сам цикл, и все временные переменные, используемые для пересылки данных.
Как следствие — полученный код будет далеко не самым оптимальным, проигрывая ассемблеру и но скорости, и по размеру, да и по времени, затраченному на его создание. Поэтому очень интересно знать: насколько эффективной окажется компиляция заведомо неоптимального кода. Данный пример позволяет оценить целесообразность использования ассемблера для решения задач, имеющих аппаратную поддержку со стороны процессора, в отсутствие эквивалентных конструкций в языке высокого уровня.
Поиск минииул~а — достаточно тривиальный алгоритм, элементарно укладывающийся в несколько строк как на ассемблере, так и на языке высокого уровня. Столь малое пространство не лает оптимизатору "развернуться и показать себя во всей красе". Да этого нам, собственно, и не нужно! Напротив, представляет интерес выяснить: какое количество избыточного кода "воткнул" сюда компилятор! Благодаря тому, что объем полезно~о кода в нем очень мал, такой пример оказывается весьма чувствительным даже к микроскопическим порциям "мусора".
Наконец, сортптт)товка прелставляет собой довольно типичный пример программистского кода, и по ней вполне можно сулить о "средневзвешенном" качестве оптимизации конкретных компиляторов. 440 Глава 4 Сравнивать различные компиляторы между собой — проще всего. Сравнение же компилятора с человеком осуществить сложнее, поскольку сразу же возникает неопределенностгк с каким именно человеком его сравнивать.
С профессионалом экстра-класса? Но будет ли показательным такое сравнение? Мы же говорим не о теоретическом превосходстве человеческого интеллекта над машинным, а о практических путях решения задач, стоящих перед рядовыми программистами. Может ли рядовой программист рассчитывать на помощь такого экстрапрофессионала? Вряд ли! Скорее всего, засучив рукава, ему придется взяться за кодирование самому. Поэтому приведенные ниже ассемблерные примеры умышленно составлены как средний по качеству и далеко не идеальный код.
Причем, поскольку целевой процессор заранее не оговорен, при их подготовке использовались лишь самые общие приемы оптимизации, без учета оптимального планирования потока команд. Тем не менее, это действительно оптимизированный ассемблерный код, приблизительно соответствующий уровню программиста средней руки. (Кстати, интересно: кто из читателей сможет улучшить качество кода хотя бы на процент?) Хорошо, с человеком мы разобрались. Теперь дело — за компиляторами. Какие же из них выбрать? Давайте остановимся на следующем "джентльменском наборе": М!сгозой 'й!зца! С++ 6, Вот!апг! С+-> 5.5 и %АТСОМ С++ !О.
Обсуждение результатов тестирования Итак, тестирование началось. Прогон "полопытных" примеров на процессорах 1пге! Репнцгп-Ш 733 и АМ!3 Айт!оп !400 (рис. 4.3, 4.4) говорит о достаточно высоком качестве кодогенерации современных компиляторов. В среднем 1за вычетом особо оговариваемых исключений) производительность откомпилированных программ лишь на 20 — 30% уступает вручную оптимизированному ассемблеру.
Конечно, это весьма внушительная величина (особенно, если вспомнить, что эталонная ассемблерная программа достаточно далека от идеала). Эй, кто там говорил, что машинная оптимизация уступает человеку не более одного-двух процентов?! А ну, подать сюда этого человека! С другой стороны, разрыв производительности (за редкими исключениями) все же не настолько велик, чтобы перенос программы на ассемблер приводил к качественным изменениям.
А теперь обо всем этом подробнее. Как и следовало ожидать, наибольший разрыв в произволительности наблюдается на копировании памяти. Впрочем, этот разрыв значительно сокращается с ростом тактовой частоты процессора. Если на Репгит-1П 733 наименьшее отставание составило целых 25%, то на Агп!оп 1400 — всего 9%! Едва ли последняя цифра нуждается в комментариях — М!сгозой "рулит" и жизнь прекрасна.
Быстрота современных про- Машинная оптимизация цессоров, помноженная на мощь современных компиляторов, — и никаких ассемблерных вставок! Конечно, не все компиляторы одинаково хороши. Так, ЖАТСОМ вЂ” вообще "в осадке"; Вот!апд уверенно держит позиции на !иге!, но генерирует несколько неоптимальный код с точки зрения АМО. С поиском минимума все компиляторы справились одинаково хорошо, а М!сгозой Ч!вва! С++ вообще построил илеальный по своей красоте код, лишь из-за лосадной случайности не дотянувшийся до 100%-ного результата, — начало цикла пришлось на наихудший с точки зрения микропроцессора адрес; Ох40!3ГГ.
"Благодаря" этому каждая итерация облагается несколькими штрафными тактами, что, в конечном счете, выливается во вполне весомые потери. Чаще всего, впрочем, судьба оказывается не столь жестока, и код, сгенерированный компилятором, исполняется лостаточно эффективно. Рис. 4.3. Сравнение качества машинной кодогенерации по скорости на 1п1е! Р-1П 733 Однако нет никаких гарантий, что даже малейшее изменение программы (да-да, — в том числе и выкидывание лишнего кода!) не ухудшит ее производительности (причем, подчас весьма значительно).
Увы, в этом смысле компиляторы все еще "тупы до безобразия". Они либо вовсе не выравнивают переходы, либо выравнивают все переходы, что неоправданно увеличивает 44г Глава 4 размер программы и нередко дает обратный эффект, многократно снижая ее производительность (если программа в результате такого "распухания" не поместится в кэш). Между тем, правильное решение — выравнивать лишь часто выполняемые переходы, в частности циклы, но — увы — ни в одном известном мне компиляторе это не реализовано. Рис.
4.4. Сравнение качества машинной кодогенрацни по скорости на АМС Айяоп 1400 Заметно лучше сложилась ситуация с сортаровхой. Компилятор М!сговой%ьца! С++ отстает от ассемблерного кода всего лишь на 13 — 14%. За ним с минимальным отрывом идет Вог1апд С++ со своими 15 и 24% лля Акоп 1400 и Р-1П 733 соответственно.
Последнее место занимает компилятор ттАТСОМ, ни в чем не уступающий Вог1апд на процессоре Репбцпц но безапелляционно сдающий свои позиции на процессоре А!11!оп. Ну, не виноват он, что создавался в ту далекую эпоху, когда и процессоры, и техника оптимизации были совсем другими! В целом, !йАТСОМ неплохой, но безнадежно устаревший оптимизатор, и любовь к нему (у тех, у кого она имеется) не должна "слепить глаза", сегодня ттАТСОМ вЂ” уже не самый лучший выбор.