Prescott: Последний из могикан? (Pentium 4: от Willamette до Prescott). Часть 4

Глава восьмая, в которой мы оказываемся на "краю Ойкумены"

Сколь бы не был увлекателен процесс углубления в особенности работы процессора Pentium 4, размер посвященной ему статьи не может быть бесконечным. Мы и без того неумолимо подбираемся к размерам, характерным скорее для монографии. Наступило время описать нечто, обнаруженное нами в процессе изучения микроархитектуры Pentium 4. По сути, это самое нечто и послужило спусковым механизмом, инициировавшим написание цикла статей о Pentium 4. Тем паче, что это нечто оказалось весьма таинственным как по устройству, так и по части описания в документации.
Данное мероприятие достойно занесения в летопись. Вот как это было.

Спустя некоторое время после начала работы, "вчерне" набросав материал, который читатель увидел в первых семи главах, наш "летописец" для себя решил, что основные черты микроархитектуры Pentium 4 примерно понятны. Пребывая в восторге от ожидавшегося вскоре выхода статьи, мы стали доделывать некоторые мелочи: проверять цифры латентности кэшей и сравнивать результаты с указанными в документации величинами. Хотя никаких сомнений в правдивости документации мы не испытывали, но методика измерения была отработана в процессе работы группы соавторов над предыдущей статьей (о микроархитектуре Athlon64/Opteron), а потому сравнение напрашивалось само собой. Тем более, что еще в процессе работы над статьей про Athlon64/Opteron мы заметили несколько странное поведение процессора Pentium 4: при измерении латентности кэша второго уровня получались загадочные результаты. Эти результаты необходимо было объяснить, хотя бы для самих себя.
В этом месте так и просится фраза наподобие: "результаты казались вполне очевидными, мир был молод, солнце ярким, а мы – дерзкими и наивными". :D Не станем отказывать себе в этой мелочи.

Итак:
Результаты казались вполне очевидными, мир был молод, солнце ярким, а мы – дерзкими и наивными.
Тестирование процессора на ядре Northwood проводилось зависимой цепочкой команд вида mov eax, [eax] (так называемый pointer-chasing). Теоретически, здесь не должно было быть никаких сюрпризов: в документации четко указано, что задержка доступа к кэшу первого уровня составляет два такта, а к кэшу второго уровня – 7 тактов. Другими словами, мы ожидали получить в качестве ответа суммарную задержку 9 тактов.
Наши впечатления от реальных результатов лучше всего выражаются словом "оторопь". "Беда нечаянно нагрянула" в том месте, где ее никто не ждал. Вместо четко оговоренного документацией значения латентности мы получили нечто невыразимое, больше похожее на кардиограмму сердечного больного.
Прежде всего, мы вообще не получили ожидаемых для Northwood-а (и указанных в документации!) 9 тактов! Вместо этого процессор умудрялся выдавать результаты в десятки тактов задержки. Складывалось впечатление, что команда "ушла бродить неизвестно где, и будет позднее". Просила не расходиться.
Казалось, для выяснения причин этого явления необходимо было провести вполне очевидные действия: в очередной раз обратиться к документации. Но не тут-то было! В документации сколько-нибудь подробное разъяснение этой ситуации отсутствовало напрочь. Руководства по оптимизации недоуменно разводили pdf-страницами и категорически не соглашались верить в то, что такие результаты вообще возможны.

Более того, подобное поведение процессора заставляло задавать себе вопрос, все ли принципиальные подсистемы микроархитектуры процессора Pentium 4 мы изучили? Нет ли в Pentium 4 какой-нибудь важной, но не описанной подсистемы? Оказывает ли замеченный нами странный эффект какое-либо влияние на производительность? Если да, то насколько велико это влияние? Будет ли подобное поведение наблюдаться в реальных программах?
Ну что ж, неожиданные трудности нас никогда не пугали. Лучшие умы засели за разработку и проверку всевозможных гипотез. К сожалению, вся очевидная гениальность этих гипотез ни капли не помогла: спустя немалое время, сквозь зубовный скрежет, нам пришлось признать, что описанные в документации подробности микроархитектуры никак не позволяют подробно объяснить вышеозначенное поведение.
"Оно само" так работает.

Разумеется, оставить ситуацию без объяснения было невозможно.
Параллельно поискам в документации мы сосредоточились на не менее "традиционных" способах добычи информации: шантажу, лести, а также подкупу высших должностных лиц корпорации Intel. :D Увы. Все эти "неоднократно проверенные временем подходы" не возымели действия. Нам же пришлось задуматься [по-настоящему] и, невзирая на опасность перегрева [и повального облысения] :D, написать некоторое количество тестового программного кода.
Кроме того, пришлось подробно разобрать доступную нам документацию в поисках хоть какого-нибудь объяснения. Такое объяснение (впрочем, весьма краткое; можно даже сказать, что лаконичное до гениальности) нам удалось обнаружить в виде несколько раз встречающегося в IA-32 Intel® Architecture Optimization Reference Manual слова replay. Кроме того, несколько раз это же слово встречалось на нескольких слайдах в ранних презентациях Pentium 4.

Поскольку в документации по поводу этого таинственного "реплея" больше ничего, кроме самого факта его существования, не нашлось, возникла идея поискать его описание в патентах Intel.
Пропустив подробности поиска, отметим, что, в конечном итоге, именно идея почитать патенты оказалась наиболее плодотворной. В частности, именно там мы нашли достаточно абстрактное описание специальной системы повторного запуска микроопераций, называемой replay. Система предназначена для повторного исполнения микроопераций, покинувших планировщик, и выполненных по тем или иным причинам неверно.

Таким образом, нам удалось найти некую таинственную подсистему процессора Pentium 4, подробного описания которой вообще нет ни в документации на процессор, ни в многочисленных статьях, посвященных микроархитектуре этого процессора. Согласитесь, достойная внимания находка! Особенно если учесть, что никаких "грандиозных открытий" в этом месте мы никак не планировали.
Нетрудно догадаться, что нас незамедлительно заинтересовало, как эта система влияет на результаты работы процессора. Тем более что внимательное изучение особенностей только что обнаруженной и ранее неизвестной широкой публике подсистемы всегда интереснее пересказа документации. В конце концов, должны же мы как-то объяснить аномальное поведение процессора!

Поэтому мы с удовольствием (и немалым интересом) взялись за этот таинственный реплей. Было интересно все, что удастся про него узнать. И, пожалуй, кое-что сделать удалось. Позволим себе некоторую нескромность (впрочем, надеемся, вполне заслуженную): данная работа является первым публичным описанием реплея не только среди русскоязычных статей, но и вообще в мире! Логично предположить, что реплей вполне мог быть тем кандидатом, который способен описать наблюдаемое нами отклонение поведения процессора от предсказанного теоретически.
Позднее мы пришли к следующему выводу: очень похоже на то, что мы нашли гораздо больше, чем искали. Этот самый реплей оказался очень интересным как с точки зрения прояснения некоторых загадочных черт микроархитектуры Pentium 4, так и с точки зрения его влияния на производительность.
Кроме того, в процессе изучения реплея нам пришлось использовать еще одну подсистему, название которой достаточно редко мелькает на страницах прессы – внутренние счетчики событий. Но, поскольку рассказ о них напрямую с общей темой статьи не связан, да и интерес они представляют лишь для узкоспециализированной публики, мы вынесли небольшой рассказ о счетчиках событий в Приложение 3.
Глава девятая, напоминающая нам, что за все на свете приходится платить

