Replay: неизвестные особенности функционирования ядра Netburst

Введение


Со времён выпуска компанией Intel процессора Pentium 4 многие задавались вопросом о причинах странных результатов, показанных процессором при измерении производительности в ряде задач. Почему, несмотря на более высокую тактовую частоту и широко разрекламированные маркетинговым отделом Intel архитектурные особенности Net Burst (такие, как Trace Cache, Rapid Execution Engine, Quad-Pumped Bus, Hardware prefetch и даже Hyper-Treading), призванные увеличить число команд, исполняемых за такт, процессоры Pentium 4 умудряются часто проигрывать своим менее частотным собратьям и конкурентам в лице семейств Pentium M и AMD Athlon? Как правило, объяснение проблем производительности сводится многочисленными обозревателями к длинному конвейеру, иногда к маленькому объёму кэша памяти, иногда к более высокой латентности памяти, по сравнению с конкурентами, ещё реже к другим причинам.
Но, как оказывается, это всё равно не объясняет некоторые аномалии, которые можно выявить в процессе тестирования. В качестве примера рассмотрим случай тестирования латентности памяти цепочкой зависимых команд MOV EAX, [EAX] (так называемый pointer-chasing) "с усугублением", где цепочка зависимых инструкций загрузки растягивается последовательностью инструкций сложения X * { MOV EAX,[EAX] - n*{ADD EAX, 0} }. Зная время выполнения сложения, мы можем оценить время T, приходящееся на выполнение загрузки, как время выполнения одной итерации минус время выполнения цепочки из N сложений. Если бы всё было просто, то на графике зависимости Т от N была бы горизонтальная прямая, положение которой определялось бы через идеальное время доступа к L2, т.е. как 9 = 2+7. На самом же деле получается следующий график, объяснить который практически невозможно, используя оптимизационные руководства Intel.


Рис. 1. Тестирование латентности кэша второго уровня процессора
Pentium 4 (Northwood) цепочкой X*{ MOV EAX,[EAX] - N*{ ADD EAX,EAX }}

Но, к счастью, одна зацепка в оптимизационном руководстве всё же есть. Это очень скудное и поверхностное описание механизма, называемого replay.

"Replay
In order to maximize performance for the common case, the Intel NetBurst micro-architecture sometimes aggressively schedules µops for execution before all the conditions for correct execution are guaranteed to be satisfied. In the event that all of these conditions are not satisfied, µops must be reissued. This mechanism is called replay.
Some occurrences of replays are caused by cache misses, dependence violations (for example, store forwarding problems), and unforeseen resource constraints. In normal operation, some number of replays are common and unavoidable. An excessive number of replays indicate that there is a performance problem."


Из этого скудного описания ясно, что реплей может привести к большим проблемам в случаях кэш-промахов. Это описание натолкнуло нас на мысль, что наличие реплея может объяснить график измеряемой латентности L2. В официальных руководствах и статьях никаких подробностей найти не удалось, их удалось найти только в патентах.
Представленный ниже материал появился в результате изучения патентов Intel:

патент 6,163,838 "Computer processor with a replay system";
патент 6,094,717 "Computer processor with a replay system having a plurality of checkers";
патент 6,385,715 "Multi-threading for a processor utilizing a replay queue"

А так же анализа большого количества низкоуровневых тестов, проведённых нами. Мы исследовали в основном работу ядра Northwood. Углублённых исследований ядра Prescott мы провели очень мало ввиду отсутствия достаточного количества времени и ресурсов.

Зачем нужен реплей?


Отличительной особенностью конвейера NetBurst от конвейеров процессоров Intel P6 и AMD K7/K8 является то, что между планировщиком и исполнительным узлом конвейер поделён на несколько дополнительных стадий.


Рис. 2a. Блок-схема участка конвейера процессора с ядром P6,
Рис. 2б. Блок-схема участка конвейера процессора с ядром NetBurst

Рассмотрим работу конвейера процессора с архитектурой NetBurst от планировщика до исполнительного устройства. На этом и других рисунках статьи используется упрощённое схематическое изображение конвейера для наглядности и облегчения понимания, в нём отсутствуют некоторые промежуточные и дополнительные стадии, присутствующие в реальном процессоре Pentium 4.
Основная задача планировщика – посылать на исполнение команды, обеспечивая максимальную загрузку исполнительных блоков. Планировщику необходимо отсылать операцию на исполнение таким образом, чтобы к моменту её прибытия на исполнительное устройство все операнды были вычислены. В случае NetBurst, длина в тактах конвейера между стадиями отправки и исполнения превышает время исполнения большинства простых операций. Поэтому очередная операция должна быть отправлена до того, как предыдущая, результат которой необходим данной операции, доберётся до исполнительного устройства. Если операцию не отправить заблаговременно, исполнение будет неэффективным.
Для вычисления момента отправки очередной команды планировщик должен предсказывать состояние готовности данных, учитывая время выполнения предыдущих операций, результат которых будет использоваться в качестве операнда для данной инструкции. Когда время исполнения операций фиксировано (и таким образом заранее известно), задача планирования решается элементарным образом. Однако для ряда инструкций невозможно заранее предсказать время исполнения. Например, для операций загрузки (LD) из памяти время получения результата будет зависеть от того, на каком иерархическом уровне подсистемы кэша или памяти находятся запрашиваемые данные. Время загрузки данных командой LD может варьироваться от двух до нескольких сотен тактов. Теоретически простейший способ решения проблемы планирования команд с заранее неизвестным временем исполнения состоит в том, чтобы использовать наихудшую оценку латентности, но для команд загрузки из памяти это сотни тактов (в случае доступа к оперативной памяти). Как вариант можно просто не выпускать инструкцию ADD, зависящую от данных команды загрузки LD, из планировщика до фактического прибытия данных из памяти. Но этот способ оказывается неэффективным в процессоре с длинным конвейером, так как в этом случае эффективное время исполнения команды загрузки из кэша L1 складывается из латентности кэша L1 и расстояния в тактах от планировщика (Scheduler) до исполнительного устройства (в приведённом примере для ADD это расстояние от Scheduler до ALU_Oper).
Для обеспечения эффективного исполнения команд необходимо спекулятивно посылать команду ADD, зависящую по данным от команды загрузки LD, учитывая наилучшую оценку латентности. При этом необходим механизм отката, иначе, в случае промаха кэша L1, команда ADD может получить неправильные данные и выдать неверные результаты или заблокировать конвейер. В этом случае потребуется останавливать не только "сбойную" операцию, но и несколько поколений только что отправленных зависимых операций. Основная сложность заключается в том, что необходимо весьма оперативно вносить обширные изменения во внутренние структуры планировщика, хранящие информацию о зависимостях и состоянии готовности операндов. Так как сложные схемные механизмы отката могут вызвать проблемы на высоких частотах, инженеры компании Intel разработали компромиссный в плане простоты механизм, названный Replay.

