К. Касперски - Техника оптимизации программ, Эффективное использование памяти (1127752), страница 84
Текст из файла (страница 84)
Рассмотрим типичный цикл с условием в середине: ртаптГГ"1й оператор)о")) ав ! †а) ьаеах) ртгпо1)"2й оператор1п")) )пь11е)1) ) Компилятор М)сгозой Ч!вца! С++, не долго думая, транслирует его в цикл с постусловием, удаляя, тем самым, оператор ьпеай, рттптг Р'1й операторхп") а —; 1Г )а>=0) рт1пег)"1й оператор1п"): ртаптг)"2й оператор1п")) ) ть11е) †а)) Платой за быстродействие становится некоторое увеличение размеров программы, т.
к. операторы цикла, прелшествуюшие ветвлению ь .ах, теперь встречаются дважды. Заметим еще раз, что из всех трех рассматриваемых компиляторов удалять внутрицикловые ветвления умеет один лишь М!сговоГГ Ч!вва! С++, — ни Вот!апГ! С-ь+, ни тЧАТСОМ на это не способны. 431 Машинная оптимизация Оптимизация вызова функций Оптимизация передачи аргументов Механизмы вызова и передачи аргументов функциями стандартных типов (как то: сеесг, аьесаы, Рьвсль) жестко декларированы, и никакой самодеятельности оптимизатор позволить себе не может. Стандарт предписывает передавать аргументы через стек, т.
е. через "жутко тормозную" оперативную память, и, чем больше аргументов принимает функция, тем значительнее накладные расходы па каждый ее вызов. Если же тип функции не задан явно, компилятор имеет право передавать ей аргументы так, как он сочтет нужным. Эффективнее всего передавать аргументы через регистры, чтение/запись которых укладывается в олин такт процессора (обращение к памяти может потребовать не одного десятка тактов в зависимости от ряда факторов — быстродействия микросхем и контроллера памяти, частоты шины, наличия или отсутствия ланных ячеек в кэше и т. д.). Беда в том, что даже у старших моделей микропроцессоров Репгшш регистров общего назначения всего семь и их приходится делить между аргументами и возвращаемыми значениями функций, регистровыми и промежуточными переменными.
Выделить все семь регистров под аргументы — было бы слишком глупым решением, ибо в этом случае переменные, содержавшие передаваемые аргументы, пришлось бы размещать в оперативной памяти, сводя весь выигрыш на нет. Компилятор М!сговой Чвша! С++ отводит для передачи аргументов два регистра, Вог1апо С++ — три, а 1ЧАТСОМ вЂ” четь1ре. Вопрос: "чья стратегия лучше?" остается открыт и единого мнения нет. Автор этой книги склоняется к мысли, что два регистра, — действительно, наилучший компромисс.
К слову сказать, М!сгогюй С 7.0 — "прародитель" М!сговогг Чиша) С++— использовал для передачи аргументов три регистра, но после серии испытаний, обсуждений, споров и дебатов, его разработчики пришли к выводу, что лва регистра обеспечивают лучшую производительность, нежели три. Другой важный момент — это тип вызова функции по умолчанию.
Компиляторы М|сгозой Чиша) С++ и Вот!апг! С++ задействуют регистровую передачу аргументов только в том случае, если перел функцией указан квалификатор г аы (исключение составляет неявный аргумент елька, по умолчанию передаваемыи через регистр). Компилятор же УУАТСОМ по умолчанию использует регистровую передачу аргументов. Поэтому, если вы используете М!сговой Ч!вца1 Сч-ч- или Вог!апг) С+ч-, вставляйте квалификатор г есаы самостоятельно. Оптимизация пролога!эпилога функций Ранние С-компиляторы использовали для адресации локальных переменных базовый указазель стека — регистр ВР (ЕВР в 32-разрядном режимс), поме- Глава 4 4Зг гцая в начало каждой функции специальный код, называемый прологозс Пролог сохранял текущее содержимое регистра ВР (ЕВР) в стеке и копировал в него указатель вершины стека, хранимый в регистре ЯР (ЕЯР).
Затем, уменьшением значения регистра ЯР (ЕЯР), выделялась память локальным переменным функции (стек, как известно, растет снизу вверх). По завершении функции код эпилога вновь "опускал" регистр-указатель вершины стека, освобождая память, занятую локальными переменными, и восстанавливал значение базового указателя стека — регистра ВР (ЕВР).
Ну, резервированиеГосвобождение памяти — все это понятно, но вот регистр ВР (ЕВР) зачем? А вот зачем: он хранит указатель кадра стека, т. е. региона памяти, отведенного под локальные переменные. Все три рассматриваемых компилятора адресуют локальные переменные иначе — непосредственно через ЕЯР.
Это значительно усложняет реализацию компилятора, т. к. указатель вершины стека меняется в ходе выполнения программы и адресация выходит "плавающей", зато такая техника высвобождает один регистр лля регистровых переменных и избавляется от двух операций обращения к памяти (сохранения/восстановления ЕВР), что заметно повышает производительность. Оптимизация распределения переменных В языках С/С++ существует ключевое слово "гея(пег", предназначенное для принулительного размещения переменных в регистрах.
И все бы было хорошо, ла подавляющее большинство компиляторов втихую игнорирует предписания программистов, размещая переменные там, где, по мнению компилятора, им будет "удобно". Разработчики компиляторов объясняют это тем, что компилятор лучше "знает", как построить наиболее эффективный код. "Не надо, — говорят они, — пытаться помочь ему". Напрашивается следующая аналогия; пассажир говорит — "мне надо в аэропорт", а таксист без возражений едет "куда удобнее". Ну, не должна работа на компиляторе превращаться в войну с ним, ну, никак не должна! Отказ разместить переменную в регистре вполне законен, но в таком случае компиляция должна быть прекращена с выдачей сообщения об ошибке, типа "убери гея)пег, а то компилировать не буду!", или на худой конец — выводя предупреждения.
Впрочем, ладно, все это лирика. Гораздо интереснее вопрос о том, какую именно стратегию распределения переменных по регистрам использует каждый компилятор, Компиляторы Вот!апд С++ и ЮАТСОМ при нехватке регистров помещают в них наиболее интенсивно используемые перемеренные, а все остальные "поселяют" в медленной оперативной памяти. Компилятор же М)сгозой авва! С++ не учитывает частоты использования переменных и размещает их в регистрах в порядке объявления. 433 Машинная оптимизация Оптимизация инициализации строк Компилятор М1сгозой Ч)гша! С++ выгодно отличается от своих конкурентов, т. е. Вот!апг) С++ и ЪЧАТСОМ, тем, что константные строки он инициирует, "заглатывая" их не байтами, а двойными словами.
К тому же, только М!сгоьой Чвша! С++ умеет хранить короткие строки в регистрах. Поэтому программы, интересно манипулирующие со строками, выгоднее компилировать компилятором М!сгоаой Чаша! С++. Оптимизация "мертвого" кода "мертвым" называют код, никогда не получающий управления. Например, объявит программист функцию, но ни разу не использует, — ситуация знакомая, не правда ли? Зачем же тогда этой функции впустую расходовать память? Увы, ни один из трех рассматриваемых компиляторов не удаляет "мертвые" функции, оставляя эту работу на откуп линкеру. Умные линкеры действительно удаляют функции, на которые отсутствуют ссылки, но все же лучше, если это заблаговременно сделает компилятор.
Возьмем другой пример — пусть в программе присутствует следующий код, выводящий отладочное сообщение на экран, если макрос савва определен как ткнг: тГ (ОЕВНС! ртгдст!"некоторое отладочное сообщение Ч; При компиляции финальной версии савва объявляется как гасан и отчадочный код никогда не выполняется, поэгому имеет смысл его улалить. Компилятор М!сгозой Чаша! С++ удаляет и проверку условия, и тело условного оператора (в ланном случае — вызов функции рт дге), но забывает "вычистить" константную строку. Правда, есть надежда, что ее удалит "продвинутый" линковщик, не обнаружив на нее ни одной ссылки. Вот компилятор %АТСОМ действительно начисто удаляет весь "мертвый' код — и проверку условия, и тело условного оператора, и константную строку. А компилятор Вот!апд С++ вообще не удаляет "мертвый" код, послушно выполняя проверку константы (константы!) гхьнн на равенство тннг.
Оптимизация константных условий Константные условия в изобилии встречаются во множестве программ. Например, бесконечный цикл (или цикл с условием в серелине) подавляющим большинством программистов объявляется так: нщ1е !1! Глава 4 434 тело цикла Логично, что проверка ! == г бессмысленна, и ее можно опустить. Компиляторы М!сгозой Чева! С++ и ФАТСОМ именно так и поступают, но вот Вот!апс! С++ аккуратно проверяет: а равен ли один одному (ну мало ли).
Определение компилятора-победителя Так какой же компилятор лучше всех? Пальму первенства по праву, безусловно, заслуживает М!сгозой Чвша! С+-ь. За ним, существенно отставая, идет *тЧАТСОМ, а Вог1апб С++ плетется в самом хвосте, показывая на удивление низкий результат, и за что только его называют оптимизирующим компилятором? "А где же количественные тесты?" — спросит придирчивый читатель. Их нет в этой главе. Нет, потому что выигрыш, даваемый оптимизатором, очень сильно зависит от рода компилируемого кода и более чем на порядок варьируется от одной программы к другой.
Смертельная схватка: ассемблер чв. компилятор "Священные войны" вокруг компиляторов бушуют очень давно. Одни обожествляют машинную кодогенерацию, другие же стремятся все делать своими руками, порой реализуя программы на чистейшем ассемблере. Конечно, всякий имеет право на выбор, но этот выбор должен делаться осмысленно, а не вслепую. Между тем, каждая из сторон распускает совершенно неправдоподобные слухи.
Приверженцы компиляторов убеждают окружающих в том, что человек физически не способен учитывать все архитектурные особенности современных процессоров, эта работа якобы по плечу одному лишь оптимизатору. Их противники в качестве контраргумента обычно приводят ассемблерную реализацию канонической программы "Нейо, тЧог!б!", — по объему раз в триста меныпей "самого оптимального кода", сгенерированного компиляторами.
Вот и разберись: кому же верить, а кому нет? Такая неопределенность часто нервирует начинаюгцих программистов, пытающихся разобраться: стоит лн изучать ассемблер или же это пустая трата времени? Чтобы там ни говорили сторонники компиляторов, машинная оптимизация всегда будет проигрывать человеку, поскольку действует по жесткому, заранее заложенному в нее шаблону, в то время как человек же способен на иа- 435 Машинная оптимизация чествеяно новые решения. Касательно же невозможности учета архитектурных способностей современных процессоров, — тем, кто это утверждает, легко возразить: не стоит, право же, судить все человечество по себе. Оптимальное планирование потока команд вполне по силам программисту срелней руки, конечно, при наличии соответствующей подготовки.