Перед тем, как мы перейдем к рассмотрению обнаруженного нами феномена по имени реплей, стоит уделить время повторению некоторых принципиальных моментов. Далее это послужит нам хорошую службу.
Прежде всего, напомним, что для некоего абстрактного процессора производительность будет зависеть от того, насколько плотно мы будем "кормить" командами его исполнительные устройства. В идеальном варианте каждый такт исполнительное устройство должно выполнять полезную работу. Обеспечить максимальную загрузку исполнительных устройств есть основная задача обслуживающей конвейер логики.
В нашем конкретном случае, в микроархитектуре NetBurst, это означает следующее: планировщик, который осуществляет непосредственную загрузку исполнительных устройств микрооперациями, должен предпринять все меры для того, чтобы исполнительные устройства простаивали как можно меньше. Запомним этот вывод, далее он нам понадобится.

Это была преамбула. Теперь зайдем несколько с другой стороны. Представим себе некий условный конвейер, в котором планировщик расположен непосредственно перед исполнительными устройствами. Тогда, как только планировщик отправил микрооперацию на исполнение, она подхватывает нужные нам операнды, и исполняется. Все просто замечательно! Если нашей следующей микрооперации понадобятся результаты предыдущей, она сможет их подхватить сразу же, как только эти результаты будут готовы. Планировщик может отправить на исполнение микрооперацию, операнды которой зависят от предыдущей команды, через некоторый промежуток времени. Этот промежуток определяется временем выполнения первой операции (точнее, временем готовности результата операции).
Все это хорошо, если планировщик находится непосредственно возле исполнительного устройства. Но мы прекрасно помним, что одной из ключевых задач идеологии NetBurst было наращивание рабочей частоты процессора. Что, в свою очередь, ведет к необходимости удлинять конвейер. Таким образом, между планировщиком и исполнительными устройствами появляется некоторое количество стадий конвейера. Они больше не рядом.
Само по себе большой проблемой это не является: можно отправлять микрооперации на исполнение "заранее", учитывая появившиеся стадии конвейера. Это означает, что планировщик теперь должен отправлять микрооперацию раньше на столько тактов, сколько стадий между ним и исполнительными устройствами. То есть, отправлять команды заранее, не зная, чем закончилось исполнение предыдущей команды.

Чего мы этим добились? На втором такте планировщик, выпустив на конвейер первую микрооперацию, уже выберет следующую из очереди. Ее он также отправит по конвейеру и займется третьей микрооперацией. И так далее – планировщик выпускает команды на конвейер, исполнительные устройства их исполняют, мы наблюдаем идеальный конвейер в работе. К моменту исполнения первой микрооперации остальные находятся "на подходе", занимая различные стадии конвейера, а планировщик яростно и целеустремленно обрабатывает очередную микрооперацию.
Таким методом мы добились максимально высокого темпа обработки микроопераций: каждый интервал времени наши исполнительные устройства обрабатывают новую микрооперацию. В идеальном случае никаких простоев в нашем варианте возникнуть не должно, и производительность нашей конструкции будет максимально возможной.
Но сделаем остановку и подробнее обсудим ситуацию вокруг отправки команд "заранее". Обратим внимание на тот факт, что планировщик отсылает микрооперацию на исполнение таким образом, что к моменту её прибытия на функциональное устройство все операнды должны быть вычислены. Так как путь до исполнительного устройства включает несколько стадий (тактов), планировщик должен оценивать состояние готовности на такое же число тактов вперед. При этом он обязан учитывать время выполнения предыдущих микроопераций, если их результат будет использоваться в качестве операнда для данной инструкции. Когда латентность операций фиксирована (и таким образом заранее известна), задача решается элементарным образом. Однако для ряда инструкций время исполнения заранее предугадать принципиально невозможно: например, для операций загрузки из памяти время получения результата будет зависеть от того, на каком иерархическом уровне подсистемы кэша/памяти находятся запрашиваемые данные.

Таким образом, все микрооперации, с точки зрения планировщика, делятся на две большие группы: группа микроопераций с фиксированным и заранее известным временем исполнения и группа микроопераций, не имеющих фиксированного времени исполнения.
Нетрудно понять, что первая группа никаких неприятных сюрпризов планировщику не несет: если сказано, что сложение выполняется один такт, значит, на следующий такт результатами этого сложения уже можно будет воспользоваться. А на исполнительное устройство на следующий такт можно будет отослать очередную операцию, тем самым выполняя "главный завет стахановца" - как можно эффективнее загружать исполнительные устройства.
При появлении микроопераций второй группы, если мы не хотим останавливать конвейер, у планировщика есть не так много принципиально различных вариантов дальнейших действий. Пусть, для определенности, на планировщик пришла команда загрузки из памяти.

Выход первый (осторожно-глупый): допустим, мы будем ориентироваться на самый плохой случай исполнения инструкции. Заранее исключив заведомо брутальные варианты (такие, как подождать данных из файла подкачки – на это уйдут десятки миллионов тактов) и варианты очень плохого расположения данных (в оперативной памяти, на доставку уйдут сотни тактов), будем ориентироваться на то, что данные располагаются в кэше второго уровня.

Откровенная неразумность такого допущения, в общем-то, очевидна: зачем тогда в процессоре нужен кэш данных первого уровня с его низкой латентностью, если мы этой самой низкой латентностью вообще не пользуемся? Эта стратегия по сути своей просто проигрышна. Тем не менее, попробуем оценить, во что нам обойдутся такие "настроения" планировщика.
Пусть данные находятся в кэше второго уровня. Пусть расстояние между планировщиком и исполнительным устройством равно, для определенности, 6 стадиям (которые микрооперация проходит, соответственно, за шесть тактов).
В момент "0" мы получили микрооперацию. Данные из кэша второго уровня мы получим в момент времени "0 + латентность доступа в кэш второго уровня". Для ядра Northwood латентность доступа в кэш второго уровня в общем случае равна 9 тактам (если быть более точным, она равна 7 тактам, но загрузка вначале обязательно проверит кэш L1, на эту проверку необходимо 2 такта). Поэтому планировщик отправит следующую микрооперацию таким образом, чтобы на исполнение она попала спустя девять тактов.

В сущности, такой вариант никуда не годится: на весь процесс у нас ушло 9 тактов. На исполнение одной микрооперации. Эту стратегию работы планировщика мы вынуждены отклонить, поскольку высокой производительностью здесь даже не пахнет.

Вариант второй ("по договоренности"): не обращая внимания ни на что, задержать все микрооперации, зависимые от исходной команды загрузки, терпеливо дождаться прихода данных и только после этого продолжить их отправку. Такой вариант хорош тем, что не требует никаких дополнительных усилий: сиди себе и жди. Плох же этот вариант тем, что хорошую производительность демонстрирует далеко не всегда.
В случае попадания операции из второй группы планировщик, теоретически, мог бы ориентироваться на информацию о ходе ее выполнения, которую ему предоставляли бы исполнительные устройства. В таком случае нам необходимо, чтобы между планировщиками и исполнительными устройствами существовала обратная связь, по которой исполнительное устройство отчитывалось бы планировщику о том, к какому моменту ожидается окончание выполнения текущей инструкции. В принципе, такой вариант вполне возможен (забегая вперед, отметим, что для FPU load этот способ даже используется), но есть один скользкий момент.
Предположим, что нам повезло наилучшим из всех возможных способов, и данные находятся в кэше первого уровня. Время их доставки для ядра Northwood составляет два такта.
Пусть в момент времени "0" исполнительное устройство получило микрооперацию. В момент времени "0+2" такта, отослав свое состояние планировщику, оно получило данные из кэша первого уровня. Оно мгновенно сообщает об этом планировщику, и планировщик сразу же реагирует, выпуская на конвейер следующую микрооперацию. Она бредет по направлению к исполнительному устройству и подойдет к нему через 6 тактов.