В первом приближении


Инструкции могут выполняться неправильно по многим причинам. Это могут быть как зависимости по данным от предыдущих инструкций, так и ряд внешних условий, к которым относятся: промах кэша первого уровня, некорректный store-to-load-forwarding, скрытые зависимости по данным и некоторые другие причины.
Рассмотрим, что представляет собой система реплея (рис. 3а, рис. 3б). Выход планировщика (Scheduler) подключен к мультиплексору (Replay mux). Далее операции из мультиплексора отправляются на два конвейера. Первый конвейер – основной, по нему команда попадает на исполнительные устройства. Второй конвейер относится непосредственно к системе реплея (Replay system) и содержит пустые стадии, не производящие никакой работы, количество которых до стадии Check в точности соответствует стадиям основного конвейера. На второй конвейер отправляются точные копии операций, уходящих параллельно на первый конвейер.


Рис. 3а


Рис. 3б

Операции по обоим конвейерам движутся параллельно до стадии Check. На этой стадии блок Checker проверяет, успешно ли была выполнена операция на основном конвейере. Если всё в порядке, то операции отправляются в "отставку" (рис. 3а). Если оказалось, что в процессе выполнения по тем или иным причинам было получено "неправильное" значение (например, получен сигнал L1 Miss), то цепочка со второго конвейера перенаправляется обратно на мультиплексор через петлю реплея (replay loop) (рис. 3б). При этом (например, в случае промаха кэша первого уровня) посылается запрос в следующий уровень кэширования (в кэш второго уровня). Петля реплея может содержать дополнительные "фиктивные" стадии (на рисунке это STG. E и STG. F), количество которых подбирается таким образом, чтобы задержка на полный оборот операции по этим стадиям и конвейеру в точности соответствовала времени, необходимому на прибытие данных из нового уровня кэша (например, латентности кэша L2 – 7-ми тактам).
К моменту ожидаемого прибытия возвращаемой команды на мультиплексор блок Checker посылает планировщику специальный сигнал (stop signal), чтобы планировщик оставил в следующем такте свободный слот, в который мультиплексор сможет вставить команду, возвращённую для повторного исполнения. Все команды, зависящие по данным от неправильно выполненной операции, также будут возвращены на повторное исполнение. При этом дистанция между командами в стадиях всегда сохраняется постоянной.
Необходимо сразу отметить, что команды могут заворачивать на реплей много раз. Так, например, данные из кэша L2 могут просто "опоздать" из-за большого количества одновременных запросов к L2, и тогда придётся совершить ещё один-два дополнительных оборота, что увеличит латентность чтения L2. К примеру, данные могут прибыть из L2 не через 7 тактов, а через 9, а дополнительный оборот добавляет как минимум 7 тактов. Или данных просто может не оказаться в L2, и цепочка команд совершит большое количество оборотов в системе реплея, занимая исполнительные ресурсы, пока запрошенные данные не прибудут из основной памяти.
Дополнительные обороты команд по системе реплея являются одной из причин того, почему наблюдаемая латентность L2 может намного превосходить латентность, заявленную в документации.

"Дырки"


Так как дистанция между командами, возвращаемыми на реплей, всегда остаётся постоянной, то между этими командами существуют пустые незанятые стадии, для которых Checker не посылает планировщику стоп-сигнал. Назовём их для простоты "дырками". В такты, соответствующие "дыркам", планировщик может посылать на исполнение команды. Это позволяет максимально использовать ресурсы вычислительного канала процессора, так как позволяет смешивать команды из разных цепочек команд. Рассмотрим пример.

LD R1, [X] // загрузка X в регистр R1
ADD R1, R2 //1 – R1 = R1+R2
ADD R1, R2 //2 – R1 = R1+R2
ADD R1, R2 //3 – R1 = R1+R2
ADD R1, R2 //4 – R1 = R1+R2
ADD R1, R2 //5 – R1 = R1+R2
LD R3, [Y] // загрузка Y в регистр R3
ADD R3, R4 //6 – R3 = R3+R4
ADD R3, R4 //7 – R3 = R3+R4
ADD R3, R4 //8 – R3 = R3+R4
….


У нас есть две цепочки зависимости команд: цепочка зависимости по регистру R1 и цепочка зависимости по регистру R3. Для простоты положим, что все команды всех типов поступают на один планировщик последовательно, и поэтому команда LD R3, [Y] не может быть спланирована на выполнение раньше пятой команды ADD R1, R2. Латентность команды загрузки при попадании в кэш L1 примем равной двум тактам, а латентность команды ADD – одному такту.
Рассмотрим два случая.


Рис. 4а


Рис. 4б

1. Значения X и Y находятся в кэше L1 (L1 hit, рис. 4а). В данном случае нет никаких сюрпризов. Ни одна команда не заворачивается на реплей, все команды последовательно уходят в "отставку".
2. Значения X нет в кэше L1, но оно есть в кэше L2 (L1 miss). Значение Y находится в кэше L1 (рис. 4б). Этот случай намного интереснее. До такта 6 планировщик отправляет на исполнение команды с учётом их ожидаемых времён исполнения. Первая команда LD на такте 6 достигает стадии Check на втором конвейере, получает сигнал о промахе кэша L1 и заворачивает на реплей (в этот же момент генерируется запрос к кэшу второго уровня на считывание данных X). Следующие 5 команд ADD вслед за первой командой LD не смогут получить правильный операнд и тоже вынуждены будут завернуть на реплей. До того как первая команда LD подойдёт к мультиплексору, планировщик успевает отправить вторую команду LD вслед за ADD5 в такте 8 и в этот момент получает сигнал оставить свободный слот для первой команды LD в следующем такте. В такте 9 первая команда LD достигает мультиплексора, и происходит её перезапуск. В такте 10 с петли реплея не приходит ни одной команды, поэтому планировщик заполняет образовавшуюся в цепочке повторно исполняемых команд "дырку" очередной командой ADD6, ожидающей исполнительные ресурсы. В следующих тактах происходит перезапуск команд ADD1 – ADD5, следующих за первой командой LD. Вслед за последней командой ADD5 на освободившийся конвейер планировщик сможет выпустить команды ADD7 и ADD8. В такте 14 первая команда LD получает данные, доставленные из L2, и выполняется корректно, поэтому первая команда LD и следующие за ней ADD1 – ADD5 завершают исполнение.
На этом примере видно, что, используя "дырку" между первой командой LD и ADD1, планировщик пытается более эффективно использовать вычислительные ресурсы и вставляет ADD6 из независимой по данным цепочки команд. Но, к сожалению, у этого стремления к эффективности есть обратная сторона, рассматриваемая ниже.

