К. Касперски - Техника оптимизации программ, Эффективное использование памяти (1127752), страница 17
Текст из файла (страница 17)
Постой, постой, а где у нас В дее рееб ЦЕЛОЧИСЛЕННОЕ ДЕЛЕНИЕ? А ВОТ, Ужс НВШЛИ! борехб[етур«еббаеа, рееб, еа11бСВС, 100*а/иех Ь«аем Здесь мы вычисляем процент проделанной работы. Забавно, но на это уходит приблизительно столько же времени, сколько и на саму работу! А раз так — ну все эти "градусники" к черту! Удаляем команду деления, подставляя вместо значения прогресса 0 или любое другое понравившееся вам число.
Перекомпилируем и в результате получаем скорость в нетырнадиать с поло- виной миллионов паролей в секунду! Шаг шестой. Удаление мониторинга производительности НесмотРЯ на пРЯмо-таки гигантскУю скоРость пеРебоРа, фУнкциЯ д ре 1 все еше оттягивает на себя 22% времени исполнения программы, что не есть хорошо. Двойной щелчок по ней показывает, что среди более или менее ровного ряда практически одинаковых по высоте диаграмм возвышается всего лишь один красный прямоугольник. А ну-ка посмотрим, что там! Дизассемблирование позволяет установить, что за этой "горячей" точкой скрывается уже знакомая нам конструкция: бс [++х>ббббб[ х=с; рхае«г [ "сееееее рееб : 110е [ъбъь) «х", арахб [О[, рходхеееы Профили овка программ Что ж! Во имя Ее Величества Производительности мы решаемся полностью отказаться от мониторинга текущего состояния и выбрасываем эти пять строк напрочь.
В результате скорость перебора повышается еше «а «я«гь м«в««онов паролей в селу«ду. Хм, вообще-то не такая уж и большая прибавка, так что не факт, что такую оптимизацию следовало бы выполнять. Шаг седьмой. Обьединение функций На этот раз самой "горячей" точкой становится сохранение регистра ЕБ1 где-то глубоко внутри функции са1 1аге скс. Это компилятор "заботливо" бережет его содержимое от посторонних модификаций.
Несмотря на то, что количество используемых переменных в программе довольно невелико и в принципе обращений к памяти можно было бы и избежать, разместив все переменные в регистрах, компилятор не в состоянии этого сделать, т. к. оптимизирует каждую функцию по отдельности. Так давайте же, забыв про структурность, объединим все наиболее интен- СИВНО ИСПОЛЬЗУЕМЫЕ фуихцИИ (деп раеб, бо раехб, Сьеог СВС И Са1оо1аее Сас) в одну "супер-функцию".
Ее реализация может выглядеть, например, так, как показано в листинге Е22. ьпг деп рахб(оьаг *огургеббага, оьаг *рахб, ьпг пах ьгег, гпг ха1ьбсвс) ).пе а, Ь, х; гпг р = О; оьаг *Ьогг! 1пг 1епдгь = агг1еп(раеб)г 1ог(а = Оу а < пах гьег; аее) х = -1; гог (ь = О; ь <= 1епдгь) ьеы х е= * сьпг ")((1пг)реиб е Ы; гг (х=ееа11бсас) ьогг = (оьаг *) па11оо(*гг1еп(огургеббага)); аггору(ьпгг, огург бага): Оесгург(р 1, ьоег); 72 Глава ! ртж Г("СВС абх: еву Со Весгурт: 1" $*',"~аГЧ ывсвс,ьигг)) ишге((~-пряма(р))>'к') ( раас(р1 = '! ': р'+: ге ('р~~~с(Р)) ( раас(р)=' '; ря>хцр+(1=0;1епч~ь++) )) р = О) ге(авв ьч Компилируем, запускаем, ой! прямо не верим своим глазам — тридцать пять миллиоиов паролей в секунду'. А ведь казалось, что резерв быстродействия уже исчерпан.
Ну, и кто теперь скажет, что Реп(гцгп — медленный процессор? Генерация очередного пароля, вычисление и проверка его контрольной суммы укладываются в каких-то двадцать тактов. Двадцать тактов?! Хм! Тут еше есть над чем поработать! Шаг восьмой. Сокращение операций обращения к памяти Основная масса "горячих" точек теперь, как показывает профилировка, сосредоточена в цикле подсчета контрольной суммы пароля — на него приходится свыше 80% всего времени исполнения программы, из них 50% "съедает" условный переход, замыкаюший цикл (Реп!)цп)-процессоры очень не любят коротких циклов и условных переходов), а остальные 50% расходуются на обрашение к кэш-памяти.
Туг уместно сделать небольшое пояснение. Расхожее мнение утверждает, что чтение нерасшепленных данных, находя- шихся в кэш-памяти, занимает всего один такт, — ровно столько же, сколько и чтение содержимого регистра. Это действительно так, но при более пристальном изучении проблемы выясняется, что "одна ячейка за такт", это пропускная способность кэш-памяти, а полное время загрузки данных с учетом латентности составляет как минимум три такта. При чтении зависимых данных из кэша (как в нашем случае) полное время доступа к ячейке определяется не пропускной способностью, а его латентностью. К тому же, на процессорах семейства Рб установлено всего лишь одно устройство чтения данных и поэтому даже при благоприятном стечении обстоятельств они могут загружать всего лишь одну ячейку за такт.
Напротив, на данные, храняшиеся в регистрах, это ограничение не распространяется. Профилировка программ 73 Таким образом, для увеличения производительности мы должны избавиться от цикла и до минимума сократить количество обращений к памяти. К сожалению, мы не можем эффективно развернуть цикл, поскольку нам заранее неизвестно количество его итераций. Аналогичная ситуация складывается и с переменными: программируя на ассемблере, мы запросто смогли бы разместить парольный буфер в регистрах общего назначения (благо )бсимвольный пароль — самый длинный пароль, который реально найти перебором, — размещается всего в четырех регистрах, а в остающихся трех регистрах располагаются все остальные переменные). Но для прикладных программистов, владеющих одним лишь языком высокого уровня, этот путь закрыт и им приходится искать другие решения. И такие решения действительно есть! До сих пор мы увеличивали скорость выполнения программы за счет отказа от наиболее "тяжеловесных" операций, практически не меняя базовых вычислительных алгоритмов.
Этот путь привел к колоссальному увеличению производительности, но сейчас он себя исчерпал и дальнейшая оптимизация возможна лишь на алгоритмическом уровне. В алгоритмической же оптимизации нет и не может быть универсальных советов и общих решений — каждый случай должен рассматриваться индивидуально, в контексте своего окружения.
Возвращаясь к нашим баранам, задумаемся: а так ли необходимо считать контрольные суммы каждого нового пароля? В силу слабости используемого алгоритма подсчета СКС, его можно заменить другим, — более быстродействующим, но эквивалентным алгоритмом. Действительно, поскольку младший байт пароля суммируется всего лишь один раз, то при переходе к следующему паролю его контрольная сумма в большинстве случаев увеличивается ровно на единицу. Именно "в большинстве" — потому, что при изменении второго и последующих байтов пароля модифицируемый байт уже суммируется как минимум дважды, к тому же постоянно попадает то в один, то в другой разряд. Это немного усложняет наш алгоритм, но все же не оставляет его далеко впереди "тупой" методики постоянного подсчета контрольной суммы, используемой ранее.
Словом, финальная реализация улучшенного переборшика паролей может выглядеть как в листинге 1.23. хпс реп рееб(сьап *спурсеббаса, сьап *репо, ьпс пах 1ьех, 1пс еа11бсас( ( 1пс а, Ь, х: 1пп р = О; сь *Ьпег; 1пс у=о; ьпс ьо 1пс 1епзпп = аеп1еп(раеб(~ ьпс иаеК; га Глава ] х = -1; вол (Ь О! Ь < 1епяеЬ) Ъ++) + *(1пс *) ((бпв)рвеб + Ьб Еос(а = О; а < щах ]Сел; а++) ( 11 (х==са1]бСРС) ( спет = (скал *) ща11ос(вьв1еп(ссурвеббаьа) ); вь ру(Ьпее, урьеббаьа)! Оеесурь(рв б, Ьпее)! рс1пьт("СВС вВХ: Ссу Со бессурв: 1"%вы"М", са1]бСЯС,Ьстт); ) у к иЬ11е( (+ервиб[р) ) >' в') ( рвиб[р) = '! '! // следующий символ у-у ! у«в! х- )с; к«в) к + ('в'-'!'); $Н.~.
) ]Г ((рвиб[р]) ( раис([р] '!'; рвиб[р+1]=О; 1епяьп++! х -1) вою (Ь О! Ь < 1епдЕЬ) Ь++) х + *(кпе *) ((1пс)рвнб + Ь] ) О; рвеб[р]=' //рлвпсг("]хоп", у)) // епб иЬ11е(рвиб) р = О! х+ ) // епб гол(а) сеьссп О; Какой результат дала алгоритмическая оптимизапия? Ни за что не догадаетесь — восемьдесят трн миллиона паролей а секунду или 1/'1[] пароля за такт. Фантастика! И это при том, что программа написана на чистом С! И ведь самое забав- ное, что хороший резерв производительности по-прежнему есть! Профипировка программ Шаг девятый.
Чапе — ваш персональный тренер А теперь мы обратимся к наименее известному средству профилировщика 'чтиве — Инструктору (в оригинале Спас)) так же переводимое, как "тренер" или "учитель"). Фактически Инструктор — это ни что иное как высококлассный интерактивный оптимизатор, поддерживающий целый ряд языков: С, С++, Гопгап и )ача. Он анализирует исходный текст программы на предмет поиска "слабых" мест, а обнаружив такие, дает подробные рекомендации по их устранению.
Разумеется, интеллектуальность Инструктора не идет ни в какое сравнение с сообразительностью живого программиста и вообще, как мы увидим в дальнейшем, Инструктор скорее туп, чем умен, и все-таки рассмотреть его поближе будет небесполезно. Несмотря на то, что Инструктор в первую очередь ориентирован на программистов-новичков (на что указывает полностью "разжеванный" стиль подсказок), но и для профессионалов он под час оказывается не лишним, особенно когда приходится оптимизировать чужой код, в котором лень досконально разбираться. Плохая новость (впрочем, ее и следовало ожидать) — при отсутствии отладочной информации в профилируемой программе инструктор не может работать с исходным текстом и опускается на уровень чистого ассемблера (см.
раэд. "Шаг десятый. Заключительный" этой главы). Тем не менее, это обстоятельство не доставляет непреодолимых неудобств, т. к. текст программы именно аналиэируетсл, а не профилируется. Поэтому, пусть вас не смущает, что включение в исполняемый файл отладочной информации приводит к автоматическому "вырубанию" всех оптимизирующих опций компилятора. Инструктор, работая с исходным текстом программы, вообще не будет касаться скомпилированного машинного кода! Итак, перекомпилируем наш демонстрационный пример, добавив ключ гз' в командную строку компилятора и ключ говвзз — в командную строку линковщика.