Вроде бы все хорошо и правильно. Что же, в конце концов, мы получили? Подсчитаем результат: наша вторая микрооперация доберется на исполнительное устройство в момент времени "0+2+6" тактов, ведь ей по-прежнему надо пройти все промежуточные стадии между планировщиком и исполнительным устройством, расстояние между ними короче не стало. Итого: 8 тактов. Получается, что зависимая инструкция стартует не в момент времени "0+2", когда данные уже готовы, а в момент времени "0+2+6" тактов. Мы потеряли 6 тактов!
Скажем прямо: не очень хороший вариант.

Более того, несложно показать, что эффективность такого варианта с ростом длины конвейера в общем случае снижается. Действительно, в использованном выше варианте для конвейера с расстоянием в 6 стадий между планировщиком и исполнительными устройствами мы получили 8 тактов вместо 2. Итоговая эффективность – 25%.
Для конвейера длиной в одну стадию тот же вариант дает эффективность 67%.
Для конвейера длиной в 666 стадий мы получим 668 тактов вместо двух. Эффективность – 0.3%.
В то же самое время для инструкций с большим временем исполнения в этом методе есть своя прелесть. Предположим, что на нашем конвейере с длиной 6 стадий некая инструкция исполняется 50 – 100 тактов (в зависимости от обстоятельств). Однако точное время исполнения становится известно не сразу, а примерно к 25-ому такту.
В момент времени "0" исполнительное устройство получило микрооперацию.
В момент времени "25" исполнительное устройство определяет, что операция будет исполняться 51 такт.
В этот же самый момент времени "25" об этом узнает планировщик. Он немного ожидает, и …
И в момент времени "45" отправляет зависимую микрооперацию, которая подойдет к исполнительному устройству как раз в …
В момент времени "51", где совершенно удачно (и "совершенно случайно") :D застанет только что полученный результат предыдущей инструкции.
То есть, существуют такие соотношения длины конвейера, времени исполнения микрооперации и времени, когда латентность исполнения микрооперации становится точно известной, при которых подобная стратегия оказывается вполне оправданной.
Этот метод имеет 100% эффективность тогда, когда [расстояние между планировщиком и функциональными устройствами] меньше, чем разница между [латентностью микрооперации] и [временем, когда латентность становится точно известной].
В нашем случае целочисленные операции этому условию не удовлетворяют, поэтому такая стратегия работы планировщика нам не подходит.

Вариант третий (оптимистический): два предыдущих наших варианта оказались в плане производительности малоинтересными. Первый вариант – чудовищно глуп, второй в нашем случае недостаточно эффективен. Остался только один вариант: отправлять инструкции "наперед", заранее, еще не зная ситуации с их выполнением.
Сделаем более развернутое описание этого способа.

Можно запускать команды одна за другой, но таким образом, чтобы рассчитывать на случай наиболее оптимистичных результатов загрузки. В нашем случае это будет означать, что надо спустя два такта после операции загрузки из памяти отправить следующую микрооперацию. Что нам дает такая стратегия?
В момент времени "0" мы отправляем на исполнительное устройство микрооперацию загрузки. Она должна достигнуть исполнительного устройства в момент времени "0+6", и планировщику это известно.
Не дожидаясь этого момента, в момент времени "0+2" (то есть на "расстоянии двух тактов" по конвейеру от предыдущей команды) планировщик отправляет следующую микрооперацию. Что у нас происходит дальше? В момент времени "0+6" наша команда загрузки достигла исполнительного устройства. Наша следующая команда, зависимая от нее, отстает на 2 такта. В момент времени "0+6+2" команда загрузки получает данные из кэша и отправляется дальше по конвейеру, а на исполнительное устройство весьма своевременно, к моменту получения результата, приходит наша вторая микрооперация! Итого, наше исполнительное устройство работает два такта подряд, не делая паузы.
Таким вот образом, заранее планируя времена прихода данных, можно отправлять микрооперации "с упреждением". Благодаря этому мы максимально эффективно нагружаем работой исполнительное устройство.
Таким образом, в условиях большого расстояния между планировщиком и исполнительными устройствами только оптимистическая стратегия способна эффективно загрузить работой длинный конвейер. Существенным является то, что в этом варианте планировщик:

1. Всегда предполагает наиболее выгодный для нас расклад с доступностью данных;
2. Не нуждается в информации о состоянии исполнительных устройств.

Казалось бы, вроде все вполне неплохо складывается. В конце концов, мы нашли такую стратегию работы нашего длинного конвейера NetBurst, при которой удается поддержать высокий темп исполнения микроопераций. И это действительно так. Но при одном очень важном условии: инструкция действительно выполняется наилучшим образом. В нашем примере с загрузкой из памяти это означает, что данные находятся в кэше первого уровня. Такой оптимистический расчет разумен в силу двух причин: во-первых, вероятность нахождения данных там выше. Во-вторых, время доставки из этой иерархии кэша минимально, и в случае удачной загрузки наше ожидание будет минимальным.

Но в случае, если это условие не выполняется, наш великолепно отточенный механизм превращается в грандиозную ловушку. Продемонстрируем это.

В самом деле, представим себе такую ситуацию: пусть, например, планировщик отправил одну за другой четыре зависимых друг от друга микрооперации. Они направлены в сторону исполнительного устройства, причем направлены еще до того, как будут загружены данные. Все происходит в строгом соответствии с оптимистической стратегией работы планировщика.
Вот они подходят к исполнительному устройству. И тут, о ужас, оказывается, что нужных данных в кэше первого уровня просто нет: вместо долгожданного операнда мы получаем роковой сигнал L1 cache miss.
Что делать в этой ситуации? Разумеется, в этом месте нам необходимо было бы притормозить конвейер и пойти искать "невесть где загулявшие" данные. Но конвейер не может остановиться "по щучьему велению", он продолжает неумолимо перемалывать каждый такт все новые и новые микрооперации. Планировщик уже выпустил цепочку микроопераций, и мы неустанно помним, что каждая из них зависит от предыдущей. Что же произойдет?

Вот попала на исполнительное устройство первая микрооперация. И, поскольку необходимых данных для нее нет, она была исполнена неверно. На следующем такте вторая микрооперация получила от первой неверные данные и, в свою очередь, была исполнена неверно. И так произойдет со всеми микрооперациями, вплоть до последней. Причем у нас возникает еще одна серьезная проблема.
Допустим, мы найдем данные в кэше второго уровня, в ядре Northwood на это понадобится 7 тактов. Но конвейер работает только в одном направлении и не в состоянии повернуть "вспять". Наша цепочка микроопераций уже прошла исполнительные устройства, причем с неправильным значением операнда. И если мы ничего в срочном порядке не предпримем, она, породив неправильные результаты, пройдет дальше по конвейеру и уйдет на отставку.

Другими словами, мало того, что мы выполнили команды неправильно! Мы еще и потеряли эти четыре микрооперации, и даже после нахождения нужных данных мы уже не сможем вернуться к исполнению этого куска кода. В самом деле, эти микрооперации благополучно были выбраны из Trace cache, поставлены в очередь, дождались обработки планировщиком и были направлены на исполнительное устройство. И не их вина, что в кэше данных не оказалось необходимых сведений. Фактически, этот участок кода для процессора оказался "потерян".
Разумеется, это абсолютно неприемлемый для нас вариант развития событий.