"Зацикливание" петель реплея


В предыдущем примере мы рассмотрели вопрос попадания в "дырку" команд из независимой по данным цепочки команд. К сожалению, планировщик современных процессоров NetBurst недостаточно "умён", чтобы отследить цепочку зависимости команд, завёрнутую на реплей. Иными словами, планировщик всегда считает, что операция выполнится успешно. Рассмотрим теперь вопрос попадания в "дырку" команды из той же цепочки зависимости. Этот вопрос очень важен для оценки производительности, так как неявные цепочки зависимости часто встречаются в программах.

В качестве простейшего примера можно привести цикл накопления суммы, в котором каждая последующая итерация зависит от результата предыдущей.
for (int I = 0; I< count; I++)
sum+=array[I];


Итак, проанализируем работу реплея на примере простой длинной цепочки зависимости по данным (рис. 5).

LD R1, [X] // загрузка X в регистр R1
ADD R1, R2 //1 – R1 = R1+R2
ADD R1, R2 //2 – R1 = R1+R2
ADD R1, R2 //3 – R1 = R1+R2
ADD R1, R2 //4 – R1 = R1+R2
ADD R1, R2 //5 – R1 = R1+R2
ADD R1, R2 //6 – R1 = R1+R2
ADD R1, R2 //7 – R1 = R1+R2
ADD R1, R2 //8 – R1 = R1+R2
ADD R1, R2 //9 – R1 = R1+R2
………


Как и в предыдущем примере (рис. 4б), латентность команды загрузки при попадании в кэш L1 примем равной двум тактам, а латентность команды ADD – одному такту. Значения X нет в кэше L1 (L1 miss), но оно есть в кэше L2.


Рис. 5

В модели на рис. 5 планировщик, начиная с первого такта, беспрепятственно отправляет на конвейер поток команд к исполнительным устройствам с учётом их ожидаемых времён исполнения. Как и в примере на рис. 4б, на такте 6 команда LD достигает стадии Hit/Miss на первом конвейере, где происходит событие L1 Miss, и её копия на стадии Check на втором конвейере получает сигнал о промахе кэша L1 и заворачивает на реплей. Идущие следом команды ADD не могут быть выполнены успешно и также заворачивают на реплей. Команда LD подходит к мультиплексору в такте 8 и в этот момент получает сигнал приостановиться и оставить свободный слот для LD, и в следующем такте 9 происходит перезапуск LD в мультиплексоре. Далее с такта 10 начинаются негативные последствия реплея. Планировщик не способен проследить цепочку зависимостей, начинающуюся с LD, и поэтому в такте 10 вставляет команду ADD7 в "дырку" между LD и ADD1. Очевидно, что команда ADD7 не может успешно выполниться раньше, чем ADD6, но фактически ADD7 оказывается на конвейере перед ADD6. Поэтому можно видеть, как на такте 14 операция LD, получив данные, уходит в "отставку", а ADD7 заворачивает на реплей, увлекая за собой команды ADD8, ADD9 и все остальные команды, которые могли бы последовать за ними. Между командами ADD7 и ADD8 существует большая "дыра", в которую попадутся другие следом идущие команды, и так далее. Кроме того, "дырки" могут образовываться благодаря командам из других цепочек, отправленных планировщиком на исполнение при переупорядочивании команд.
Таким образом, наблюдается следующая картина: планировщик, не подозревая о том, какие команды выполнились правильно, а какие нет, продолжает отправлять на исполнение новые команды из зависимой цепочки, вставляя их в "дырки" между командами, возвращаемыми на реплей. Так как все предыдущие команды не успели выполниться правильно, команды, попавшие в "дырки", также выполнятся неправильно и вынуждены будут возвратиться на реплей. Все команды этой цепочки будут проворачиваться через петлю реплея подобно звеньям одной цепи, обвитой вокруг стержня. Если нарисовать временную диаграмму, отражающую состояние стадии Dispatch в каждый момент времени, то мы будем наблюдать следующую схему:


Рис. 6. "Затягивание" цепочки команд в реплей

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

Как нам удалось выяснить, именно "дырки" между перезапускаемыми командами, создающие "долгоиграющие" петли реплея, являются основной причиной, по которой цепочка зависимых команд MOV EAX, [EAX] не может помочь при измерении латентности кэша L2 процессоров Pentium 4. Наличие "дырок" также объясняет график тестирования латентности в начале статьи. Проблема, как оказывается, в том, что команды из зависимой цепочки, попадая в "дырки" при негативном сочетании факторов, могут дружно "зацикливаться" и проводить в системе реплея несколько лишних оборотов, что увеличивает общее время исполнения цепочки команд. Количество таких "зацикливаний" зависит от сочетания "дырок", команд загрузки и команд между командами загрузки.
Мы провели исследования, анализируя недокументированные счётчики команд, возвращаемых на реплей, а также писали специальные тесты, подбирая команды между загрузками таким образом, чтобы организовались "заплатки" к "дыркам", не позволяющие командам из зависимой цепочки проникать в "дырки" между командами, возвращаемыми на реплей. Результаты экспериментов подтвердили теорию: если вовремя "затыкать" "дырки" другими командами и тем самым уберечь цепочку от зацикливания, то скорость исполнения кода резко повышается, а измеряемая латентность кэшей в точности соответствует указанным в документации значениям.


