К. Касперски - Техника оптимизации программ, Эффективное использование памяти (1127752), страница 30
Текст из файла (страница 30)
Препроцессор языка С не поддерживает циклических макросов и потому не позволяет реализовывать эффективную развертку циклов. Программист вынужден самостоятельно проделывать утомительную и чреватую ошибками работу по многократному дублированию тела цикла, не забывая к тому же о постоянной коррекции счетчика. "Астматики" находятся гораздо в лучшем положении, Развитые макросредства ассемблеров МАЯМ и ТАЯМ позволяют переложить всю рутинную работу на компилятор, и программисту ничего не стоит написать макрос, разворачивающий цикл какое угодно количество раз. Это необычайно облегчает отладку и оптимизацию программы! Одна из таких программ и приведена ниже (листинг 2.7) в качестве наглядной иллюстрации. Согласитесь, элегантно решить эту (отнюдь не надуманную!) задачу штатными средствами А О)Я! С/С++ физически невоаиож//о/ /* макрос, дублирующий свое тело М раз НЕАО ВОГГ МАСНО Н мун = и МУА = О // цикл дублированиа своего тела ННГЬЕ МГА НЕ МУН // тело цикла // макропроцессор продублирует его заданное число раз МОЧ ЕОХ, [ЕВХ т 32 * МУА1 МУА = МУА е 1 ЕНОМ ОМНОЬЬ РОМЕН ЕСО а ; // глубина разворота цикла ьоор: ВЕАО ВОГГ ОННОЬЬ РОНЕН Оперативная память // // // обратите внимание, глубина разворота задается препроцессорной константой! никакой ручной работы! АРО ЕАХ, ЕРХ АРО ЕВХ, 4 * ОМНОЬЬ РОИЕН // коррекция " """"" "" " количества итераций ОЕС ЕСХ ле ьоор Разворот цикла средствами препроцессора С Хотя автоматически развернуть цикл директивами препроцессора АМБ! С/С++ невозможно, пути изгнания рутины из жизни программиста все- таки существуют! Стоит только подумать.
Одна из идей состоит в отказе от модификации счетчика цикла внутри заго- ловка цикла и выполнении этой операции внутри макроса "продразверстки". В результате необходимость ручной коррекции счетчика отпадает, и все ко- пии тела цикла становятся идентичны друг другу. А раз так, то почему бы их не поместить в препроцессорный макрос? Это можно сделать, например, так, как показано в следующем примере (листинг 2.8). Фбезеле воот хт=р~аты; // тело цнкяа Гог(а-О; а < ВЬОСК 81ЕЕО ( // разворот цикла в 8 раз вбот; воо/; вост; вбоу; ЕООУ; ВОРУ/ ВОРУ; ВООХ; Устранение зависимостей по данным Если запрашиваемые ячейки оперативной памяти имеют адресную зависимость по данным (т. е., попросту говоря, одна ячейка содержит адрес другой), процессор не может обрабатывать их параллельно и вынужден простаивать в ожидании поступления адресов.
Рассмотрим это на следующем примере: иЛ11е~лехб=р~лехзм Глава 2 До тех пор, пока процессор не узнает значение переменной е, он не сможет приступить к загрузке следуюшей ячейки, т. к. еше не знает ее адреса. Время выполнения такого цикла определяется в основном латентностью подсистемы памяти и практически не зависит от ее пропускной способности. В этом случае и ЯРКАМ, и РРК ЯРКАМ, и даже сверхпроизводительная КРКАМ покажут практически одинаковый результат, над которым посмеялась бы и ЕРО РКАМ, будь она до сих пор жива.
Латентность же подсистемы памяти на современных компьютерах весьма велика и составляет приблизительно 20 тактов системной шины, что соответствует полному времени доступа в 200 нс. Прямую противоположность этому составляет цикл вида: ъш1е ~а=р[леха++1~ Процессор, отправив чипсету запрос на загрузку ячейки р ~ ае~, немедленно увеличивает переменную е на единицу и, не дожидаясь ответа (зачем? ведь адрес следуюшей ячейки известен), посылает чипсету еше один запрос. Потом еше один, и еше. Так продолжается до тех пор, пока количество необработанных запросов не достигает своего максимально допустимого значения (для Рб — четырех). Поскольку запросы следуют друг за другом с минимальным интервалом, то в первом приближении можно считать, что они обрабатываются параллельно.
И это действительно так! Если время загрузки Аг зависимых ячеек в общем случае равно: г = Аг (Т„+ т„„„], где Т,ь — латентность чипсета, а Та, — латентность памяти, то такое же количество независимых ячеек будет загружено за время, которое можно определить по следующей формуле: Аг ~ ~с11 + ~ пап1 С где С вЂ” пропускная способность подсистемы памяти. Таким образом, при обработке независимых данных пагубное влияние латентности подсистемы памяти в значительной мере ослабляется, и производительность определяется исключительно пропускной способностью.
Правда, достичь заявленного производителем количества мегабайт в секунду таким способом все равно не удастся (ведь число одновременно обрабатываемых запросов ограничено и полного параллелизма в этой схеме не достигается), но полученный результат будет, по крайней мере, не худшего порядка.
Наглядно сравнить скорость обработки зависимых и независимых данных позволяет следующая программа (листинг 2.9). (Полный исходный текст программы читатель найдет в файле ~згс~]2].гпетогу~дерепдепсе.с, который находится на прилагаемом компакт-диске.) >Э9 Оперативная память * цнкл чтения зависимых данных (неоптимизированный вариант) бог (а=б> а < 81ОСК 8128> а += Зг) /* """ мы разворачиваем цикл для более быстрой обработки */ // читаем ячейку х * (1пс *) ((ьпс)р1 ь а + О) ) // адрес следухщей ячейки вычисляется на основе значения предыдумей, // позтому процессор не может послать очередной запрос чипсету до тех пор, // пока не получит зту ячейку в свое распоряжение а+ х) // палыче — аналогично... У *(1пе *) ((1пг)р1 + а ь 4) а += у> х = *(1пп *) ((1пп)р1 + а + 8) а += х) у (ьпг *) ((1пе)р1 + а + 12) > а+= у> х = *(ьпе *) ((ьпе)р1 + а + 16)> а + х; у = *(ьпс *) ((апс>р1 е а е гб); а х = *(1пе *) ((1пс)р1 + а + 24) > а~х> у = *(1по *) ((1пе)р1 + а + 28) > а ':= у> 140 Глава й цикл чтения независимых данных (сптимизирсванньгг вариант) ссг (а=о) а<въоск 8128) а += 12) // теперь процессор может посылать очередной запрос чипсету, // не дсвидаясь завершения предыдуиегс, т.
к. адрес ячейки // никак не связан с обрабатываемыми данными *(1ПС *) ((1вь)р1 + а + О) ) у +-- *(1пе *) (стпв)р1 + *)((тпт)р1 *) ((тпт) р1 + *)((тпв)р1 *)((1ггт)р1 + *) ((1пг)р1 + *) ((гпг)р1 + ат 4)) а : 8)) а т 12)) а + 16)) а + 20)) а + 24)) а + 28)) * (1пс * (1 па *(1пв *(1пв *(шг * (тпс Результат тестирования двух компьютеров, имеющихся в распоряжении автора, представлен на диаграммах (рис.
2.15). Первое, что сразу бросается в глаза, — это значительный разрыв во времени обработки зависимых и независимых данных. В частности, на Р-Ш/733/133/100 цикл чтения независимых данных выполняется в два с половиной раза быстрее! Несколько худший результат показывает система АМ(3 А()1!оп-1050/100/133/1/1А КТ 133, что объясняется серьезными конструктивными недоработками этого чипсета (см. равд. "Вычисление полного времени доступа" этой главы). Недостаточная пропускная способность канала между контроллером памяти и блоком интерфейса с шиной (оба смонтированы в северном мосте чипсета) приводит к образованию постоянных заторов, и как следствие — к ограничению количества одновременно обрабатываемых запросов.
Тем не менее, даже в этом случае чтение независимых данных осуществляется намного эффективнее и весь вопрос в том, как именпо следует их обрабатывать. Линейное (оно же — последовательное) чтение ячеек памяти — не самая удачная идея, что и демонстрирует рис. 2.15. На Р-Ш мы не достигли и 60% от расчетной пропускной способности, а на АМ(3 А(11!оп и того меньше,— всего лишь немногим более 30%. "Потрясающая" производительность, не правда ли? Это что же такое получается?! Неужели архитектура РС настолько крива, что не позволяет справиться даже с такой простой штукой, как оперативная память? Кстати, мы не первые, кому эта "здравая" мысль при- Опера тивная память У41 шла в голову.
У большинства прикладных разработчиков существует весьма устойчивое убеждение, что РС вЂ” это просто "тормоз". "По жизни". Но не спешите пересаживаться на Сгау. Рис. 2.15. Тест пропускной способности оперативной памяти при линейном чтении зависимых и независимых данных. На "правильном" чипсете !п1е! 815ЕР независимые данные обрабатываются в два с половиной раза быстрее. На чипсете тт!А КТ133 (за счет его высокой латентности) различие в производительности намного меньше и составляет всего 1,7 крат.
Но, как бы то ни было, на любой системе обрабатывать зависимые данные крайне не выгодно. Обратите также внимание, что при линейном чтении данных заявленная пропускная способность (800 Ут(байт/с для данного типа памяти) не достигается Параллельная обработка данных Итак, обработка независимых данных выполняется намного быстрее, но насколько быстро она выполняется? Увы, если от относительных величин перейти к абсолютным цифрам, — весь восторг мгновенно улетучится и наступит глубокое уныние. Наивысшая пропускная способность, достигаемая при линейном чтении независимых данных, составляет не более чем 40 — 50% от завяленной пропускной способности данного типа памяти. И это притом, что подсистема 14г Глава 2 памяти лля линейного доступа как раз и оптимизирована, и хаотичное чтение ячеек происходит, по меньшей мере„на порядок медленнее.