Boroda_2
.docАссоциативная кэш-память с множественным доступом по сути гораздо сложнее, чем кэш-память прямого отображения, поскольку, хотя элемент кэш-памяти и можно вычислить по адресу основной памяти, требуется проверить п элементов кэш-памяти, чтобы узнать, есть ли там нужная нам строка. Тем не менее практика показывает, что 2- или 4-входовая ассоциативная кэш-память дает хороший результат, поэтому внедрение этих дополнительных схем вполне оправданно.
достоверности достоверности достоверности достоверности
Использование ассоциативной кэш-памяти с множественным доступом ставит разработчика перед выбором. Если нужно поместить новый элемент в кэшпамять, какой именно из старых элементов удалить? Для большинства задач хорошо подходит алгоритм обработки элемента, который дольше всего не использовался (Least Recenly Used, LRU). Имеется определенный порядок каждого набора ячеек, доступных из данной ячейки памяти. Всякий раз, когда осуществляется доступ к любой строке, в соответствии с алгоритмом LRU список обновляется, и маркируется элемент, к которому произведено последнее обращение. Когда требуется заменить какой-нибудь элемент, удаляется тот, который находится в конце списка, то есть тот, который использовался раньше других.
Возможен также предельный случай — 2048-входовая ассоциативная кэш-па- мять, содержащая единственный набор из 2048 элементов. В данном случае все адреса памяти оказываются в этом наборе, поэтому при поиске требуется сравнивать нужный адрес со всеми 2048 тегами в кэш-памяти. Отметим, что для этого каждый элемент кэш-памяти должен содержать специальную логическую схему. Поскольку поле строки в данном случае нулевое, поле тега — это весь адрес за исключением полей слова и байта. Более того, когда строка кэша заменяется, возможными кандидатами на смену являются все 2048 элементов. Для хранения упорядоченного списка потребовался бы громоздкий учет использования системных ресурсов, поэтому применение алгоритма LRU оказывается невозможным. (Вспомните, что список требуется обновлять при каждой операции с памятью.) Интересно, что кэш-память с большим числом входов далеко не всегда превосходит по производительности кэш-память, в которой число входов невелико, а в некоторых случаях работает даже хуже. Поэтому число входов больше четырех встречается редко.
Наконец, особой проблемой для кэш-памяти является запись. Когда процессор записывает слово, а это слово есть в кэш-памяти, он, очевидно, должен либо обновить слово, либо удалить данный элемент кэш-памяти. Практически во всех разработках имеет место обновление кэш-памяти. А что же можно сказать об обновлении копии в основной памяти? Эту операцию можно отложить на потом до того момента, когда строка кэша будет готова к замене в соответствии с алгоритмом LRU. Выбор труден и ни одно из решений не является предпочтительным. Немедленное обновление элемента основной памяти называется сквозной записью. Этот подход обычно гораздо проще реализуется, и к тому же он более надежен, поскольку современная память при ошибке всегда может восстановить свое предыдущее состояние. К сожалению, при этом требуется передавать больше данных в память, поэтому в сложных проектах стремятся использовать альтернативный подход — обратную, или отложенную, запись.
С процессом записи связана еще одна проблема: что происходит, если нужно записать что-либо в ячейку, которой нет в кэш-памяти? Должны ли данные передаваться в кэш или просто записываться в основную память? И снова ни один из ответов не является во всех отношениях лучшим. В большинстве разработок, в которых применяется обратная запись, данные передаются в кэш-память. Эта технология называется заполнением по записи (write allocation). С другой стороны, в тех разработках, где применяется сквозная запись, элемент в кэш-память при записи обычно не помещается, поскольку это усложняет систему. Заполнение по записи полезно только в том случае, если имеют место повторные записи в одно и то же слово или в разные слова в пределах одной строки кэша.
Эффективность кэширования является крайне важным условием повышения общей производительности системы в силу огромного разрыва между быстродействием процессора и памяти. Дискуссия об альтернативных стратегиях кэширования ведется постоянно [6, 96, 145, 149, 197].
Прогнозирование ветвлений
Современные компьютеры в значительной степени конвейеризированы. Конвейер, изображенный на рис. 4.24, имеет семь ступеней; более сложно организованные компьютеры содержат конвейеры с десятью и более ступенями. Конвейеризация лучше работает с линейным кодом, поэтому блок выборки команд может просто считывать последовательные слова из памяти и отправлять их в блок декодирования заранее, еще до того, как они понадобятся.
Единственная проблема состоит в том, что эта модель совершенно нереалистична. Программы вовсе не являются линейными кодами. В них полно команд переходов. Рассмотрим простые команды листинга 4.4. Переменная i сравнивается с 0 (вероятно, на практике это самое распространенное сравнение). В зависимости от результата другой переменной, к, присваивается одно из двух возможных значений.
Листинг 4.4. Фрагмент программы
if(i—0)
к=1;
else
Возможный вариант трансляции на язык ассемблера показан в листинге 4.5. Язык ассемблера мы будем рассматривать позже в этой книге, а сейчас достаточно знать, что программа, более или менее похожая на программу из листинга 4.5, вполне возможна. Первая команда сравнивает переменную i с нулем. Вторая совершает переход к предложению Else, если i не равно 0. Третья команда присваивает значение 1 переменной к. Четвертая команда выполняет переход к следующему оператору программы. Компилятор поместил там метку Next. Пятая команда присваивает значение 2 переменной к.
Листинг 4.5. Программа из листинга 4.4 после трансляции на язык ассемблера
|
CMP i, 0 |
: сравнение i с 0 |
|
BNE Else |
: переход к Else, если они не равны |
Then: |
MOV k, 1 |
: присваивание значения 1 переменной к |
|
BR Next |
: безусловный переход к Next |
Else: Next: |
MOV k, 2 |
; присваивание значения 2 переменной к |
Мы видим, что две из пяти команд являются командами перехода. Более того, одна из них, BNE, — это команда условного перехода (переход, который осуществляется тогда и только тогда, когда выполняется определенное условие, в данном случае — это равенство двух операндов предыдущей команды СМР). Самый длинный линейный код состоит здесь из двух команд, поэтому очень трудно организовать высокоскоростной конвейер.
На первый взгляд может показаться, что безусловные переходы, например, команда BR Next в листинге 4.5, не влекут за собой никаких проблем. Вообще говоря, в данном случае нет никакой двусмысленности в том, куда дальше идти. Почему же блок выборки команд не может просто продолжать считывать команды с целевого адреса (то есть с того места, куда осуществляется переход)?
Сложность объясняется самой природой конвейера. На рис. 4.24, например, мы видим, что декодирование происходит на второй ступени. Следовательно, блоку выборки команд приходится решать, откуда вызывать следующую команду еще до того, как он узнает, команду какого типа он только что вызвал. Только в очередном цикле он сможет выяснить, что получил команду безусловного перехода, хотя еще до этого он вызывает команду, следующую за командой безусловного перехода. То есть в значительной части конвейеризированных машин (например, UltraSPARC III) сначала выполняется команда, следующая после команды безусловного перехода, хотя по логике вещей так быть не должно. Позиция после перехода называется слотом отсрочки (delay slot). Pentium II (а также машина, используемая в листинге 4.5) не поддерживают слот отсрочки, а обойти эту проблему путем внутреннего усложнения часто сложно. Оптимизирующий компилятор постарается найти какую-нибудь полезную команду, чтобы поместить ее в слот отсрочки, но часто ничего подходящего нет, поэтому компилятор вынужден вставлять туда команду N0P. Хотя программа остается корректной, объем ее растет и работает она медленнее.
С условными переходами дело обстоит еще хуже. Во-первых, они тоже содержат слоты отсрочки, во-вторых, блок выборки команд узнает, откуда нужно считывать команду, гораздо позже. Первые конвейеризированные машины просто простаивали, пока выяснялось, нужно совершать переход или нет. Простой по три или четыре цикла при каждом условном переходе, особенно если 20% команд являются командами условного перехода, значительно снижает производительность.
Поэтому большинство машин прогнозируют, будет выполнен встретившийся условный переход или нет. Для этого, например, можно предполагать, что все условные переходы назад будут выполняться, а все условные переходы вперед нет. Что касается первой части предположения, то команды перехода назад обычно помещаются в конце циклов, а большинство циклов выполняются многократно, поэтому предположение о переходе к началу цикла чаще всего будет правильным.
Со второй частью предположения дело обстоит сложнее. Некоторые переходы вперед осуществляются в случае обнаружения ошибки в программе (например, невозможность открытия файла). Ошибки случаются редко, поэтому в большинстве случаев подобные переходы не происходят. Естественно, существует множество переходов вперед, никак не связанных с ошибками, поэтому процент успеха здесь не так высок, как в случае перехода назад. Однако это все же лучше, чем ничего.
Если переход спрогнозирован правильно, то ничего особенного делать не нужно. Просто продолжается выполнение программы. Проблема возникает тогда, когда переход спрогнозирован неправильно. Вычислить, куда нужно перейти, и перейти именно туда несложно. Самое сложное — отменить уже выполненные команды, которые не нужно было выполнять.
Существует два способа отмены команд. Первый способ — продолжать выполнять команды, вызванные после спрогнозированного условного перехода до тех пор, пока одна из этих команд не попытается изменить состояние машины (например, сохранить значение в регистре). Тогда вместо того, чтобы перезаписывать регистр, нужно поместить вычисленное значение во временный (скрытый) регистр, а затем, когда выяснится, что прогноз был правильным, просто скопировать это значение в обычный регистр. Второй способ — сохранять (например, в скрытом временном регистре) значение любого регистра, который может быть переписан. В результате машина сможет вернуться в предыдущее состояние в случае неправильно спрогнозированного перехода. Реализация обоих подходов очень сложна и требует громоздкой системы учета использования системных ресурсов. А если встретится второй условный переход еще до того, как станет известно, был ли правильно спрогнозирован первый условный переход, ситуация может совершенно запутаться.
Динамическое прогнозирование ветвлений
в
зана на рис. 4.28, а. В данном случае таблица содержит по одной ячейке для каждой команды условного перехода.
В ячейке находится адрес команды перехода, а также бит, который указывает, произошел ли переход, когда эта команда встретилась в последний раз. Прогноз заключается в выборе того же пути, по которому программа пошла в предыдущий раз при выполнении команды перехода. Если прогноз оказывается неправильным, бит в таблице меняется.
Существует несколько вариантов организации данной таблицы. Фактически они полностью аналогичны вариантам организации кэш-памяти. Рассмотрим машину с 32-разрядными командами, которые расположены таким образом, что два младших бита каждого адреса памяти равны 00. Таблица содержит 2п ячеек (строк). Из команды перехода можно извлечь п + 2 младших бита и осуществить сдвиг вправо на два бита. Это гг-разрядное число можно использовать в качестве индекса в таблице, проверяя, совпадает ли адрес, сохраненный там, с адресом перехода. Как и в случае с кэш-памятью, здесь нет необходимости сохранять п + 2 младших бита, поэтому их можно опустить (то есть сохраняются только старшие адресные биты — тег). Если адреса совпали, бит прогнозирования используется для прогнозирования перехода. Если тег неправильный или элемент недействителен, значит, имеет место промах. В этом случае можно применять правило перехода вперед/назад.
Если таблица динамики переходов содержит, скажем, 4096 элементов, то адреса 0, 16384, 32768 и т. д. будут конфликтовать; аналогичная проблема встречается и при работе с кэш-памятью. Здесь возможно такое же решение: 2-альтерна- тивный, 4-альтернативный или w-альтернативный ассоциативный элемент. Как и в случае кэш-памяти, предельный случай — один //-альтернативный ассоциативный элемент.
При достаточно большом размере таблицы и достаточной степени ассоциативности эта схема хорошо подходит для большинства ситуаций. Тем не менее систематически приходится сталкиваться с проблемой. При выходе из цикла переход предсказывается неправильно, и, что еще хуже, этот неправильный прогноз изменяет бит в таблице, который после этого будет показывать, что переход совершать не требуется. То есть в следующий раз, когда опять потребуется выполнять цикл, переход в конце первого прохода цикла окажется спрогнозированным неправильно. Если цикл находится внутри другого цикла или внутри часто вызываемой процедуры, эта ошибка будет повторяться достаточно часто.
Для решения проблемы можно немного изменить метод прогнозирования, чтобы прогноз менялся не после одного, а только после двух последовательных неправильных прогнозов. Такой подход требует наличия в таблице двух битов прогнозирования переходов (рис. 4.28, б): один должен указывать, предполагается совершать переход или нет, а второй — был сделан переход в прошлый раз или нет.
Этот алгоритм можно представить в виде конечного автомата с четырьмя состояниями (рис. 4.29). После ряда последовательных успешных прогнозов отсутствия перехода конечный автомат будет находиться в состоянии 00 и в следующий раз также покажет, что перехода нет.
Если этот прогноз окажется неправильным, автомат перейдет в состояние 01, но в следующий раз он все равно покажет отсутствие перехода. Только в том случае, если этот последний прогноз тоже окажется ошибочным, конечный автомат перейдет в состояние И, прогнозируя наличие перехода. Фактически левый бит — это прогноз, а правый бит — то, что случилось в прошлый раз (то есть был ли переход). В данной разработке используются только 2 бита, но возможно применение и 4, и 8 бит.
Это не первый конечный автомат, который мы рассматриваем. На рис. 4.19 тоже изображен конечный автомат. На самом деле все наши микропрограммы можно считать конечными автоматами, поскольку каждая строка представляет особое состояние, в котором может находиться автомат, с четко определенными переходами к конечному набору других состояний. Конечные автоматы очень широко используются при разработке аппаратного обеспечения.
До сих пор мы предполагали, что цель каждого условного перехода известна. Обычно либо в явном виде давался адрес, к которому нужно перейти (он содержался непосредственно в команде), либо было известно смещение относительно текущей команды (то есть число со знаком, которое нужно было прибавить к счетчику команд). Часто это предположение имеет силу, но некоторые команды условного перехода вычисляют целевой адрес, предварительно выполняя определенные арифметические действия над значениями регистров. Даже если взять конечный автомат, изображенный на рис. 4.29, который точно прогнозирует переходы, прогноз окажется невостребованным, поскольку неизвестен целевой адрес. Один из возможных выходов из подобной ситуации — сохранить в таблице адрес, к которому был осуществлен переход в прошлый раз, как показано на рис. 4.28, в. Тогда, если в таблице указано, что в прошлый раз, когда встретилась команда перехода по адресу 516, переход был совершен к адресу 4000, целевым снова будет адрес 4000 (в случае, если предсказывается переход).
Еще один подход к прогнозированию перехода — следить, были ли совершены последние k условных переходов независимо от того, какие это были команды. Это ^-разрядное число, которое хранится в сдвиговом регистре динамики переходов, затем сравнивается параллельно со всеми элементами таблицы с ^-разрядным ключом, и в случае совпадения применяется тот прогноз, который найден в этом элементе. Удивительно, но эта технология работает достаточно хорошо.
Статическое прогнозирование ветвлений
Все технологии прогнозирования ветвлений, которые обсуждались до сих пор, являются динамическими, то есть выполняются во время работы программы. Они приспосабливаются к текущему режиму работы программы, и это их положительное качество. Отрицательной стороной этих технологий является то, что они требуют специализированной и дорогостоящей аппаратуры, и микросхемы для реализации этих технологий получаются очень сложными.
Можно пойти другим путем, призвав на помощь компилятор. Обратите внимание на следующий оператор:
for (1=0; i < 1000000; i++) { ... }
Когда компилятор встречает такой оператор, он знает, что переход в конце цикла будет происходить практически всегда. Если бы был способ сообщить это аппаратуре, можно было бы избавиться от огромного объема работы.
Хотя такой подход связан с изменением архитектуры (а не только реализации), в некоторых машинах, например, UltraSPARC III, помимо обычных команд условного перехода (которые нужны для обратной совместимости), имеется еще один набор команд. Новые команды содержат бит, по которому компилятор определяет, совершать переход или не совершать. Когда встречается такой бит, блок выборки команд просто делает то, что ему положено. Более того, нет необходимости тратить драгоценное пространство в таблице динамики переходов для этих команд, что сокращает количество конфликтных ситуаций.
Наконец, наша последняя технология прогнозирования ветвлений основана на профилировании [65]. Это тоже статическая технология, только в данном случае программа не заставляет компилятор вычислять, какие переходы нужно совершать, а какие нет. Программа реально выполняется, а ветвления фиксируются, эта информация поступает в компилятор, который затем использует специальные команды условного перехода для того, чтобы сообщить аппаратуре, что нужно делать.
Исполнение с изменением последовательности и подмена регистров
Большинство современных процессоров являются и конвейеризированными и суперскалярными, как показано на рис. 2.5. Это означает, что имеется блок выборки команд (IFU), который заранее вызывает команды из памяти и передает их в блок декодирования. Блок декодирования, в свою очередь, передает декодированные команды соответствующим функциональным блокам для выполнения. В некоторых случаях блок декодирования может разбивать отдельные команды на микрооперации перед тем, как отправить их функциональным блокам.
Ясно, что проще всего выполнять все команды в том порядке, в котором они вызываются из памяти (предполагается, что прогнозирование ветвлений всегда оказывается верным). Однако из-за взаимозависимости команд такое последовательное выполнение не всегда дает оптимальную производительность. Если команде требуется значение, которое вычисляется предыдущей командой, вторая команда не сможет выполняться, пока первая не выдаст нужную величину. Таким образом, в ситуации реальной взаимозависимости второй команде приходится ждать. Существуют и другие виды взаимозависимостей, но о них мы поговорим позже.
Чтобы обойти эти проблемы и достичь оптимальной производительности, некоторые процессоры пропускают взаимозависимые команды и переходят к следующим (независимым) командам. Естественно, что при этом алгоритм распределения команд должен давать такой же результат, как если бы все команды выполнялись в том порядке, в котором они написаны. А теперь продемонстрируем на конкретном примере, как происходит переупорядочение команд.
Чтобы изложить суть проблемы, начнем с машины, которая запускает команды в том порядке, в котором они расположены в программе, и требует, чтобы выполнение команд также завершалось в порядке, соответствующем программному. Важность второго требования прояснится позднее.
Наша машина содержит 8 доступных программисту регистров, от R0 до R7. Все арифметические команды используют три регистра: два для операндов и один для результата, как и в микроархитектуре Mic-4. Мы предполагаем, что, если команда декодируется в цикле п, выполнение начинается в цикле п + 1. В случае простой команды, например команды сложения или вычитания, запись обратно в выходной регистр происходит в конце цикла п + 2. В случае с более сложной командой, например командой умножения, запись в регистр происходит в конце цикла /2 + 3. Чтобы сделать наш пример реалистичным, мы позволим блоку декодирования выдавать до двух команд за цикл. Некоторые суперскалярные процессоры могут генерировать 4 или даже 6 команд за цикл.
Последовательность выполнения команд иллюстрирует табл. 4.11. В первом столбце приводится номер цикла, во втором — номер команды, в третьем — сама команда. В четвертом столбце показаны выданные команды (максимум две команды за цикл). Цифры в пятом столбце сообщают, какие команды завершены. Помните, что в нашем примере мы требуем, чтобы команды и запускались, и завершались в строго определенном порядке, поэтому выдача команды k + 1 может произойти только после выдачи команды k, а результат команды k + 1 не может быть записан в выходной регистр до того, как завершится команда k. Оставшиеся 16 столбцов мы обсудим позже.
После декодирования команды блок декодирования должен определить, запускать команду сразу или нет. Для этого блок декодирования должен знать состояние всех регистров. Если, например, текущей команде требуется регистр, значение которого еще не подсчитано, текущая команда не выдается, и центральный процессор вынужден простаивать.
Следить за состоянием регистров призвано специальное устройство — счетчик обращений (scoreboard), впервые появившийся в системе CDC 6600. Для каждого регистра счетчик обращений содержит небольшую схему, которая подсчитывает, сколько раз этот регистр используется выполняющимися командами в качестве источника. Если одновременно может выполняться максимум 15 команд, будет достаточно 4-разрядного счетчика. Когда запускается команда, элементы счетчика обращений, соответствующие регистрам операндов, увеличиваются на 1. Когда выполнение команды завершено, соответствующие элементы счетчика уменьшаются на 1.
Счетчик обращений содержит аналогичные схемы подсчета для целевых регистров. Поскольку допускается только одна запись за раз, эти схемы могут быть размером в один бит. Правые 16 столбцов в табл. 4.11 демонстрируют показания счетчика обращений.
В реальных машинах счетчик обращений также следит за использованием функционального блока, чтобы избежать выдачи команды, для которой нет доступного функционального блока. Для простоты мы предполагаем, что подходящий функциональный блок всегда есть в наличии, поэтому функциональные блоки в таблице не показаны.
В первой строке табл. 4.11 представлена команда 1, которая перемножает значения регистров R0 и R1 и помещает результат в регистр R3. Поскольку ни один из этих регистров еще не используется, команда запускается, а счетчик обращений показывает, что регистры R0 и R1 считываются, а регистр R3 записывается.
Ни одна из последующих команд не может записывать результат в эти регистры и не может считывать регистр R3 до тех пор, пока не завершится выполнение команды 1. Поскольку это команда умножения, она закончится в конце цикла 4. Значения счетчика обращений, приведенные в каждой строке, отражают состояние регистров после запуска команды, записанной в этой же строке. Пустые клетки соответствуют значению 0.
Поскольку в качестве примера рассматривается суперскалярная машина, которая может запускать две команды за цикл, вторая команда выдается также во время цикла 1. Она складывает значения регистров R0 и R2, а результат сохраняет в регистре R4. Чтобы определить, можно ли запускать эту команду, применяются следующие правила:
-
Если какой-нибудь операнд записывается, запускать команду нельзя (RAW- взаимозависимость).
-
Если считывается регистр результатов, запускать команду нельзя (WAR- взаимозависимость).
-
Если записывается регистр результатов, запускать команду нельзя (WAW- взаимозависимость).
Мы уже рассматривали реальные взаимозависимости (RAW-взаимозависи- мости), имеющие место, когда команде в качестве источника нужно использовать результат предыдущей команды, которая еще не завершилась. Два других типа взаимозависимостей менее серьезные. По существу, они связаны с конфликтами ресурсов. При WAR-взаимозависимости (Write After Read — запись после чтения) одна команда пытается перезаписать регистр, который предыдущая команда еще не закончила считывать. W AW-взаимозависимость (Write After Write — запись после записи) похожа на WAR-взаимозависимость. Подобной взаимозависимости можно избежать, если вторая команда будет помещать результат где-либо в другом месте еще (возможно, временно). Если ни одна из трех упомянутых ситуаций не возникает и нужный функциональный блок доступен, команду можно выдать. В этом случае команда 2 использует регистр R0, который в данный момент считывается незаконченной командой, но подобное перекрытие допустимо, поэтому команда 2 может запускаться. Сходным образом команда 3 запускается во время цикла 2.