Таким образом, на NetBurst нельзя переносить обычный способ подсчёта задержек (например, задержек при промахе кэша первого уровня). Это будет не только значение латентности, но и дополнительная "накрутка" за счёт реплея. "Накрутка" может быть очень большой, в результате чего эффективная латентность может достигать сотни тактов вместо девяти. Что хуже, реплей даёт не только задержку выполнения одной инструкции, но и блокирует часть исполнительных ресурсов, которая могла бы быть доступна другим независимым операциям.

Дополнительные петли реплея


Промах кэша первого уровня L1 – это основное, но не единственное событие, приводящее к реплею. Кроме него можно выделить ещё ряд событий (список неполный):

Промах L1, при этом в L1 содержится строка с определёнными совпадающими битами адреса (так называемый aliasing; биты, ответственные за aliasing, отличаются у Willamette/Northwood и Prescott).
Промах DTLB.
Невозможность STLF (store-to-load-forwarding).
Выделение новых строк в Write Buffer.

События эти возникают на разных стадиях конвейера. Время, необходимое для прибытия корректных данных, также может отличаться. Для решения этой проблемы существуют дополнительные петли реплея.
С пристрастием изучая латентности этих событий на процессоре Pentium 4 Northwood, мы с интересом обнаружили, что некоторая часть событий (например, L1 miss L2 hit; L1 64KB aliasing) вызывает задержку, соответствующую произведению полной длины петли реплея (7 тактов) на число прохождений + 2 такта (латентность доступа к L1D). А латентность другой части событий (например, L1 hit DTLB miss; L1 1MB aliasing) кратна 12 + 2 такта. Это говорило о существовании ещё одной петли реплея, более широкой. Для простоты будем дальше называть петлю с полным периодом переисполнения команды в 7 тактов RL-7, петлю с периодом переисполнения 12 тактов, соответственно RL-12. Теперь рассмотрим, как они работают.
Рассмотрим следующий достаточно часто встречающийся в реальных программах случай: строка с запрашиваемыми данными находится в L1, но в DTLB отсутствует запись о странице памяти, которой принадлежит строка (L1hit, DTLB miss).


Рис. 7. Блок-схема, иллюстрирующая работу системы реплея с двумя петлями

Цепочка команд с LD во главе движется по конвейеру. На стадии CacheRead операция LD инициировала обращение к контроллеру кэша L1 и на стадии Hit/Miss получила сигнал L1 Hit, означающий, что тэг запрашиваемой строки был найден в L1. Параллельная стадия EarlyCheck, получив сигнал о том, что операция LD выполняется успешно, не заворачивает LD для переисполнения, позволяя ей дальше двигаться по конвейеру вместе с зависимыми командами. Кэш L1 процессора Northwood устроен таким образом, что просмотр его тэгов происходит гораздо быстрее, чем просмотр буферов трансляции виртуальных адресов в физические (TLB). А ёмкость TLB в записях гораздо меньше, чем ёмкость кэша L1D в линиях. Поэтому, если после просмотра DTLB не будет найдена запись для запрашиваемой страницы, то дальше команду LD ждёт сюрприз. Через несколько стадий, когда LD подойдёт к стадии LateCheck, происходит событие DTLB miss. И команда LD, к всеобщему неудовольствию, будет вынуждена завернуть на реплей. Все команды, зависящие от LD, также вслед за ней поворачивают на реплей на той же стадии LateCheck. Одновременно с разворотом на реплей со стадии LateCheck посылается сигнал force-early-replay-safe на стадию EarlyCheck. Основное назначение этого сигнала – не допустить разворот любой неверно выполненной команды, находящейся на стадии EarlyCheck, чтобы обе команды не подошли одновременно к мультиплексору и не создали конфликтов. Но это не значит, что команда, получившая force-early-replay-safe, в любом случае выполнилась успешно, просто её перезапуск временно отложен до стадии LateCheck. Блок LateChecker умеет обрабатывать полный набор реплей-событий (включая события, обрабатываемые EarlyChecker). Поэтому если команда должна была быть возвращена на переисполнение на стадии EarlyCheck (например, получила сигнал L1 miss одновременно с force-early-replay-safe), то она обязательно будет развёрнута на стадии LateCheck.
Возникает вопрос: зачем необходимо наличие двух параллельно работающих петель RL-7 и RL-12 в каждом вычислительном канале, если все команды можно разворачивать на перезапуск на RL-12? Ответ на него достаточно прост. Процессор старается производить спекулятивное переисполнение. Разворот команды LD при промахе кэша первого уровня на более раннем этапе обеспечивает скорейший её перезапуск и уменьшает латентность в случаях, когда данные удаётся быстро отыскать в кэше второго уровня. Таким образом, RL-7 играет роль вспомогательной петли, сокращающей латентность в ряде случаев.
В процессоре на ядре Prescott произошли некоторые изменения в структуре петель. Из-за возросшей латентности кэша первого уровня до 4-х тактов в нём совпадают стадии, на которых происходит выявление событий L1 miss и DTLB miss. А из-за возросшей до 18-ти тактов латентности кэша второго уровня необходимость в скорейшем перезапуске команд загрузки данных тоже отпала. Поэтому в процессоре Prescott существуют только петли реплея одного уровня с длиной полного оборота в 18 тактов (RL-18), что подтверждают проведённые нами тесты.

Реплей на конвейере FPU-блока


В механизме реплея на конвейере FPU используется схема, отличная от конвейера ALU. Здесь, очевидно, используется "обратная связь" между блоком загрузки и планировщиком. Планировщик отправляет зависимую инструкцию после успешного завершения первой проверки ("check") на наличие в L1 данных. Соответственно, в случаях раннего обнаружения неприятностей (аналогичных петле RL-7 для ALU-загрузки) команды группы FP-Load, к которым относятся операции загрузки x87, MMX, SSE и SSE2, "переигрываются", однако выпуск зависимых операций не производится.Для RL-12 различий нет – в этом случае FP-операции также циркулируют на RL. Латентность операций FP-загрузки в случае нахождения данных в L1 составляет 9 тактов. В случае кэш-промаха добавляется n*7 или n*12 тактов, в зависимости от ситуации. Нам вообще не удалось "загнать" какую-либо последовательность FP-операций на RL-7. Например, если на RL-7 "крутится" Int-цепочка, то зависимая от неё FP-цепочка попадёт на RL-12. К примеру, две инструкции "MOVD MM0,EAX – MOVD EAX,MM0" переводят Int-цепочку с RL-7 на RL-12 (зависимость по EAX).
Почему это так, а не иначе? У нас есть гипотеза, что подавляющее большинство инструкций, направляемых через FP Move, проходят в конечном счете блок вроде "Convert & Classify" процессора AMD K8, где производится перевод результата в необходимое внутреннее представление ("formatting"). В пользу этого говорят:

