К. Касперски - Техника оптимизации программ, Эффективное использование памяти (1127752), страница 60
Текст из файла (страница 60)
Кзш Выход за пределы кзша второго уровня Во-первых, не забывайте, что кэш второго уровня хранит не только код, но и данные. Как было показано в предыдущей главе, эффективная емкость кэша второго уровня не всегда совпадает с физической, — ведь исполняемый код и обрабатываемые данные могут претендовать на одни и те же кэш-линейки, в результате чего падение производительности начнется задолго до того, как алгебраическая сумма размеров интенсивно исполняемого кода и обрабатываемых им данных превысит размер каша второго уровня.
Причем, падание производительности будет нет — даже не обвальным, а по настоящему ошеломляющим — порядка тридцати (!) крат на процессоре АМ0 Абйоп и шести — на Р-П) (рис. 3.23). Так никаких мегагерц процессора не хватит! Впрочем, с выходом исполняемого кода за границы кэша второго уровня приходится сталкиваться не так уж и часто, ну, а если и приходится, то практически всегда удается разбить его на несколько циклов меньшего размера, обрабатывающихся последовательно. Таким образом, при разработке программы стремитесь проектировать ее так, чтобы все интенсивно используемые пиплы вмеигались в кзи первого или по крайней мере второго уровня.
Рис. Згвэ. Изменение удельного времени выполнения команд в зависимости ст размера исполняемого кода зог Глава 3 Выравнивание данных С выравниваем данных мы уже сталкивались в главе 2 настоящей книги, но тогда речь шла об обработке больших блоков данных, намного превышающих собой емкость кэш-памяти всех иерархий. Здесь же мы будем говорить об эффективности выравнивая данных, полностью умещающихся в сверхоперативной памяти первого (второго) уровней. Как мы увидим, это совсем не олно и то же! Широко распространенное заблужленис гласит, что обращение к невыровненным ланным всегда происходит медленнее, чем к выровненным.
Возьмем получивший большое хождение в сети Интернет перевод Агнера Фога, выполненный Дмитрием Померанцевым. Цитирую: "На невыровнениые данные потребуется, по крайней мере, на 3 такта болыие для доступа". К сожалению, я не нашел оригинала переводимого текста, но в имеющейся у меня версии та же самая фраза звучит так: "Оп РР1тп апд РММХ, т!заЬВпед г(ага яч(( (айе аг (еав( 3 с(ос(г сус(ев ех(га (о асеево 1('а 4 буге Ьоипдагу (л сговвед. 'ГЬе ргпа!(у (з (иййег и Ьеп а гасйе Йпе Ьоипдагу ц сгоззед".
Оп РРго, РН апг( РН(, тйайдпед дага яч(( созг уои б — (2 с(ос(гл ех(га мйеп а саейе Впе Ьоипдагу и сгозтед. Мзай ед о егапдз ь а((ег (Ьап lб Ь ге (~а( до пог сгогз а 32 Ь ге Ьоипдаг гхе ао еноту (Выделение и подчеркивание мое — К. К.) ". ("Ластуп к невыровненцым данным на процессорат Репбит и Реппат ММХ отнимает по крайней мере на три такта больисе если бкьга пересечена 4-байтовая граница При пересечении гранпцы кэш-линейки задерэкка окажется егце большей. На Репгат Рго, Р-Н и Р-(Н обработка невыровненных данных, пересекаюгцих границьс кэш-линейки, будет вам стоить от б до 12 дополнительных тактов. Невыровнениые операнды, имеюгцие разл~ер л~еньте (б байт, не образуют пенальти при условии, что они целиком содержатся в одной кэиылинейке".) Я отнюдь не собираюсь упрекать Дмитрия Померанцева в чем бы то ни было (судя по всему, в его версии руководства о современных процессорах ничего и не говорилось, хотя на момент выполнения перевола локументация по этим процессорам уже имелась и свободно распространялась фирмами 1пгс! и АМ0).
Тем не менее, его перевод прочитали тысячи, если не десятки тысяч программистов, а с первоисточником ознакомилось значительно меньшее количество людей (ну. не все же свободно читают по-английски!). Как слелствие — подавляющее большинство разработчиков оказались введены в заблужление. Вопреки расхожему мнению, чтение двойного слова, начинающегося, скажем, с адреса Ох40001, для современных процессоров вполне законно и осуществляется безо всяких задержек (по репайу!), а все потому, что оно гарантированно нс пересекает кэш-линию. От конца читаемой ячейки до начала следующей кэш-линии остается 5ЕТАО1)ВЕ5 + САСНЕ.Ы)х)Е.512 Ев — гйвсо((сей) — АЕ)Е)ВЕ55 = ((Ох40001 + Ох20) — 4 — Ох40001) = 27 байт, сле- зоз Кзш довательно, запрашиваемые данные целиком находятся в одной кэш-линейке и выравнивать их нет никакой необходимости. Разумеется, такая стратегия выравнивания "встретит серьезные возражения" со стороны процессоров Репйшп Р!а1п (Репт1цгп-просто) и Рептшш ММХ.
Но сколько таких процессоров еще осталось? На сегодняшний день их парк сократился практически до нуля и с каждым днем продолжает неотвратимо уменьшаться. (Если верить 'Тласу Рунета" на середину 2001 года только !5% пользователей обладали процессором "просто" Реп!(пт.) Поэтому, все, сказанное далее, относится только к процессорам, базирующимся на ядрах К5/Рб или более старших.
Обработка "расщепленных" (Ипе-арИпт) данных Чтение данных размером в байт, слово и двойное слово, целиком нахолящихся в одной кэш-линейке сверхоперативной памяти первого уровня, процессоры семейства Рб осуществляют за 1 такт. Если же данные выходят за границу 32- (б4-/128-) байтовой кэш-линейки и своим "хвостом" попадают в следующую кэш-линейку (рис. 3.24), — эта операция отнимает уже от 6 до 12 тактов, а сами данные называются "расщепленными" (от английского!1ле-зр11лг). Рис.
3.24. Если читаемые данные начинаются в одной, а заканчиваются в другой строке каша, при их обработке возникает задержка Так происходит потому, что: во-первых, процессор не может за один такт считать сразу две кэш-линейки, — хотя кэш-память и двухпортовая (т. е. Глава 3 304 поддерживает параллельную обработку двух кэш-линеек), блок взаимодействия с памятью (Т.ааг( Ииг) способен загружать только одну ячейку за каждый такт. Точнее, полное время загрузки отнимает как минимум три такта, но при благоприятном стечении обстоятельств конвейер загружает по одной ячейке за каждый такт, ликвидируя тем самым ее трехтактовую латентность.
Расщепленные данные блокируют конвейер и хвостовые ячейки начинают обрабатываться только после завершения обработки головы. Отсюда: минимальное время загрузки расщепленных данных составляет з*ь в.е !.ь«с«у == з*з == е тактов. Во-вторых, обрабатывая расщепленные данные, кэш-контроллер вынужден производить дополнительные расчеты, "прикидывая" как две половинки объединить в одну, что тоже обходится не бесплатно. Гораздо лучше справляется с чтением расшепленных ланных процессор АМ 0 А!11!оп, теряющий при этом всего один такт, а то и вовсе не теряющий ничего (хотя последнее утверждение и вступает в противоречие с документацией, оно подтверждается экспериментально).
Это объясняется тем, что А!)з!оп имеет две очереди запросов на загрузку данных из кэш-памяти первого уровня и потому может считывать сразу обе кэш-линейки. А вот с записью расшепленных данных процессор Аг)!!оп справляется несравнимо хуже, поскольку очередь на запись у него всего олна. Тем не менее, за счет эффективного и грамотно спроектированного механизма буферизации записи возникающая при этом задержка приблизительно в полтора раза меньше, чем на процессорах Р-11/Р-П1. На обоих процессорах запись расшепленных данных вызывает блокировку опережаюшей записи Гзгаге )аг!«агс((п№), что зачастую оборачивается весьма внушительным падением производительности (см. разд.:Особенности буферизации записи этой главы). Некоторые машинные инструкции (в частности новхез) требуют обязательного выравнивания своих операндов.
Попытка "скармливания" невыровненных данных (независимо от того, пересекают они границу кзш-пинейки или нет) заканчиваегся "выбрасыванием" исключения. В этом случае необходимость выравнивания оговаривается в описании данной машинной инструкции. Взять, например, описание уже упомянутой инструкции ночхвз "...Ю(пеп Фв зоигсе ог оез1(па(юп орегапг( (з а гпеглогу орегапб, !Ье орегало пшз1 Ье айдпей оп а 16-Ьуге Ьоипоагу ог а делега(-рго(есгюл ехсер1юл (№ОР) (з делега1еси (" Когда операнд-источник или операнд-приемник представляет собой «операнд в памятия, он должен быть выровнен ло 16-байтовой границы, в противном случае сработает исключение общей защиты — депегг(-рго(ес!юп ехсер(юп №ОР",) Естественное (па1ига1) выравнивание данных Данные размером в 1 байт, очевидно, никогда не пересекают кэш-строки, поэтому никакого выравнивания для них и не требуется.
Данные размером в 305 Кзш слово, начинающиеся с четных адресов, также гарантированно умещаются в олпу кэш-строку. Наконец, двойные слова, начинающиеся с адресов, деля- щихся на четыре без остатка, никогда не пересекают границу кэш-строк (рис. 3.25). 32-байтовая кзш-строка 2 ~ ю~лтФВч: иааааиааааааааааааиа~аааааа ' двойные слова Рис. 3.26. Естественное выравнивание данных Обобщив сказанное, мы получим следующую таблицу (табл. 3.5).
Степень выравнивания дпя процессоров Размер данных Р Рто, Р-И, Р-!11 произвольная А1Ыоп произвольная кратная 2 байтам кратная 2 байтам кратная 4 байтам кратная 4 байтам кратная 8 байтам кратная 8 байтам кратная 8 байтам кратная 8 байтам кратная 16 байтам кратная 16 байтам кратная 16 байтам кратная 16 байтам Естественное выравнивание данных так же называют "выравниванием (для/ перестраховщиков" ахи "выравниванием (для/ бюрократов", поскольку оно исходит из худшего случая, когда выравниваемые данные пересекают обе кэшлинейки, не учитывая лействительной ситуации. Платой за это становится увеличение количества потребляемой приложением памяти, что в ряле случаев приводит к значительному падению производительности (в общем, за что боролись, на то и напоролись).
1 байт (8 битов) 2 байта (16 битов) 4 байта (32 бита) 6 байт(48 бит) 8 байтов (64 бита) 10 байтов (80 битов) 16 байтов (128 битов) Таблица 2.5. Предпочтительная степень выравнивания для различных типов данных Р-Р1а(п, Р ММХ произвольная кратная 2 байтам кратная 4 байтам кратная 4 байтам кратная 8 байтам кратная 16 байтам кратная 16 байтам Глава 3 Как компиляторы выравнивают данные Большинство компиляторов все заботы о выравнивании данных берут на себя — прикладной программист об этих проблемах может даже не задумываться. Однако стратегия, используемая компиляторами, не всегда эффективна, и в критических ситуациях без "живой головы" не обойтись! Стратегия выравнивания для каждого типа переменных своя: статические (з/а//с) и глобальные ®оба!) переменные большинство компиляторов насильно выравнивают в соответствии с их размером, игнорируя при этом кратность выравнивая, заданную прагмой ра х или соответствующим ключом компилятора! Во всяком случае, именно так поступают М)сгозой Н!ьца! С++ и Вот!апг! С++, Причем оба этих компилятора (как, впрочем, и подавляющее большинство других) не дают себе труда организовать переменные оптимальным образом, и размещают их в памяти в строгом соответствии с порядком объявления в программе.