Поэтому в процессоре Pentium 4 существует специальная подсистема, задача которой состоит в том, чтобы при вышеуказанных ситуациях микрооперации не терялись и не уходили на "отставку", а заново были направлены на исполнение.
Другими словами, нам необходим некий механизм отката. В самом простом и незамысловатом варианте надо иметь хоть какую-то возможность в момент прихода сигнала L1 cache miss завернуть микрооперации на повторное исполнение. То есть, необходимо предусмотреть как бы "боковой выход" с конвейера, чтобы не потерять микрооперации.
И, по мере поступления "потерявшихся" данных, необходимо организовать повторное прохождение этих микроопераций через исполнительные устройства. И уже только после повторного прохода и получения правильных результатов можно разрешить отставку микроопераций.
Таким образом, система реплея призвана работать "хранителем первоначальной задачи": что бы ни произошло внутри процессора, у нас всегда должна быть возможность исполнить код программы правильно.

Немаловажным вопросом является вопрос о том, сколько микроопераций может быть направлено на реплей, и каков для них "период обращения". В самом деле, поскольку реплей в значительной степени "аварийный выход", его "емкость" не может быть велика. Через некоторое время надо направить команду на повторное исполнение. Можно, конечно, направлять микрооперации на повторное исполнение по получении необходимых данных, но тогда команды необходимо где-то хранить, а затем откуда-то выбирать. К тому же, все эти проверки доступности данных, поворот на реплей, повторная отправка должны происходить на сверхвысоких скоростях (напоминаем, что планировщики и исполнительные устройства – часть системы Rapid Execution Engine, работающей на удвоенной частоте).
В итоге, нам надо достичь компромисса: с одной стороны, потерять как можно меньше времени в ожидании данных; с другой, обрабатывать ситуацию нам надо на удвоенной частоте, поэтому сложные схемотехнические решения неприемлемы.

Чтобы минимизировать время простоя, продолжительность "паузы" необходимо сделать равной времени доставки из кэша второго уровня. Ведь это следующая [и наиболее быстрая после L1 D] иерархия памяти. Да и объем L2 обычно намного больше, чем объем кэша данных первого уровня, что, наиболее вероятно, приведет к нахождению необходимых нам данных.
На самом деле вполне очевидно, почему необходимо сделать время "паузы" равным латентности кэша второго уровня. Если сделать время ожидания меньше, то данные все равно не успеют вовремя дойти до исполнительных устройств, и мы нашу проблему не решим. Если сделать больше, то данные прибудут на исполнительное устройство раньше, чем микрооперация, что приведет к тому же итогу: перезапуск микрооперации.
Таким образом, для ядра Northwood минимальное время для паузы и доставки данных составляет 7 тактов, все равно раньше данные не успеют. Соответственно, период обращения микрооперации в системе реплея надо рассчитывать таким образом, чтобы микрооперация прошла перед планировщиком (притормозив его работу в этот момент), а спустя 7 тактов вновь попала на исполнительное устройство. И при этом, напомним, нам необходимо относительно простое устройство системы реплея, чтобы система могла работать на высоких частотах.

Реализованное в процессоре Pentium 4 устройство системы реплея является таким вот компромиссом – попыткой заново выполнить микрооперации, не слишком усложняя логику их обработки.
Итак, что собой представляет реплей? По сути, система реплея в своей основе представляет кусок фиктивного конвейера, расположенный параллельно основному.
Когда микрооперация выходит из планировщика, она попадает на специальное устройство, мультиплексор (Replay mux, подробнее см. статью "Replay: неизвестные особенности функционирования ядра Netburst"). Далее микрооперация клонируется, то есть возникает ее точная копия. Микрооперация-образец идет дальше по основному конвейеру в направлении исполнительных устройств, а микрооперация-клон имеет более интересную судьбу. Рядом с основным конвейером, параллельно ему, существует еще один конвейер, конвейер реплея. Длина этого фиктивного конвейера равна длине основного конвейера от планировщика до исполнительных устройств (ниже будет понятно, почему).
Микрооперация-образец уходит из мультиплексора на исполнение, в этот самый момент на параллельном, фиктивном конвейере начинает двигаться точная копия отправленной микрооперации. Микрооперация-клон просто перемещается вдоль стадий фиктивного конвейера, без какой-либо обработки. И исходная микрооперация, и ее клон движутся параллельно в течение всех стадий, когда микрооперация путешествует к исполнительному устройству.

Рискуя запутать читателя, все же отметим, что в реальности микрооперация никуда не движется. Дело в том, что описанная нами система реплея в своем "кремниевом" воплощении вовсе не обязана дословно следовать нашему описанию, лишь бы точно соответствовала друг другу реакция описываемой и реально воплощенной "в кремнии" системы. Тем не менее, для понимания работы системы реплея представление о ней как о фиктивном конвейере достаточно удобно. Самое интересное, что для нашего повествования точное устройство системы реплея даже не слишком важно, важно лишь, чтобы мы правильно описывали поведение этой системы.
При исполнении микрооперации специальное устройство (Checker) в Pentium 4 проводит проверку, "легитимны" ли полученные микрооперацией данные. Если результат положителен, то микрооперация на основном конвейере идет дальше, на отставку, а ее копия на фиктивном конвейере спокойно уничтожается. Все произошло правильно, и повода для вмешательства системы реплея нет.
Если же проверка показала, что произошел кэш-промах (либо еще некоторые события, такие, как загрузка данных из памяти; более подробное и строгое описание изложено в статье "Replay: неизвестные особенности функционирования ядра Netburst"), тогда задействуется механизм реплея. В этом месте на секунду остановимся и поясним, что в общем случае есть две причины общей неудачи исполнения: либо неудача исполнения текущей микрооперации, либо неудача исполнения тех микроопераций, от которых зависит текущая.

Микрооперация-оригинал с неверными операндами уничтожается, а копия нашей микрооперации направляется на реплей, описывая "петлю" по конвейеру. Она попадает непосредственно после выхода планировщика в мультиплексор (основная функция которого – автоматически притормаживать выпуск на конвейер следующей микрооперации из планировщика при попадании микрооперации из реплея) и начинает двигаться в сторону исполнительных устройств.
Выше уже указывалось, что своевременный приход микрооперации на исполнительное устройство обеспечивается следующим равенством: длина конвейера реплея (в стадиях) выбрана равной времени доступа (в тактах) в кэш второго уровня. То есть 7 тактам для ядра Northwood и 18 тактам для ядра Prescott.
По сути, это означает, что микрооперация описывает "петлю", один "оборот" по которой протекает за 7 (18 для ядра Prescott) тактов.
К моменту повторного попадания перезапускаемой микрооперации в мультиплексор операции планировщика с другими микрооперациями будут приторможены. Произойдет это по той причине, что микрооперация с реплея имеет приоритет перед всеми другими микрооперациями. Точнее сказать, планировщик под воздействием мультиплексора делает паузу в выпуске новых микроопераций на конвейер. Вполне понятно, что повышенный приоритет в исполнении микроопераций с реплея необходим ради того, чтобы они не накапливались в системе реплея.

Что произойдет, если данных не окажется в кэше второго уровня? Либо если из-за большого количества одновременных запросов кэш второго уровня просто не успеет выдать данные за 7 тактов?
Тогда микрооперация будет вынуждена сделать второй оборот. Снова попав в реплей, она будет во второй раз направлена на исполнение. Если данные в заданный промежуток времени опять не будут доставлены, она будет вновь направлена в реплей и совершит третий оборот петли. В случае ожидания данных из памяти, время доступа к которой составляет сотни процессорных тактов, команда может совершать десятки и сотни (!) таких оборотов, совершенно бессмысленно занимая ресурсы процессора.
Теперь мы можем твердо утверждать, что именно реплей отвечает за ту странную картину с латентностью кэша L2, которая и послужила поводом для углубленного изучения процессора Pentium 4.
В следующей главе мы опишем некоторые ключевые особенности реплея и следствия из него. По возможности, в основном тексте статьи мы будет придерживаться упрощенного изложения, более строгое мы вынесли в статье "Replay: неизвестные особенности функционирования ядра Netburst", каковое, по сути, превратилось в отдельную большую статью.