очень большие значения латентности операций межрегистровой пересылки;
факт, что ощутимые штрафы вызывают последовательности разнородных команд, обрабатывающих содержимое одного SSE-регистра, вроде "ADDSD XMM0,XMM0 – ADDSS XMM0,XMM0".

Возможно, многие операции FP Move представляют собой более-менее жестко фиксированные пары примитивных команд "load + convert" или "convert + store", в которых на исполнение "части" convert приходится ~6-7 тактов. Возвращаясь к реплею, при такой (гипотетической) организации время выполнения "convert" превышает "расстояние" в тактах между планировщиком и исполнительным блоком, и таким образом здесь имеется достаточный запас для того, чтобы планировщик мог безболезненно отправлять зависимую операцию по результату первой проверки. В случае неудачи будет переигрываться только пара "load + convert".

Нарушение STLF


Интересующиеся микроархитектурой NetBurst знают, что в этой микроархитектуре операция Store разбивается на две условно-независимые микрооперации Store Data (STD) и Store Address (STA), результаты исполнения которых объединяются в Store Buffer (SB). Данные хранятся в SB до "отставки" команды записи, после которой они сохраняются в кэше и оперативной памяти через промежуточный Write Buffer, в котором производится "склеивание" модифицированных данных. Спекулятивное чтение командами загрузки данных, ещё не выгруженных в кэш, непосредственно из SB называется store-to-load-forwarding (STLF).
Для того чтобы STLF прошел успешно, требуется соблюдение ряда дополнительных условий:

данные, запрашиваемые операцией чтения, могут "добываться" либо из SB, либо из L1D, но не в результате комбинации, поэтому размер и адрес читаемых данных должны соответствовать записи в SB;
к моменту выполнения загрузки SB должен содержать корректно вычисленные результаты STA и STD.

Нарушение любого из этих условий может привести к неприятным задержкам. Перезапуск команды загрузки, которая не смогла выполниться успешно вследствие нарушения STLF, также осуществляется через систему реплея.
Итак, чтобы Store мог передать результат для последующего Load, SB к моменту выполнения загрузки должен содержать результаты STA и STD. Хотя "IA-32 Intel® Architecture Optimization Reference Manual" и относит это условие к разряду "Store Forwarding Restrictions", мы должны понимать, что оно достаточно специфическое, – как по последствиям, так и по типу обработки. Нарушение STLF в строгом смысле, когда невозможна передача данных, уже содержащихся в SB, приводит к длительной задержке: Store и все предшествующие ему инструкции должны уйти в "отставку", и результат Store должен быть записан в кэш. В нашем же случае "STLF Restriction on Data Availability" необходимо только дождаться появления результата STA/STD. Как можно догадаться, для этого используется реплей: LD и все зависимые инструкции отправляются на RL и циркулируют там до появления результатов Store. Мы изучали два варианта нарушения STLF: к моменту исполнения Load не готов либо STD, либо STA (третий вариант – не готово вообще ничего – будет определяться наиболее злостными последствиями первых двух), так как их существенно труднее предупредить в коде программы, в отличие от нарушения размеров данных. При тестировании нарушений STLF выяснилось, что они могут приводить к переисполнению как по RL-7, так и по RL-12, в зависимости от разновидности нарушения. Неготовность STD приводит к перезапуску по RL-7, а неготовность STA отправит команду на RL-12. Почему отсутствие результата STD ведёт к RL-7, а отсутствие STA – к RL-12, – понять нетрудно: если при обработке Load в момент просмотра Store Buffer обнаруживается позиция с совпадающим адресом, то возможное отсутствие данных сразу же детектируется. Однако, если данные есть, но адрес не прибыл, – процессору не остаётся ничего другого, как надеяться на лучшее и предполагать, что этот Store не затронет Load. Позже будет проведена проверка, и в случае неприятностей операция пойдёт на RL-12.
Для того чтобы LD был успешно выполнен с первого захода, необходимо, чтобы он был отправлен на исполнение не ранее STD и STA. При этом по отношению к STA необходима задержка LD минимум на 3 такта. Процессор не даст Load выполниться раньше STA, однако 3 такта – более чем широкая "лазейка", которой планировщик очереди Load всенепременно будет пытаться воспользоваться, так как ничего не знает о скрытой зависимости адресов со Store.
Архитектура Pentium 4 предоставляет самые благоприятные условия для "преждевременного" выполнения загрузок – для этого у него есть очереди для адресных операций. Поэтому нарушения STLF гарантированно избежать невозможно даже на уровне изменения реализации алгоритма. Любая пара зависимых Store-Load, находящаяся в "окне" из нескольких десятков инструкций, является потенциальным источником неприятностей.

В качестве очень показательного примера хочется отметить вызовы функций, которые есть во всех без исключения программах. Вызов функции, как правило, осуществляется последовательностью команд сохранения параметров в стеке PUSH и командой вызова функции CALL.
….
PUSH EAX
CALL Func
………
Далее внутри вызова функции происходит сохранение регистров и чтение параметров.
PUSH ESI
MOV EAX, [ESP+8]
………
Легко заметить, что пара PUSH EAX и MOV EAX, [ESP+8] является потенциальной причиной реплея в связи с нарушением условий "STLF Restriction on Data Availability" как из-за неготовности STD, когда в процедуру передается значение, являющееся результатом длительных вычислений, так и из-за неготовности STA, в случае, если планировщик отправит MOV EAX, [ESP+8] на исполнение менее чем через 3 такта после PUSH EAX
.

Взаимоблокировки (deadlocks)


