К. Касперски - Техника оптимизации программ, Эффективное использование памяти (1127752), страница 61
Текст из файла (страница 61)
Что отсюда следует? Рассмотрим следующий пример: асасьс хаь а; Зьдьтс сьдс Ьл аьдььс ььс аьааас сьас д; Компилятор, выравнивая переменную с по адресу, кратному четырем, пропускает три байта, следующих за переменной ь, образуя незанятую "дырку", напрасно "отьедаюшую" память. А вот если объявить переменные так: асасзс ась д/ аьаьтс апь с; аьаьтс сьас Ь; асассс сьас д; компилятор расположит их вплотную друг другу, ие оставляя никаких "дыр"! Обязательно возьмите этот трюк на вооружение! (Однако помните, что в этом случае цикл наподобие г ы/~ ь = д будет выполняться достаточно неэффективно, т.
к. переменные ь и д попадут в один банк, и это делает ие- Кэш за возможной их синхронную обработку — подробнее см. разд. "Стратегия распределения данных ло кэ[и-банкам" этой главы.) Автоматические переменные (т. е. обыкновенные локальные переменные) независимо от своего размера большинством компиляторов выравниваются по адресам, кратным четырем.
Это связано с тем, что машинные команды помещения и снятия данных со стека работают с одним типом данных — 4- байтовыми двойными словами, поэтому переменные типа оьье занимают в стеке ровно столько же места, как и ° е. И никакой перегруппировкой пе- ременных при их объявлении от "пустот" избавиться не удастся. Локальные массивы также всегда расширяются до размеров, кратных четы- рем, т. е.
сваг а[11] и сьат ь[[2[ занимают в памяти одинаковое количество места. (Впрочем, это утверждение не относится к массивам переменных ти- па о~, т. к. поскольку размер каждого элемента массива равен четырем бай- там — длина массива всегда кратна четырем.) Другой тонкий момент — поскольку локальные переменные адресуются от- носительно вершины стека, на этапе компиляции адрес их размещения еще неизвестен! Он определяется значительно позже, т. е. в ходе исполнения программы, и зависит и от операционной системы, и от потребностей всех ранее вызванных в стеке функций, и да мало ли еще от чего! Как же в этой ситуации осуществить выравнивание? Компилятор Вог)апд С++, не мудрствуя лукаво, при входе в функцию про- сто обнуляет четыре младших бита регистра-указателя вершины стека, ок- ругляя его тем самым до адреса, кратного шестнадцати.
Компилятор же М[сгозой Чиша[ С+~-, так сказать, пускает все на самотек, "рассуждая" следующим образом: поскольку размеры всех локальных пере- менных насильно округляются до величин, кратных четырем, а начальное значение регистра-указателя стека также гарантированно кратно четырем (за это ручается операционная система), то в любой точке программы значение регистра указателя вершины стека всегда кратно четырем и никакого допол- нительного выравнивания ему не требуется.
Динамические переменные размещаются в куче — области памяти, выделен- ной специальной функцией наподобие [тос. Степень выравнивания (если таковая имеется) у каждой функции может быть своя. Например, па[~ ос вы- равнивает выделяемые ей регионы памяти по адресам, кратным 16. Структуры. По умолчанию каждый элемент структуры выравнивается на ве- личину, равную его размеру (т. е. переменные типа сье~ — вообще не вы- равниваются, яловое ое — выравниваются по четным адресам, е и [т адресам, кратным четырем и о ые с ьо~ы — восьми). Как уже отмеча- лось выше, это не самая лучшая стратегия, к тому же выравнивание струк- тур, работающих с сетевыми протоколами, оборудованием, типизированны- ми файлами, категорически недопустимо! Как же запретить компилятору Глава 3 самовольничать? Универсальных решений нет, поскольку управление выравниванием не стандартизовано и специфично лля каждого компилятора.
Например, компиляторы М!сгозой Чкша! С-ь+ и Вот!ап(( С-ь+ поддерживают пРагмУ раск, задающУю тРебУемУю степень выРавниваниЯ. НапРимеР, №ркасаа расК(1) ЗаСтаВЛяЕт КОМПИЛятОр ВЫраВНИВатЬ ЭЛЕМЕНТЫ ВСЕХ ПОСЛЕдуЮ- ших структур по адресам, кратным единице, т. е, не выравнивать их вообше. Аналогичную роль играют ключи командной строки: гзо. (для М(сгозой У!ьца! С++) и — . (лля Вог!ап(! С++), где — "." степень выравнивания.
Однако было бы неразумно полностью отказываться от выравнивания структур — если внутреннее представление структуры некритично, выравнивание ее элементов может значительно ускорить работу программы. К счастью, действие пРагмы р к, в отличие от соответствУющих ей ключей командной строки, — локально. Таким образом, в тексте программы р к может встречаться неоднократно и с различными степенями выравнивания. Более того, прагма р .«может сохранить предылушее значение степени вы- РаВНИВаНИЯ, а ЗатЕМ ВОССтаНОВИтЬ ЕГΠ— расК(роаш ЗаПОМИНаЕт, а расК(рор) извлекает значение выравнивания из внутреннего стека компилятора.
Разумеется, вызовы рось/рор могут быть вложенными. Приведем пример их использования. №рсасааа расК(риаш №рсаяпа расК(1) аосос№ (р Наавва( №рсасса расК (рор) Если программу не предполагается использовать на процессорах ниже чем Р-11, достаточно выровнять начало структуры по адресу, кратному 32 и, если размер структуры не превышает 32 байт, о выравнивании кажлого элемента можно вообше забыть. Стратегия оптимального выравнивания Если обращение к данным, пересекаюшим кэш-строку, происходит лишь эпизодически, то никакого "криминала" в этом нет, и накладными расходами на дополнительную задержку можно смело пренебречь.
Никакой осязаемой выгоды от выравнивания таких переменных мы все равно не получим. Иитеисивио используемые переменные (например, счетчики цикла) — дело другое. В этом случае потери производительности скорее всего окажутся весьма велики, и по меньшей мере неразумно "закрывать на это глаза"! Чтобы окончательно убедиться в этом, запустим следующий контрольный при- Кэш мер !листинг 3.5), используюший в качестве счетчика цикла 32-разрядную переменную, смещенную на 62 байта от начала, гарантируя тем самым ее расщепление и на Р-П/Р-П1, и на АМП А(!йоп. !Полный исходный текст программы читатель найдет в файле ~его(,!3!.Сас)(е~а!!йп.Гог.с, который находится на прилагаемом компакт-диске.) // кол-во итераций цикла // максимально возможный О размер кзш-линии (бегапе Н гтЕН (ООО (бе(ипе НАХ САСНЕ ЫНЕ Етгв Фбегтпе Он гох (ь(1пт.((мпюгок ь ивх схсне ыне е(ге — я1зеог(1пт(/2ы ((бе11пе ЕОХ (*гох( // определение выровненного (ГОХ( и // невыровненного ШН ЕОХ( счетчиков // выделяем память ток = (1пт *( ыа11ос32(НАУ.
САСНЕ ЫНЕ З(ЕЕ*2( оптимизированный вариант (счетчик ьтчкла ие разбивает кэк-строку( гол(есх = О/ есх < н гтею ЕОХ+=1( с++; * (счетчик цикла разбивает кэз-сарову! гогшн гох = о( сн гох < н ттен( сн гох+=1( сн-/ Прогон программы показывает !рис. 3.2б), что на процессоре Р-П1 733 расщепленный счетчик цикла снижает производительность практически в пять раз, а на АМВ А(1(!оп падение производительности не достигает и двух крат, подтверждая тем самым что А()(!оп — чрезвычайно непритязательный к выравниванию процессор. И если бы компьютерный мир ограничивался бы одним А()(!оп, — данные можно было бы вообше не выравнивать !шутка) Глава 3 350 Рис.
3.2В. Снижение производительности при обработке расщепленных данных Обработка массивов. Несколько иначе обстоят дела с последовательной обработкой массивов данных. Этот вопрос мы уже рассматривали в главе 2 настоящей книги, но тогда речь шла о взаимодействии с оперативной памятью, а время загрузки ячейки из основной оперативной памяти намного превышает штрафное пенальти конфликта кэш-линий, поэтому мы, как помнится, пришли к выводу, что потоковые данные выравнивать бессмысленно.
Если же данные целиком умещаются в кэш-памяти первого (второго) уровня, то пагубное влияние кэш-конфликтов достигает весьма значительных величин, но только на неразвернутых циклах. Воспользуемся несколько модернизированной программой ~его~[2!.гпептогу'~а113п.с, находящейся на прилагаемом компакт-диске, чтобы изучить этот вопрос подробнее. Уменьшим размер блока до 8 — 16 Кбайт и запустим программу. На неразвернутом цикле чтения (рис. 3.27) даже процессор Р-!Н показал всего лишь 7% падения производительности, чего и следовало ожидать, т.
к. во время кэш-конфликта процессор не простаивает, а обрабатывает команды, составляющие тело цикла, практически полностью маскируя задержку. Кзш Рис. 3.27. Влияние выравнивания данных на производительность А вот неразвернутый цикл записи ой-ой-ой! Четырехкратное отставание по скорости! Чем же вызвано это, как бы помягче сказать, безобразие? А вот чем: "хитрый" компилятор М!сгозой Ч!зпа! С++ заменил цикл записи всего одной машинной командой ага зтозо, чем к минимуму свел накладные расходы на организацию цикла.
Кроме того, поскольку длина буферов записи равна длине кэш-линейки совмещение начального адреса записи с адресом начала кэш-линейки, обеспсчивает эффективную трансляцию адресов, позволяя выгружать весь 32-байтовый буфер всего за один такт. На фоне этого 100%-ный результат АМ!3 Аййоп выглядит весьма сильно, правда, при развороте циклов он все же начинает сдавать, отставая от выровненного варианта на 24% при чтении и на 48% при записи данных. Впрочем, Р-П! выглядит еще слабее: +74% и +!33% соответственно.
Глава 3 з(г Стратегия распределения данных по кэш-банкам В выравнивании данных есть еше один далеко неочевидный момент, по непонятным обстоятельствам упущенный составителями оригинальных руководств по оптимизации пол процессоры Репйпш и АМО, а потому практически неизвестный программистской общественности.