Весьма поучительно, что изначально вроде бы совершенно логичный способ организации работы длинного конвейера приводит к катастрофическим для производительности последствиям. Подробные причины этого мы откладываем до следующей главы, пока же важно осознать вот что: реплей является той ценой, которую приходится заплатить за длинный, сверхглубокий конвейер. Идеологическое решение о важности прежде всего высокой частоты работы заставило архитекторов сделать длинный конвейер. А длинный конвейер, в свою очередь, потребовал специальной системы "отката" в ситуациях, когда данные не доставлены к микрооперациям вовремя.
Особо отметим: штрафы, вызванные реплеем, не зависят от качества кода и количества "ветвлений" в нем. Реплей есть оборотная сторона медали по имени "Hyper Pipeline" – то, чем приходится расплачиваться за оптимистическую стратегию работы планировщика. А, в свою очередь, такая стратегия – единственно возможный вариант работы конвейера в ситуации, когда планировщик удален от исполнительных устройств, а расстояние (в стадиях) между ними превышает время исполнения большинства простых команд. Поскольку данные к исполняемым микрооперациям не могут быть доставлены мгновенно, происходит "холостой цикл" работы конвейера. Причем плохо не столько то, что отдельная операция переисполняется – тут деваться некуда, мы вынуждены это сделать – сколько то, что вслед за первой операцией переисполняется вся цепочка зависимых операций, сколько бы их ни было. То есть, первоначальную проблему с перезапуском микрооперации "наследует" остальная цепочка.
Если давать оценку реплею в целом, то "реплей – это плохо, но неизбежно".
Перейдем к более подробному рассмотрению обнаруженного феномена.
Глава десятая, которая говорит о том, что не существует такой идеи архитекторов, которую не сумели бы воплотить в жизнь талантливые технологи

В этом месте сделаем оговорку: авторский коллектив надеется, что у читателя еще не пошла голова кругом от всех этих описанных выше подробностей. Потому что мы планируем углубиться в реплей еще сильнее. Благо, найденные нами подробности достаточно интересны и важны для понимания принципов работы процессора Pentium 4.
Для лучшего закрепления повторим кое-какие моменты из прошлой главы. Мы уже говорили, что микрооперация при попадании на реплей исполняется фактически дважды, первый раз (неверно) и второй (уже с верными операндами). Таким образом, при первом проходе исполнительные устройства отработали вхолостую, не производя никакой полезной работы. Более того, попавшая на реплей микрооперация может потянуть за собой еще несколько! В частности, нам удавалось строить вполне безобидные на первый взгляд цепочки из тысяч команд, которые совершали сотни (!) оборотов в петле реплея после одного-единственного кэш промаха! Справедливости ради, отметим, что сотни оборотов реплея все же не слишком частый случай, иногда все ограничивается гораздо меньшим количеством: единицы, реже десятки оборотов. Естественно, такое зацикливание снижает эффективность работы исполнительных устройств, так как добрая половина команд исполняется вхолостую.

Рассмотрим ситуацию с многократным оборотом цепочки микроопераций в петле реплея чуть подробнее. Почему возникает такая проблема?

Причиной этому, как ни странно это звучит, служит энтузиазм планировщика. Точнее сказать, совокупность двух причин: желание планировщика добиться как можно более эффективной загрузки исполнительных устройств и его незнание о текущем состоянии микрооперации на исполнительном устройстве.
Вернемся к нашему примеру с цепочкой зависимых команд. Пусть первая завернула на реплей. Тогда поворачивает на реплей и вторая, и третья, и так далее. Здесь важно отметить следующее: поскольку микрооперация-образец по основному конвейеру и микрооперация-клон по фиктивному конвейеру движутся параллельно, расстояние между микрооперациями на конвейере реплея будет сохраняться. Собственно, свойство сохранять расстояние между микрооперациями есть одно из изначально планируемых свойств реплея: если бы расстояния не сохранялись, это сильно усложнило бы работу логики процессора.
Таким образом, если между зависимыми командами был промежуток, в течении которого планировщик не выпустил ни одной микрооперации, то между клонами на конвейере реплея будет такая же пауза. По аналогии с электронно-дырочной проводимостью в полупроводниках назовем ее "дыркой".
Оказывается, у этой дырки достаточно интересные свойства. В частности, именно существование дырок вместе с энтузиазмом планировщика приводят к существованию такого явления, как оборот реплея для всей цепочки микроопераций.

Пусть у нас есть дырка между микрооперациями на реплее. Когда они, пройдя реплей, возвращаются на мультиплексор (совершают первый оборот реплея) в том месте, где у нас была дырка, возникает возможность отправить еще какую-нибудь микрооперацию на исполнение. Чем, разумеется, тут же пользуется планировщик: ведь ему необходимо как можно эффективнее загрузить исполнительные устройства! И дело здесь уже даже не столько в "энтузиазме" планировщика, сколько в том, что планировщик не знает, в каком состоянии находятся ранее выпущенные микрооперации, и потому вынужденно следует оптимистической стратегии.
Не тут-то было. Как назло, следующей командой у нас "под рукой" оказывается очередная зависимая команда из нашей цепочки. Но она зависит от результатов тех микроопераций, которые были в первоначальном коде программы перед ней! А сейчас, в результате того, что начало цепочки сделало оборот по реплею, она опередила своих предшественниц на пути к исполнительным устройствам! Разумеется, она попадет на исполнительное устройство раньше, чем будут готовы ее операнды, исполнится неверно, и совершенно резонно будет направлена на реплей!

Соответственно, на следующем обороте реплея, после того как начало цепочки благополучно исполнится с правильными операндами, мы будем иметь зеркальное отражение ситуации: множество пустых позиций в петле реплея и одну команду, отправленную раньше времени. И тут же жаждущий работы планировщик заполнит все пустые позиции петли реплея, включая предшествующие нашей микрооперации, новыми микрооперациями! Которые, по логике программы, должны были идти после нее, но попали перед ней!
Думаем, большинство читателей догадалось, как все продолжится… Правильно, вся цепочка зависимых микроопераций пойдет на реплей. Наша команда на этот раз исполнится, а все остальные на следующем круге образуют знакомую картину: петля реплея, заполненная микрооперациями, и дырка между ними. Круг замкнулся. Получается следующая картина: планировщик, не подозревая о том, какие команды выполнились правильно, а какие нет, продолжает отправлять на исполнение новые команды из зависимой цепочки, вставляя их в "дырки" между командами, возвращаемыми на реплей.

Что у нас в итоге получилось? Получилось, что наша цепочка зависимых микроопераций вся (!) проходит через реплей. Чтобы представить это наглядно, вообразим цепочку, состоящую из мелких звеньев. Она перехлестнута через карандаш (допустим, на один оборот). Если потянуть за конец цепочки, то вся цепочка совершит один оборот вокруг карандаша.
Точно так же вся наша цепочка зависимых команд совершит минимум один оборот в петле реплея. А, возможно, и больше, в зависимости от доступности данных.
И оборвать это событие может только окончание цепочки зависимых команд. Таким образом, у нас получилось, что "дырка" способна привести к "бесконечному" реплею, до тех пор, пока не закончится вся цепочка зависимых команд. Либо до тех пор, пока не включится некая "волшебная" сторонняя система.

В нашей аналогии это означает вот что: петля вокруг карандаша закончится тогда, когда закончится цепочка. Либо, буде планировщику в момент прохождения "дырки" случайно попадется независимая команда, цепочка попросту "рвется", и реплей постепенно прекращается.
Но если вокруг карандаша "намотается" не один оборот цепочки, а несколько, вероятность срабатывания механизмов выхода из реплея заметно уменьшается. Существуют такие цепочки команд, которые вообще не выходят из петли реплея.