Здесь нам хотелось бы остановиться ещё на одной неочевидной проблеме, возникающей из-за реплея. Рассматривая принцип работы системы реплея, мы несколько раз уже говорили о том, что планировщик приостанавливает работу, когда команда, возвращаемая на реплей, подходит к мультиплексору. При этом вполне возможна ситуация, когда вся цепочка команд, отправленная планировщиком на конвейер перед приостановкой работы, будет возвращена на реплей. В этой ситуации вся цепочка переисполняемых команд будет крутиться в системе реплея, и до тех пор, пока необходимые данные не прибудут, переисполняемые команды не будут выполнены успешно. А что будет, если для успешного исполнения команд, крутящихся по системе реплея в ожидании данных, необходимо выполнить команду, которая по каким-то причинам всё ещё находится в планировщике, но не может покинуть его, так как конвейер полностью забит? Такая ситуация называется взаимоблокировкой (deadlock). Ситуацией, при которой возможна взаимоблокировка, является неготовность STD при store-to-load-forwarding. Проиллюстрируем это на примере цепочки команд.

IMUL EAX
MOV [ESI], EAX
MOV EBX, [ESI]
14*{ AND EBX, EBX } // and ebx, ebx, повторённое 14 раз


Команда сохранения в памяти разбивается на условно независимые микрооперации STA и STD. Эти микрооперации попадают в разные планировщики: STA попадает в тот же планировщик, что и команда LD, а STD попадает в один планировщик с командами AND. Это разделение микроопераций и становится причиной взаимоблокировки. Происходит это так:

пока вычисляется значение EAX, планировщик Mem отправляет микрооперацию STA команды MOV [ESI], EAX на исполнение, так как значение ESI уже было вычислено;
вслед за STA планировщик Mem отправляет на исполнение микрооперацию LD, соответствующую команде MOV EBX, [ESI], в надежде на то, что к моменту исполнения LD данные микроопераций STA/STD будут доступны;
через два такта вслед за микрооперацией LD планировщик Fast_0 начинает спекулятивно отправлять цепочку команд AND EBX, EBX на исполнение в FastALU0. STD в это время ещё не может быть отправлена, так как данные EAX ещё не готовы;
так как STD ещё не готово, LD, достигнув стадии проверки результата, заворачивает на реплей;
вслед за LD на реплей заворачивает цепочка команд AND на своём исполнительном конвейере;
так как планировщик FastALU0 ничего не знает о подвохе, он продолжает поставлять команды AND на исполнительный конвейер до тех пор, пока не вынужден будет приостановиться, чтобы пропустить команды AND, возвращаемые для перезапуска;
конвейер FastALU0 оказывается полностью забит циркулирующими командами AND, которые не могут быть исполнены правильно, пока не выполнится LD. LD ждёт STD, который может отправляться на исполнение после вычисления значения EAX, но не отправится, так как конвейер забит командами AND. Это deadlock.



Рис. 8. Взаимоблокировка

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

Механизм вынужденного выхода из реплея


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

{XOR EAX,EAX}
256* {AND EAX,EAX}
{MOV EAX,[EAX+ESI]} // L1 miss, L2 hit
{AND EAX,EAX}
{ADD EAX,EAX} // "дырка"
N* {AND EAX,EAX}


Рассмотрим диаграмму, отражающую разницу (в тактах) времен исполнения последовательности в случаях "L1 miss, L2 hit" и "L1 hit" в зависимости от длины цепочки (рис. 9а). Мы видим, что на коротких цепочках система ведет себя вполне предсказуемо: наши грубые оценки времени исполнения четко совпадают с теоретическими значениями. Постепенно увеличивая количество инструкций в цепочке зависимых AND, мы наращиваем количество оборотов RL – через каждые 14 новых последовательностей время исполнения цепочки скачкообразно возрастает на 7 тактов. И при N=82 наблюдается неожиданная задержка (~22 такта), причина которой с первого взгляда совершенно не понятна. Для выяснения причин задержки были привлечены счётчики производительности процессора Pentium 4, среди которых есть подгруппа, учитывающая события, связанные с реплеем. Анализ счётчиков производительности, подсчитывающих количество инструкций, возвращённых на переисполнение, показал, что инструкции, начиная с N=87, перестают заходить на реплей (рис. 9б).


Рис. 9а. Относительное время исполнения цепочки с "дыркой" в
зависимости от её длины



Рис. 9б. Количество переигрываемых команд цепочки с "дыркой" в
зависимости от её длины

Очевидно, что задержка каким-то образом связана с принудительным выходом из реплея. Но какой же механизм останавливает переигрывание цепочки?
Допустим, остановка происходит всё же "естественным путем", то есть без привлечения специального внешнего механизма. Например, из-за переполнения внутренних ресурсов процессора. Вопрос в том, каких ресурсов, так как по объёму основных (ROB, пул внутренних регистров и др.) имеется существенный запас, позволяющий процессору выдержать намного более сильные "стрессы". Мы пока не будем исключать этот вариант из рассмотрения, но заметим, что он не является решением для случая взаимной блокировки – там проблемы всё равно придется ликвидировать тем или иным способом.
Подойдем с другой стороны: самое простое и кардинальное решение проблем – сброс конвейера. В таком случае можно ожидать, что повторный поток инструкций будет немедленно обнаружен с помощью счетчиков. Однако в экспериментах ничего подобного не наблюдается, что неудивительно, так как сброс конвейера – метод довольно грубый.
В случае цепочек из наших тестов "с дырками" можно было бы поступить очень просто: достаточно перекрыть на некоторое время выход из очередей планировщика. Это довольно естественное решение, так как проблемы связаны как раз с тем, что планировщик, не имея представления о событиях, происходящих на RL, продолжает отсылать туда новые инструкции. Однако, опять же, такой метод не то чтобы совершенно не годится для прерывания взаимоблокировки, – он является полной противоположностью тому, что сделать следовало бы. Действительно, там нам требуется каким-то образом "втиснуть" на RL инструкцию, застрявшую на планировщике, – а мы её собираемся окончательно изолировать. К тому же видно, что при дальнейшем увеличении цепочки на реплей попадают ещё, по крайней мере, 5 инструкций. Это с нашим предположением совершенно не согласуется.
Оставался ещё один вариант, который нам и предстояло проверить: перекрытие входа в планировщик. Так как изложение всех нюансов расследования займёт целую статью, мы вынуждены привести только выводы по результатам тестов.
Мы точно выяснили, что наступает момент, после которого на исполнение (с последующим переигрыванием) может быть отправлено только восемь инструкций, что в точности соответствует глубине очереди schQ FAST_0.
Вывод напрашивается сам собой: мы имеем дело ни с чем иным, как с перекрыванием входа в планировщик. Это очень важный вывод, так как такой механизм является вполне логичной основой для единой и универсальной системы остановки реплея, системы, применимой как в случаях взаимоблокировки, так и в случаях "длинных цепочек". Тесты подтверждают, что в момент перекрытия входа планировщика перекрывается вход не только очереди schQ FAST_0, но и других планировщиков тоже, о чём свидетельствует ограниченное число инструкций, попадающих на реплей с этих планировщиков после перекрытия их входов. Накладные расходы на остановку составляют, по грубым оценкам, 15-35 тактов.
Система в течение определенного времени отслеживает соотношение переигрываемых инструкций, новых инструкций и незанятых позиций (назовём их для краткости паттернами), однако на основании имеющегося материала невозможно восстановить точный алгоритм, по которому принимается решение о необходимости остановки. Тесты показали, что точкой отсчета времени в системе является событие, вызывающее сериализацию, поэтому если взять тестовый код нашего примера, оставив его неизменным, но немного модифицировав вызывающую процедуру, то максимальное количество переигрываемых инструкций, скорее всего, окажется иным.
Нам необходимо заметить, что остановка длинных цепочек не носит глобального характера. Так, последовательность из зависимых сдвигов никогда не останавливается. То же справедливо и для многих других инструкций – то есть, отсутствие остановки – скорее правило, а не исключение. Модельные "цепочки с дырками" при заходе на RL-7 останавливаются всегда, но, оказавшись на RL-12, при определенных положениях "дырки" продолжают "крутиться до бесконечности". Зацикливание наших модельных цепочек приводит к идеальной повторяемости состояния конвейера через каждые 14 тактов, чего в реальных программах не наблюдается. Таким образом, тут создается впечатление, что мы имеем дело не с механизмом, специально предназначенным для разрешения таких ситуаций, а с системой, призванной для решения иных задач, и включающейся здесь только в определенных, благоприятных условиях.
По нашему мнению, механизм предназначен в основном как раз для решения задач выхода из взаимоблокировки. Внимательный читатель спросит: "Но как же перекрытие входа планировщика решит проблему взаимоблокировки?". По нашему мнению, сценарий здесь такой: после перекрытия входа планировщика цепочка, "циркулирующая" по системе реплея, перенаправляется в некий специальный буфер, и на освободившееся на конвейере место проникают оставшиеся в планировщике микрооперации, которые могут успешно исполниться, решая тем самым проблему взаимоблокировки.

