К. Касперски - Техника оптимизации программ, Эффективное использование памяти (1127752), страница 78
Текст из файла (страница 78)
Решение, первым приходящее на ум, — перемежевать операции переноса данных с их предвыборкой, неверно. Правда, объяснить: почему оно неверно "на пальцах", не углубляясь в тонкости реализации шинных транзакций, невозможно. В общих чертах дело обстоит так: системная шина у процессора одна, но запросы на чтение/запись памяти делятся на множество фаз, которые могут перекрывать друг лруга. Полного параллелизма в такой схеме, естественно, не достигается и неупорядоченная обработка запросов несет свои издержки.
Уменьшеш1е количества транзакций между чтением и записью данных позволяет значительно ускорить доступ к памяти, что, в свою очередь, существенно увеличивает производительность системы. Слеловательно, целесообразнее выполнять предвыборку в одном, а копировать намять в другом цикле. Но вот вопрос — какими кусками "нарезать" память? В приведенном примере !п[е! рекомендует выбрать размер блока равным размеру кэша первого уровня для процессора Р-П! и половине каша второго лля Р-4 (т. к. у него команда предвыборки умеет загружать данные только в этот кэш). В то же время, описывая команду предвыборки в руководстве по оптимизации под процессор Р-4, 1пге! остерегает пересекать 4- килобайтовую границу страниц при предвыборке.
(Такое впечатление, что авторы технической документации из !п[е! понимают в выпускаемых их фирмой процессорах меньше, чем сторонние разработчики.) Итак, у нас имеются два противоречащих друг другу утверждения — какому же из них верить? Но зачем, собственно, верить? Наука — она тем от рели- две Глава 3 гии и отличается, что в первую очередь полагается не на авторитеты и логические умозаключения, а на эксперимен~.
Эксперимент же недвусмысленно "скажет", что наилучшая производительность достигается при предвыборке блоков размером В 4 Кбайт, т. е. одной страницы, а при увеличении размера начинает стремительно падать. Так, с этим мы разобрались. Идем дальше. Реализация цикла предвыборки (между нами говоря) представляет собой достойный "увековечивания" пример как не надо программировать. Смотрите сами: г )1=книги 1<хюсхсннззгн; ~~=е). То, что вместо схснннзгн должно стоять гхзензгг, мы уже выяснили, но вот величина шага прирашения цикла очень интересна.
Теоретически она должна быть равна размеру прелвыбираемой строки, т. е, 32 байта для процессора Р-!1! и 128 байт для Р-4. Откуда же злесь взялась четверка? Дело в том, что данный пример копирует массив, состояший из элементов типа Оооъте, кажлый ИЗ хо~~рык Им~ет раз~~р 8 байт (ПРО еггеог пари~ ИЗ 1ПГЕ!, конечно же, забыли), а 8*4 = 32! А если придется копировать данные другого типа? Так не лучше ли перейти от частностей к бестиповым указателям о? К тому же, данная реализация процессорно-зависима и неоптимальна для процессора Р-4. Правильным решением будет определять тип процессора при старте программы и выбирать соответствующий ему шаг цикла— 32 байта для Р-1!! и 128 байта для Р-4.
Следуюший на очереди цикл копирования памяти. В примере 1)ие! допущена еще одна грубая ошибка (не иначе, как документацию писали не на трезвую голову) — тое Ю=хю з<«+снсннвггн: >е=е) — конечно же, здесь должно быть не схсннззгн, а, по крайней мере, схснеззгнге геогшоеые), а точнее даже — Рхсннтге/е~геогшоеь)е). ЭтОт ляп испраВлен лишь В рукоВодстВе по оптимизации под процессор Р-4, где вместо схснезтгн фигурирует нниггнньзн — количество элементов на страницу. Это уже лучше, но все же остается непонятным стремление авторов документации любой ценой избе- жаТЬ ОбРВШЕНИЯ К енгеок Тело цикла состоит из нескольких подряд идущих команд переноса 128- битового (16-байтового) блока памяти — в примере из руководства по процессору Р-П! их две, а процессору Р-4 — аж восемь! Зачем же нам столько, когда достаточно и одной? А затем — чем больше операций копирования выполняется в каждой итерации цикла, тем меньше накладные расходы на организацию этого цикла.
Различие же в количестве операций в обоих процессорах объясняется тем, что на процессоре Р-1П размер кэш-линий равен 32 байта, а на Р-4 — 128 байт. Поскольку каждая операция копирования переносит !б байт памяти, легко видеть, что В обоих случаях за одну итерацию цикла кэш-линия обрабатывается целиком, а задержки, связанные с поддержкой цикла, приурочиваются к переходу на следующую кэш-линию, что обеспечивает максимально воз- можную производительность. Любопытно, что цикл переноса памяти, оптимизированный под процессор Р-4, ничуть не хуже чувствует себя и на Р-Ш, а вот пример, оптимизированный под процессор Р-П1, попав на Р-4, показывает далеко не лучший результат. Универсальным решением'будет перенос 128-байтового блока памяти за каждую итерацию — это оптимально для обоих процессоров.
Теперь остается решить лишь технические проблемы. Во-первых, рассмотреть случай копирования блока памяти, размер которого не кратен размеру страницы — оставшийся "хвост" последней страницы придется переносить отдельно. Во-вторых, команда некэшируемой записи требует 1а команда чтения рекомендует), чтобы данные были выровнены по !б-байтовой границе. В противном случае генерируется исключение. Вообще-то, требование выравнивания можно просто оговорить в спецификации функции, но будет лучше, если функция автоматически выровняет переданные ей указатели, не "забыв", конечно, скопировать остаток обычным способом.
И, наконец, последнее: в фирменном примере присутствует бессмысленная На ПсрВЫй ВЗГЛЯД КОНСтруКЦИЯ: Севр =. в (КХ+НСНРЕРРХСЕ(; // т(.В рг~н1РЧ— для чего же она предназначена? Программистам, знакомым с защищенным режимом, должно быть известно, что для трансляции виртуальных адресов в физические процессор обращается к специальной структуре данных— страничному каталогу. Страничный каталог формируется операционной системой и хранится в оперативной памяти. А оперативная память — это "тормоза". Поэтому данные страниц, к которым обращались в последнее время, автоматически запоминаются в специальном кэше — бункере ассоциативной трансляции 1Т1, — Тгапеасг)оп 1оо)( азвйе Вийег).
Разумеется, данных о станицах, к которым еще не обращались, в Т).В нет. Конструкция С р = в(ХХМЮНРЕРРХСЕ(; КаК раэ И ЗаГружаЕт даННЫЕ СЛЕдуЮ(цЕй КОПИруЕМОй страницы в Т1.В. К сожалению, это рождает проблему выхода за границы копируемого массива !ведь в последней итерации цикла мы обращаемся к возможно не существующей странице за его концом и если не предпринять дополнительных мер, то операционная система может выбросить исключение, а дополнительные меры — это лишние накладные расходы). Ко всему прочему, оптимизирующий компилятор просто уничтожит бессмысленное 1с его точки зрения!) присвоение — ведь переменная с р никак не используется в программе! Приведенный далее пример !листинг 3.38) реализации функции турбокопирования для оптимизации под конкретный процессор использует два определения: Рясе ззге — в обоих случаях равное 4 Кбайт, и Ряегетсн зтае— равное 32 байтам для процессора Р-Ш и 128 для Р-4.
Адреса источника и приемника должны быть выровнены по 1б-байтовой границе, а размер копируемого блока — кратен размеру страницы. Эти ограничения объясняются Глава 3 стремлением максимально упростить листинг для облегчения его понимания. (Полный исходный текст программы !листинг 3.38) читатель найдет в файле ~бгс),!31сас1)е~ гвгбо п)епзсру.б)ке.с, который находится на прилагаемом компакт-диске.) тотЬо мемсру(сбат *бвт, сват *втс, кпт 1еп) тпт а, ь, се~пр) кот ( =0) а < 1еп) а а= рхбк кткю // Предамбираем теор = ' Гкпт *)( Гкпт) втс а + раде втте)7 тот (Ь = ав Ь < а + рлсК 61КК) а += рВКККтен 61КК) ртегетсьпта(зтс<ь)) тот ш=а) ь<аа рлакбткк) ьа=(6*б) вттеап срубает ь + 16*0, втс < ь + 16*0) ) вттеап1 ору(бвт + Ь + 16*1, втс + ь + 16*1)) вттеам сруывт + Ь - 16*1, в + Ь + 16*1); вттеам ору(бвт Ь + 16*3, втс + Ь + 16*3); вттеа7в ору(бвт + о а 16*<, втс + Ь + 16" Е); вттеап1 ору(бвт а Ь + 16*6, втс + Ь + 16*6) ) вттеап1 ору швс + ь + 16*6, втс + ь + 16*6) ) вт еап1 ору швт + ь + 16*7, втс + ь + 16*7) ) тетотп сешр) Результаты тестирования функции турбокопирования на блоках памяти различного размера приведены на рис.
3.50 и 3.5!. Впечатляющее зрелише, не так ли? !Особенно в свете того, что функция копирования написана на чистом С.) Но и зто еще не предел! Во-первых, переписав код на ассемблер, можно на несколько процентов увеличить его производительность, добившись, по крайней мере, пятикрат- Кзш ного превосходства над штатной функцией пвз ру при копировании небольших блоков.
Рис. 3.50. Результаты тестирования производительности функции турбокопнровання на блоках памяти различного размера под процессором Репбцгпип 733/133/100. Скорость копирования измеряется в относительных единицах в сравнении со штатной функцией аезсру, производительность которой принята за 100% Во-вторых, представляет интерес рассмотреть частные случаи оптимизации.
Например, если копируемые данные уже находятся к кэше (что очень часто и происходит при обработке блоков данных небольших размеров)„то цикл предвыборки можно исключить. (Особенно это актуально для программ, выполнякпцихся под процессором Р-4, команда предвыборки которого загружает данные в кэш второго, а не первого уровня.) При копировании блоков большого размера, обрабатываемых с начала, напротив, целесообразно использовать дополнительный цикл предвыборки, загружавший скопированные данные в кэш. Впрочем, оптимальная стратегия копирования очень сильно зависит от конкретной ситуации и универсальных решений не сушествует, поскольку каждая программа требует своего подхода. Поэтому не будем давать готовых решений, оставляя читателям полную свободу творчества. Глава 3 Рис.