Вот эта ситуация и есть самое плохое следствие реплея: одно дело, когда какая-то команда исполнилась пару раз вместо одного. Обидно, но терпимо, абсолютная величина потерь невелика. Совсем другое дело, когда значимый по размеру участок кода исполняется минимум дважды. Фактически, в этом месте эффективность работы процессора уменьшается во столько раз (!), сколько оборотов сделают микрооперации в петле реплея. Но в любом случае на этом участке мы потеряем в эффективности не менее чем в два раза!

Должно ли нас сильно волновать, что эффективность работы процессора в данном случае мала? Будет ли это сказываться на производительности (скорости выполнения попавшего на реплей кода)? Посмотрим еще раз на наш пример с "дыркой". Заметим, что на каждой паре оборотов цепочки (14 тактов в случае Northwood), планировщику приходилось ожидать по 7 тактов, прежде чем он мог отправить на исполнение новую команду. То есть в нашем случае, вместе с эффективностью упала в 2 раза и производительность!
Это хорошо объясняет, почему же, несмотря на весьма заметное теоретическое преимущество в виде: более высокой частоты, более быстрой шины, большего и более быстрого кэша, большего значения IPC (instruction per cycle), процессор Pentium 4 в некоторых ситуациях уступает в результирующей производительности своему предшественнику (!), процессору Pentium III. Отметим, что реплей – зачастую гораздо более строгое объяснение, нежели полу-мифические "остановки и сброс конвейера на коде с большим количеством переходов"; объяснением, которое, будучи весьма привычным, часто является неверным.

Что самое интересное, будь у планировщика возможность задержать исполнение всего на несколько тактов (ровно на столько, сколько нужно, чтобы цепочка успела "выползти" из петли реплея), всех этих проблем не было бы. Но совершенно благое желание максимально эффективно использовать ресурсы и поддерживать максимальный темп исполнения плюс незнание о ситуации дальше по конвейеру приводят к совершенно противоположному результату. Исполнительные ресурсы расходуются в высшей степени неэкономно. Как мы уже писали выше, попавшие на реплей операции исполняются минимум дважды. Максимум десятки (а в исключительных случаях и сотни) раз. Это неизбежно приведет к падению на этом участке кода производительности нашего процессора (хоть и не столь радикально, сколь эффективности).
Это означает, что "благодаря" реплею на этом участке кода производительность нашего процессора упала минимум вдвое, а максимум – в десятки и сотни раз (!).
Воистину, справедлива народная мудрость: торопливость ни к чему хорошему не приводит. Забыв на секунду о политкорректности, приведем следующую аналогию: в вышеописанной ситуации Pentium 4 напоминает заику, который лихорадочно пытается рассказывать слова в такт музыке. Так сказать, спеть. Но, поскольку речевой аппарат просто не успевает, вместо гармонии текста и слов получается какофония. А "заикание" приводит к тому, что многие куски текста повторяются два и более раз. Хотя упорства не занимать, и текст он все-таки договаривает.
Жалкое зрелище.

Впрочем, отставим лирику. Впереди много не менее интересного.
В процессе изучения движения микроопераций по реплею мы обнаружили, что иногда команды бродят в реплее дольше, чем должны были бы. Например, такая ситуация возникала при ошибках алиасинга или в случае промаха D-TLB. Складывалось впечатление, что петля реплея у планировщика не одна (!).
Так оно и оказалось. В ядре Northwood некоторые виды ошибок (таких, как алиасинг, подробнее в статье "Replay: неизвестные особенности функционирования ядра Netburst") приводят к попаданию на другую петлю реплея. Которая, в отличие от первой, занимает не 7, а 12 тактов!

Происходит это потому, что в некоторых ситуациях требуется проводить особую, нестандартную проверку на наличие ошибок. Результат такой проверки становится известным несколько позже, чем в обычных случаях. Однако у нас имеется лишь ограниченное время для того, чтобы в случае возникшей неприятности успеть завернуть операцию на реплей. Если результат проверки не подоспеет вовремя, то микрооперация, исполнившись неверно, успеет пройти по конвейеру дальше и уйдет на отставку, что означает катастрофу: код нашей программы исполнился неверно.
Разумеется, "такой хоккей нам не нужен!" (с). Посему, в лучших традициях шпионских романов, на конвейере предусмотрен второй "пост проверки", на котором в случае "несовпадения документов" (сиречь неверного результата) микрооперацию развернут на месте и отправят "куда положено", то есть на реплей. Таким способом мы страхуем себя от неверно исполненного кода.

Поскольку эта проверка расположена дальше от планировщика, понятно, что новый круг, по которому придется бродить "отвергнутой" микрооперации, будет большего размера. В числах это как раз 12 тактов вместо 7.
Указанные петли реплея мы назвали по их длительности в тактах, RL-7 (Replay Loop – 7) и RL-12 (Replay Loop – 12) соответственно.
Итог: у ядра Northwood есть две (!) петли реплея. У ядра Prescott, кстати, только одна петля (см. статью "Replay: неизвестные особенности функционирования ядра Netburst"), RL-18. Связано это с тем, что, благодаря увеличившемуся времени доступа к кэшу L2, у нас появилось больше времени для осуществления проверки, поэтому процессор успевает провести ее в "один заход".
Обращаем внимание читателей: до сего момента мы рассматривали только один планировщик. Из главы 6 мы помним, что в ядре Pentium 4 таких планировщиков ПЯТЬ. Они являются независимыми, каждый из них имеет свою очередь. И это означает….
И это означает, что у каждого планировщика есть своя система реплея. То есть, в ядре Northwood всего есть десять (!) фиктивных конвейеров, спрятанных от пользователя.

Масштабы неизвестной нам части процессора Pentium 4 откровенно ошеломляют. Поневоле начинаешь задаваться вопросом: неужели ЭТО можно назвать красивым архитектурным решением?
В ядре Prescott ситуация несколько изменилась: петель реплея у каждого планировщика только одна, но длина каждой больше, 18 тактов. Итого: пять фиктивных конвейеров. Вспоминая, что по меньшей мере часть их работает на удвоенной частоте (вместе с Fast ALU конвейерами), а используется при этом дифференциальная (LVS) логика, нас перестает удивлять нагрев ядра Prescott: он вполне понятен. Реплей заставляет конвейер работать "в холостом цикле" минимум дважды на каждую микрооперацию, попавшую на него. В результате, поскольку нужно совершать больше работы на одно и то же количество кода, выделяется больше тепла.

Также можно выдвинуть осторожное предположение, на что именно ушла заметная часть "лишних" транзисторов в этом ядре. Напомним, что с ядром Prescott не все ясно по части количества транзисторов: заявленное корпорацией Intel количество транзисторов в этом ядре слишком велико для банального увеличения кэша второго уровня вдвое; приходится допускать, что в Prescott-е есть "что-то еще". Этим "что-то" вполне может быть совокупность пяти систем реплея и ALU с поддержкой ЕМ64Т.
Но вернемся к реплею. Оказывается, наличие реплея в теории способно привести к полной блокировке (!) процессора. В частности, в нашем примере видно, что при наличии дырки команды в петле реплея меняют места относительно первоначального порядка в коде программы. Может оказаться так, что цепочке команд, совершающих оборот за оборотом круги в петле реплея, для выхода необходима команда, находящаяся в данный момент в планировщике. Если петля реплея полностью заполнена, то места для того, чтобы она вышла, просто нет: у микроопераций из реплея приоритет по исполнению. Такая блокировка называется "deadlock", и штатными средствами она разрешена быть не может.