Запись данных в Write Buffer


Здесь история сама по себе оказывается довольно забавной – всё началось с обнаружения совершенно "левых" петель реплея, возникающих как бы из ничего и на ровном месте. Разумеется, была поставлена задача – петли отловить и причину выяснить, – однако поначалу отлов происходил медленно, т.к. петли и возникали часто, и столь же часто по неизвестным обстоятельствам исчезали. Тщательное расследование показало, что причины этих петель связаны с записью данных в Write Buffer. Остановимся на них чуть-чуть подробнее.
Кэш первого уровня процессоров Pentium 4 имеет политику обновления данных Write Through, что подразумевает одновременное обновление содержимого не только L1, но и L2. Поэтому запись, в конечном счёте, ведётся в L2, однако результаты множественных операций накапливаются в WB с тем, чтобы передача в L2 осуществилась за минимальное число транзакций. После того, как в строку, не содержащуюся в WB, производится запись, она на некоторое время блокируется для чтения.
Чтобы возникла ситуация, связанная с блокировкой чтения данных из Write Buffer, требуется выполнение ряда условий:

строка с адресом, по которому производится запись, должна содержаться в L1;
запись в Write Buffer для этой строки ещё не выделена;
между записью строки в Write Buffer и чтением из неё данных должно пройти определённое время.

При этом адреса для операций записи и чтения могут отличаться – они должны всего лишь попадать в одну и ту же строчку. Как показали произведённые тесты, в том случае, если чтение производится из первой половины строчки (байты 0..31), Load пойдёт на реплей во временном диапазоне 21-33 такта включительно после Store, а для второй половины (байты 32..63) во временном диапазоне 21-34 такта после Store. Различие ровно в один такт для младших и старших 256 бит строки указывает на то, что в эти периоды происходит объединение модифицированных данных с немодифицированными из кэша.
Процессор Pentium 4 Northwood имеет Write Buffer, содержащий 6 строк по 64 байта. Это немного, поэтому, если операции записи и чтения идут достаточно плотно, то к задержкам записи могут добавляться задержки, связанные с невозможностью чтения сохранённых данных из Write Buffer и перезапуском команд чтения через систему реплея.

Влияние реплея на Hyper-Threading


Основная цель технологии Hyper-Threading – повысить КПД использования вычислительных ресурсов за счёт того, что два потока не имеют зависимости по данным, и один из потоков может использовать те вычислительные ресурсы, которые не использует другой, особенно в моменты, когда один из потоков простаивает. Обычно в процессорах ожидание данных из RAM вызывает длительные простои вычислительных ресурсов. Самое время эти ресурсы использовать потоком, не ожидающим данных из RAM.
Но что будет с производительностью, когда вмешивается реплей?
Как уже было описано выше, "зацикливание" цепочки команд в системе реплея на долгое время может приводить к большому и совершенно неэффективному потреблению ресурсов. Например, в случае отсутствия данных в кэшах первого и второго уровней цепочка команд будет вынуждена совершить десятки, а то и сотни оборотов в системе реплея, бесцельно занимая вычислительные устройства в ожидании прибытия данных из основной памяти. Если NetBurst-процессор однопоточный, то ожидание данных из памяти в системе реплея не создаёт больших дополнительных проблем производительности, так как процессор в любом случае теряет сотни тактов в ожидании данных из оперативной памяти (вычислительный поток всё равно надолго приостанавливается в ожидании данных). Дополнительная работа узлов процессора сказывается в этих случаях больше на тепловыделении. :) Но когда на процессоре одновременно исполняются два потока, неэффективное потребление ресурсов многократным реплеем одного из потоков просто не может не сказываться на производительности другого. Можно предположить, что, чем чаще поток обращается к данным, отсутствующим в кэшах первого и второго уровней, тем больше ресурсов он потребит из-за реплея в ожидании данных.
Мы решили проверить теорию на практике. Для этого была написана программа, один поток которой имеет длинную цепочку зависимости по данным и для вычислений постоянно обращается к данным в памяти по случайным адресам, а другой поток просто проводит вычисления на регистрах, почти не обращаясь к памяти. Оба потока исполняют команды одного типа (AND) на одном и том же FastALU0. Целью эксперимента была проверка того, как изменяется производительность второго потока, не обращающегося к памяти, в зависимости от того, обращается первый поток к данным в кэше первого уровня, второго уровня или оперативной памяти. Результаты тестирования процессора Pentium 4 Northwood приведены на рис. 10.


Рис. 10. Тестирование влияния реплея на
Hyper-Threading (процессор Northwood)

На рис. 10 отображена зависимость производительности второго вычислительного потока (Поток2) от размера буфера данных первого потока (Поток1), обращающегося к данным по псевдослучайным адресам.
Результаты говорят сами за себя. Ожидание данных из памяти одним потоком приводит к ощутимому замедлению скорости исполнения второго потока (> 35% по сравнению с ожиданием данных из L1). Поток, ожидающий данные из оперативной памяти, вместо того чтобы освободить ресурсы на время простоя, занимает их больше (!), чем во время нормального исполнения, когда данные находятся в L1. Ситуацию при HT усугубляет тот факт, что два потока разделяют объём L1 и L2 между собой, а значит эффективный объём кэш-памяти, приходящийся на каждый поток, сокращается вдвое. Это в свою очередь означает, что увеличивается число кэш-промахов и, как следствие, реплей-случаев, а значит снижается производительность обоих потоков. Именно реплей может быть одной из причин того, почему включение HT на некоторых задачах приносит вред вместо пользы.
Разобравшись с результатами, показанными процессором Pentium 4 с ядром Northwood, мы решили протестировать процессор с новым ядром Prescott, тем более, что компания Intel заявляла об усовершенствовании технологии Hyper-Threading в этих процессорах. Получив результаты тестирования, отображающие влияние количества кэш-промахов (а значит и реплея) одного потока на производительность другого потока, мы не остались разочарованными.


Рис. 11. Тестирование влияния реплея
на Hyper-Threading (процессор Prescott)

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

Replay Queues


В чём же причины? Так как официальные руководства по-прежнему упрямо замалчивали причины наблюдаемых явлений, нам пришлось самим искать информацию в патентах Intel. Мы разыскали очень интересный патент "Multi-threading for a processor utilizing a replay queue", который, как нам кажется, проливает свет на наблюдаемые результаты. Рассмотрим вкратце, что такое replay queues, и как они работают.


Рис. 12. Блок-схема, иллюстрирующая работу системы реплея с Replay Queues

В системе, изображённой на рис.12, команды, которые возвращаются на реплей со стадии Check, попадают в Replay queue loading controller (контроллер постановки в очередь реплея), основное назначение которого – принятие решения о дальнейшей судьбе команд, возвращаемых на реплей. В том случае, если команда загрузки не смогла выполниться из-за промаха кэша первого уровня, ей даётся шанс попытаться получить данные из кэша второго уровня, и контроллер возвращает команду в мультиплексор непосредственно через петлю реплея. В случае, если при попытке переисполнения команды произошёл промах кэша второго уровня, контроллер очереди реплея получает сигнал L2 Miss и отправляет команду в очередь реплея, чтобы она не занимала без дела исполнительные устройства, ожидая сотни тактов свои данные из RAM. Контроллер также отправляет в эту же очередь все неудачно исполненные команды, которые программно идут за первой операцией, попавшей в очередь, так как возможны скрытые зависимости по данным от неё, из-за которых неудачно исполненные команды могли быть возвращены на реплей. Так как на процессоре, поддерживающем Hyper-Threading, могут одновременно исполняться команды двух независимых потоков, необходимы две независимые очереди для команд разных потоков, которые будут использоваться параллельно друг другу.
Команды выпускаются из очереди реплея на повторное исполнение, когда Replay queue unloading controller (контроллер освобождения очереди реплея) получает сигнал Data return, означающий, что из оперативной памяти в кэш прибыли данные. Контроллер освобождения очереди выпускает команды обеих очередей на исполнение в надежде, что команды будут исполнены успешно. Выбор, какой из входов мультиплексора примет очередную команду (от одной из очередей реплея, петли реплея или планировщика), ложится на систему приоритетов. Могут быть использованы разные схемы приоритетов: фиксированная (предпочтение отдаётся потоку 0, затем потоку 1, после чего петле реплея и последнему планировщику) или возрастная (предпочтение отдаётся инструкциям в порядке их прихода в планировщик). Точная схема, использованная в Prescott, нам, к сожалению, не известна.
Кроме описанной выше борьбы с растратой вычислительных ресурсов цепочкой команд, ожидающих прибытия данных из оперативной памяти, replay queues вполне могут быть использованы для борьбы с взаимоблокировками, о которых мы писали выше. Допускаем, что в подобный буфер сбрасываются команды, застрявшие в системе реплея в процессоре Northwood.
Вполне возможно, что в Prescott реализована именно схема replay queues, описанная в патенте "Multi-threading for a processor utilizing a replay queue", по крайней мере, об этом говорит качественная разница результатов в сравнении с Northwood. Однако мы не можем не отметить, что скорость исполнения второго потока в Prescott всё равно падает до 20%, если первый поток, использующий команды такого же типа, имеет много промахов кэша первого уровня.

Итоги


Подводя итог можно сказать, что реплей не является самостоятельной чертой архитектуры NetBurst, призванной увеличить производительность процессоров. Скорее, реплей – это "обратная сторона медали" длинного конвейера, вспомогательный механизм, необходимый для исправления ошибок спекуляции. Снижение производительности за счёт реплея является расплатой за высокую тактовую частоту. Возможно, именно по этой причине скудные упоминания о реплее лишь изредка можно встретить в официальных руководствах и публикациях Intel. Очень часто реплей приводит к совершенно неоправданной растрате ресурсов и снижению производительности. Отсутствие описания причин и следствий реплея в официальных руководствах приводит к тому, что многие программисты о его существовании не подозревают и не могут оптимизировать свои программы с учётом его существования. Это вызывает лишь сожаление. Тот факт, что в процессорах Prescott появились replay queues, уменьшающие негативное влияния реплея на параллельно работающие потоки при включенной технологии HT, несомненно, новость хорошая, однако и в этом случае наблюдается 20-процентное снижение производительности второго потока из-за реплея первого потока.


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