Тем не менее, практика показывает, что в реальной жизни процессор "в ступор" все же не впадает. Это означает, что есть некая аварийная система, которая в такой ситуации каким-либо образом разрешает ситуацию. Забегая вперед, скажем, что, по всей видимости, эта же система часто обрывает "бесконечный" реплей спустя несколько десятков оборотов. Подробности функционирования этой системы – в статье "Replay: неизвестные особенности функционирования ядра Netburst".
Таким образом, становится понятно, что в процессоре Pentium 4, кроме реплея, есть еще как минимум одна неизвестная (!) нам ранее аварийная система. Необходима она для разрешения такого события, как взаимоблокировка (deadlock).

На самом деле систем даже две: одна из них обнаруживает проблему, а вторая предпринимает усилия к ее разрешению. В этом месте остается только аплодировать мужеству и находчивости технологов, которые воплотили все это в жизнь.
Впрочем, изучение этих систем все еще продолжается, мы же пока перейдем к такому небезынтересному вопросу, как реплей и FPU.
До этого момента мы, говоря о реплее, нигде не упоминали о работе с плавающей точкой. И этому есть разумное объяснение. Дело в том, что взаимодействие системы реплея и FPU команд строится по другой схеме.
Латентность загрузки FPU, MMX и SSE/2 регистров из кэша L1 заметно выше, чем у целочисленных (9/12 тактов против 2/4 в ядрах Northwood/Prescott соответственно). Этих дополнительных 7/8 тактов оказывается как раз достаточно, чтобы организовать "обратную связь" между планировщиком и исполнительными устройствами. На фоне ожидания выполнения команды fp_load у нас оказывается достаточно времени для того, чтобы, узнав о промахе в L1, поставить планировщик в известность. Тот учтет сей грустный факт и не выпустит зависимые FPU/MMX/SSE/2 команды на исполнение. Иными словами, эти микрооперации до отправки на исполнение успевают пройти проверку на доступность операндов. Это исключает основную причину возникновения реплея. И очень "кстати", так как процессоры архитектуры NetBurst не имеют "лишних" FPU блоков: он всего один, и работает в темпе одна инструкция за
такт (сравним с ALU: четыре инструкции за такт). И если попавшие на реплей FPU операции займут его бесполезным выполнением, то производительность неизбежно упадет.
В результате команды FPU никогда не попадают на петлю реплея RL-7. Тем не менее, на петлю RL-12 они попадать будут. Например, в случае, если FPU микрооперация зависит от целочисленной команды, которая сама попала в RL-7.
В заключение отметим еще два небезынтересных факта, связанных с обработкой FPU операций:

1. В абсолютном большинстве вычислительных алгоритмов область локальности данных заметно больше размера кэша первого уровня;
2. Любые варианты команд предзагрузки данных (prefetch) не позволяют загрузить строку кэша непосредственно в L1. Таким образом, без проверки на доступность операндов, FPU команды были бы частыми "завсегдатаями" реплея, особенно в "излюбленных" потоковых алгоритмах.

Нам необходимо обсудить еще одну область применения реплея.
Выше по тексту мы познакомились с ситуацией, когда реплей используется для разрешения неприятностей, связанных с промахами кэша. На самом деле роль реплея в функционировании Pentium 4 этим вовсе не ограничивается. Реплей используется в процессоре Pentium 4 для решения самых разнообразных проблем. Не сильно погрешив против истины, можно даже сказать "везде, где только можно".
В частности, именно реплей используется при решении весьма частой (и весьма противной для программистов) проблемы: проблемы загрузки сразу после записи.

Суть ее вот в чем. После того, как мы получили некоторые данные и отдали команду Store сохранить их в памяти, должно пройти некоторое время перед тем, как мы начнем последующее считывание этих же данных.
Подробное объяснение этой проблемы мы вынесли в статью "Replay: неизвестные особенности функционирования ядра Netburst", здесь же отметим, что такая попытка (особенно, если прошло мало времени после сохранения) частенько заканчивается реплеем.
Хуже всего то, что программист практически ничего не может сделать для противодействия реплею: процессор агрессивно переупорядочивает инструкции внутри, и потому операция загрузки, даже далеко отстоящая в коде программы, может изменить свое положение относительно других инструкций (как мы знаем, новые независимые цепочки часто начинаются именно с операции загрузки).
В результате операции загрузки исполняются слишком рано и уходят на реплей. А вслед за ними идет на реплей вся (!) зависимая цепочка.

Что это означает? Подобные операции всегда встречаются в вызовах функций: вызывающая процедура сохраняет параметры в стеке, а вызванная оттуда их читает. Вызовы функций есть во всех без исключения программах. Соответственно, делаем вывод: во всех без исключения программах есть крайне благоприятные условия для возникновения реплея.
Ну и в завершение перейдем к взаимодействию реплея и технологии Hyper Threading.
Напомним, что технология Hyper Threading призвана увеличить эффективность использования незагруженных блоков процессора. Поскольку реплей занимает исполнительные блоки, нам было интересно проверить, есть ли взаимное влияние? И если да, то насколько оно сильно?
За подробностями традиционно отсылаем к статье "Replay: неизвестные особенности функционирования ядра Netburst", размер которой поневоле оказался весьма немалым в силу большого количества поднятых в ней вопросов. Здесь же изложим только результат исследований.

Из общих соображений понятно, что технология Hyper Threading тем менее эффективна, чем сильнее загружены исполнительные блоки процессора. В то же время наличие реплея приводит к тому, что часть микроопераций многократно переисполняется, занимая свободные ресурсы. Отсюда следует, что две подсистемы, претендующие на одни и те же ресурсы, будут конфликтовать между собой.
Результат исследований не обманул наших ожиданий.
Наличие реплея существенным образом может снижать эффективность технологии Hyper Threading. В частности, при определенных условиях реплей способен "потерять" до 45% производительности (!) в ядре Northwood и до 20% в ядре Prescott. Более того, наблюдаемое повышение эффективности технологии Hyper Threading в ядре Prescott следует связывать скорее с усовершенствованием реплея, чем с улучшением собственно самой технологии Hyper Threading!
Глава одиннадцатая, о проблемах в жизни заслуженных охотников: "Сто способов убить медведя, или Пособие по охоте"

Ну что ж, пора сделать паузу. Тем паче, что интересного материала мы накопили предостаточно, пора его упорядочивать и делать из него некоторые выводы. Да и читателю необходимо дать передышку, количество свалившегося на него материала действительно велико. Статья получилась большой – и это притом, что в большинстве случаев мы старались уводить подробное описание в Приложения и смежные статьи.
Отметим, что работа над реплеем не была ни легкой, ни быстрой. Откровенно сказать, она была долгой, муторной и местами весьма нудной. Достаточно только сказать, что существование реплея как явления мы достаточно четко сформулировали для себя еще в феврале 2004 года и практически все остальное время работали над тем, чтобы изучить принципы работы этой подсистемы и выяснить, как наличие реплея меняет работу остальных подсистем процессора.
Кроме того, уже в процессе написания текста статьи мы столкнулись с очевидным противоречием: с одной стороны, наличие реплея и его свойства действительно интересны и ранее нигде публично не описывались. Соответственно, хочется дать как можно более подробный отчет о найденных нами нюансах работы Pentium 4.

С другой стороны, это в достаточной степени специализированная особенность процессора, и нам совершенно неизвестно, насколько это интересно читателям. Есть лишь некоторые сомнения по поводу того, насколько глубоко большинство читателей интересуется микроархитектурой процессоров.
В результате для того, чтобы массовый читатель заинтересовался этим феноменом, необходимо было тщательно выдержать баланс между сравнительно простым объяснением ситуации, оставаясь, в то же самое время, корректным в описании явления для тех читателей, которые хорошо разбираются в архитектуре Pentium 4.

Удалось ли нам справиться с этой задачей? Что ж, это может решить читатель, только читатель, и никто более. Если эту статью было интересно читать, если в результате ее прочтения что-то запомнилось и отложилось в памяти, если читатель хоть на секунду прочувствовал всю сложность проектирования микропроцессоров – тогда можно с чистой совестью умывать руки. Одной из главных (хоть и не высказанных вслух) целей мы достигли.
Если же читатель с трудом добрался до этой главы, если большую часть он пропустил в силу невнятности объяснений, если ему было скучно и грустно – что ж, мы приносим извинения за зря потраченное на статью время. В свое оправдание можем только сказать, что мы изо всех сил старались избегнуть такого впечатления от статьи. Посыпая головы пеплом, нам останется утешать себя лишь известной фразой "Дорогу осилит идущий" (с).
В завершение попробуем подвести итог реплею и нашему исследованию микроархитектуры Pentium 4.

Итак: реплей есть неотъемлемая часть идеологии NetBurst. Долгое время эта часть была неизвестной для широкой публики. Но это тот механизм, который обеспечивает работоспособность микроархитектуры Pentium 4, и, хотя бы поэтому, его описание необходимо.
Влияние реплея на производительность отрицательно. Тем не менее, это та цена, которую пришлось заплатить за сильно удлинившийся конвейер и заметно выросшую частоту работы. Весьма вероятно, что именно сложности с реплеем, его отрицательное влияние на производительность и вызванный им дополнительный нагрев послужили причинами отмены наследника Prescott, ядра Tejas. По крайней мере, эта гипотеза хорошо объясняет наблюдаемые факты (разумеется, точные мотивы решения об отмене знает только менеджмент Intel).

Авторский коллектив надеется, что наше описание реплея хоть в какой-то степени заполнило тот информационный вакуум, который сложился вокруг одной из самых интересных и загадочных подсистем процессора Pentium 4.
С нашей точки зрения, этот материал обязан был быть изложен, как минимум, в описании микроархитектуры и в руководствах по оптимизации: люди, оптимизирующие свои программы для достижения максимальной производительности, должны знать о "подводных камнях" этого процесса.
С другой стороны, в определенной степени мы понимаем корпорацию Intel – изложить этот материал, не производя отрицательного впечатления на потенциальных потребителей своей продукции, практически невозможно. А отрицательное впечатление – это не то впечатление, которое хотела бы производить на своих потенциальных покупателей любая коммерческая корпорация. К сожалению, выбранный корпорацией путь – фактическое замалчивание существования такого явления, как реплей – тоже разумным не назовешь. Грань между маркетингом и прямым обманом очень тонка, и, похоже, в ситуации с реплеем маркетинг эту грань перешел.

Да, к сожалению, реплей ухудшает производительность Pentium 4. Оправдывает существование этого явления только то, что без него процессор Pentium 4 корректно работать попросту не смог бы.
Как бы там ни было, на этом наш коллектив в исследовании процессора Pentium 4 вовсе не останавливается. Просто наступила пора сделать небольшую паузу, вдохнуть полной грудью и посмотреть: чего же мы достигли за год напряженной работы? В какую сторону двигаться при изучении дальнейших особенностей? Какие задачи необходимо поставить перед собой в дальнейшем изучении архитектуры Pentium 4?
Часть таких задач перед нами ставит жизнь: например, с выпуском Pentium 4 серии 6xx становится немаловажным, насколько эффективно реализована поддержка 64 бит в процессоре Pentium 4. И этой задачей наш авторский коллектив уже занимается. Надеемся, через некоторое время мы сумеем продемонстрировать читателям интересные подробности реализации этой технологии.

По-прежнему не все ясно и с реплеем: остались многие потенциально интересные вопросы.
В любом случае, тема архитектуры – это тема, которую можно исследовать и "вглубь" и "вширь" практически бесконечно. В какой-то степени даже немного грустно заканчивать эту статью. Но ее объем не бесконечен, да и прощаемся мы ненадолго.
Мы надеемся, что путешествие "в глубины" Pentium 4 было для Вас увлекательным. И, надеемся, ко всему оно было еще и полезным.

Искренне Ваш,
Коллектив авторов.
Список литературы

1. Hyper-Threading Technology Architecture and Microarchitecture

Deborah T. Marr, Desktop Products Group, Intel Corp.
Frank Binns, Desktop Products Group, Intel Corp.
David L. Hill, Desktop Products Group, Intel Corp.
Glenn Hinton, Desktop Products Group, Intel Corp.
David A. Koufaty, Desktop Products Group, Intel Corp.
J. Alan Miller, Desktop Products Group, Intel Corp.
Michael Upton, CPU Architecture, Desktop Products Group, Intel Corp.


2. Hyper- Threading Technology in the Netburst™ Microarchitecture

Debbie Marr, Hyper- Threading Technology Architect, Intel Corp.

3. Pipeline Depth Tradeoffs and the Intel® Pentium® 4 Processor

Doug Carmean, Principal Architect, Intel Architecture Group

4. Intel® Pentium® 4 Processor Specification Update

5. IA-32 Intel® Architecture Optimization

6. The Microarchitecture of the Pentium® 4 Processor

Glenn Hinton Desktop Platforms Group, Intel Corp.
Dave Sager, Desktop Platforms Group, Intel Corp.
Mike Upton, Desktop Platforms Group, Intel Corp.
Darrell Boggs, Desktop Platforms Group, Intel Corp.
Doug Carmean, Desktop Platforms Group ,Intel Corp.
Alan Kyker, Desktop Platforms Group, Intel Corp.
Patrice Roussel, Desktop Platforms Group, Intel Corp.


7. The Intel® Pentium® 4 Processor

Doug Carmean, Principal Architect Intel Architecture Group

8. Inside the Pentium® 4 Processor Microarchitecture

Doug Carmean, Principal Architect Intel Architecture Group

9. Intel® Pentium® 4 Processor on 90nm Process Datasheet

10. The Microarchitecture of the 90nm Intel® Pentium® 4 Processor

Darrell Boggs, Desktop Products Group, Intel Corp.
Aravindh Baktha, Desktop Products Group, Intel Corp.
Jason Hawkins, Desktop Products Group, Intel Corp.
Deborah T. Marr, Desktop Products Group, Intel Corp.
J. Alan Miller, Desktop Products Group, Intel Corp.
Patrice Roussel, Desktop Products Group, Intel Corp.
Ronak Singhal, Desktop Products Group, Intel Corp.
Bret Toll, Desktop Products Group, Intel Corp.
K.S. Venkatraman, Desktop Products Group, Intel Corp.


11. The Microarchitecture of the Intel® Pentium® 4 Processor on 90nm Technology

12. LVS Technology for the Intel® Pentium® 4 Processor on 90nm Technology

13. 64-Bit Extension Technology Software Developer's Guide, Vol. 1

14. 64-Bit Extension Technology Software Developer's Guide, Vol. 2

15. Intel® Xeon™ Processor MP with up to 2MB L3 Cache (on the 0.13 Micron Process) Datasheet

16. Intel® Pentium® 4 Processor with 512-KB L2 Cache on 0.13 Micron Process and Intel® Pentium® 4 Processor Extreme Edition Supporting Hyper-Threading Technology Datasheet

17. Low Voltage Swing Logic Circuits for a Pentium® 4 Processor Integer Core

Daniel J. Deleganes, Micah Barany, George Geannopoulos
Kurt Kreitzer, Anant P. Singh, Sapumal Wijeratne
Intel Corporation


18. Патент Intel № 6,163,838 "Computer processor with a replay system"

19. Патент Intel № 6,094,717 "Computer processor with a replay system having a plurality of checkers"

20. Патент Intel № 6,385,715 "Multi-threading for a processor utilizing a replay queue"