Хакеры сновидений: Архив 1-6 - Lokky
Шрифт:
Интервал:
Закладка:
-----------------------------------------------------------------------------------------------------------------------------------------------------------
“Конвейерность и суперскалярность“.
Для повышения скорости работы процессора каждый этап выполняется специализированным блоком процессора. Чтобы уменьшить число простоев, следующую команду загружают в процессор и начинают выполнять не дожидаясь окончания выполнения предыдущих - это и есть конвейеризация.
Конвейеризация вычислений – это когда каждая последующая команда начинает выполняться сразу же после прохождения первой ступени конвейера предыдущей командой. Под конвейером в данном случае понимается такой метод внутренней обработки команд, когда исполнение команды разбивается на несколько ступеней (Stages) и каждой ступени соответствует свой модуль в структуре CPU. По очередному тактовому импульсу каждая команда в конвейере продвигается на следующую ступень, при этом выполненная команда покидает конвейер, а новая поступает в него.
При этом каждая команда может выполняться довольно долго, но за счет одновременного выполнения нескольких команд (находящихся на разных стадиях обработки их процессором) общая скорость работы процессора повышается по сравнению с последовательным выполнением команд во столько раз, сколько команд умещается в конвейере; это число называется “глубиной конвейеризации” или “длиной конвейера“. В результате мелкого разбиения выполнения команды на подэтапы удается повысить тактовую частоту процессора (т.е. частоту, с которой процессор “заглатывает“ на выполнение очередную команду).
Конвейерную обработку можно сравнить с работой грузчиков, стоящих в “цепочке“ и передающих из рук в руки упаковки с продуктами. В этом случае процесс погрузки (разгрузки) существенно ускоряется по сравнению с тем, когда каждый грузчик бегает с отдельной упаковкой к месту их складирования. Процессоры, имеющие несколько конвейеров, называются суперконвейерными, а имеющие несколько ступеней - суперскалярными. В простейшем случае можно разложить выполнение каждой команды на несколько этапов:
1. Считывание команды;
2. Распознавание команды;
3. Подготовка аргументов;
4. Выполнение операции, заложенной в команде;
5. Запись результатов.
Каждая из этих операций тоже может быть разложена на несколько: так считывание команды состоит из отправки запроса в подсистему памяти и получения ответа. Подготовка аргументов, если аргументом является ячейка памяти, может потребовать дополнительного считывания данных.
Подобная система хороша лишь при отсутствии нарушений последовательного выполнения кода - операторов перехода и аппаратных прерываний. Попытка организовать конвейерную обработку команд натыкается на конфликт разных команд, находящихся в конвейере, при доступе к одинаковым устройствам (в полной мере это проявляется в суперскалярной архитектуре).
Если какая-то команда хочет обратиться в оперативную память, то она конфликтует за доступ к шинам адреса и данных со считыванием очередной команды; это устранено в системах с раздельными кэшами для команд и для данных.
Если команда хочет использовать данные, которые еще не вычислены (например, использует в качестве аргумента регистр, чье значение изменяется предшествующей командой, а эта команда еще не записала результаты), то конвейер должен быть приостановлен до разрешения этого конфликта. Если команда изменяет содержимое памяти, в которой содержатся команды, уже поступившие в конвейер, то процессор имеет полное право проигнорировать это и продолжать выполнение содержащихся в конвейере команд.
Однако, пожалуй, наиболее серьёзным камнем преткновения являются условные ветвления, ведь процессор не может знать, будет переход выполнен или нет, до тех пор, пока команда не пройдет исполнительную ступень конвейера. Для ускорения определения направления условного перехода применяется так называемое предсказание ветвлений. Одни участки кода выполняются чаще чем другие (собственно, на этом и построена идея кэширования); то же самое относится к участкам, определяемым ветвлениями. Чаще всего ветвление служит либо для проверки исключительной ситуации, либо для организации цикла; в любом случае, зная, с какой частотой до этого совершался переход, можно оценить вероятность перехода в этот раз и направить конвейер по наиболее вероятному направлению. Процессор определяет наиболее вероятное направление перехода либо статически (по заранее заданным правилам), либо динамически — по собираемой им статистике выполнения этого ветвления за прошедшее время (так как часто переход выполняется более одного раза). А в некоторых моделях (чаще всего с программным распараллеливанием) и программисту оставлена возможность “намекнуть“, куда переход более вероятен! Декларируемая вероятность правильного определения переходов в современных процессорах — 90-95%. Предсказанная ветвь сразу направляется на конвейер для выполнения — это и есть упреждающее выполнение переходов – и если предсказание “сбылось“, то переход выполняется за один такт. Но если процессор ошибся, то приходится очищать весь длиннющий конвейер и заполнять его уже командами из “правильной“ ветви. Здесь и теряется драгоценное время. Ограниченность этого метода очевидна - если бы можно было гарантированно предсказать результат ветвления, то его можно было бы заменить безусловным переходом. Один из напрашивающихся путей решения проблемы – посылать вторую ветвь условного перехода на дополнительный конвейер параллельно с первой и после проверки условия ветвления дальше идти лишь в правильном направлении, отменив выполнение другой ветви.
В случае дополнительного конвейера, при обнаружении ветвления основной конвейер продолжает выполнение основного потока, как будто уверен, что перехода не произойдет, а дополнительный конвейер выполняет команды начиная с той, куда должен произойти переход (естественно, ни один не записывает результаты вычислений); по вычислении условия и выполнении оператора ветвления становится окончательно ясно, какой из конвейеров работал впустую. Ну, а если и в ветвях есть свои условные ветвления? Нелегкая задачка... Поэтому, применимость этого метода ограничивается возможностью появления в одном из этих конвейеров (а то и в обоих) новых операций ветвления. Можно, конечно, заиметь несколько конвейеров или вести очередь ветвлений - в любом случае сложность диспетчеризации конвейеров экспоненциально растет с глубиной конвейера.
Суперскалярная архитектура подразумевает одновременное выполнение нескольких команд в параллельно работающих исполнительных устройствах. Например, происходит параллельная работа арифметико-логических блоков, проверка условий ветвления для команд условного перехода. При этом исполнительное ядро работает с повышенной скоростью выполнения операций (Intel называет это RapidExecuteEngineр – “Устройство Быстрого Исполнения”). Проблемы появляются, когда, например, одна команда должна получить на вход результат выполнения другой.
Последовательность команд, составляющих программу, в большинстве случаев соответствует принципу фон Неймана - один поток команд, каждая из которых работает со своими данными. Однако нередко последовательность выполнения команд можно изменить, не оказав влияния на результат выполнения программы:
A=B*C
D=E+F
G=H-I
Эти три команды можно выполнять в любом порядке, потому что они независимы по данным - ни одна из них не использует переменных, изменяемых другими командами. Эти три команды, поступив в процессор, могут выполняться независимо, причем на разных устройствах: умножителе, сумматоре и вычитателе соответственно (здесь специально подобраны команды, которые не используют одинаковые операции - это упрощает рассмотрение). Но если они выполняются независимо, ничто не мешает командам, поступившим позднее, закончиться раньше - это и есть суперскалярность, точнее, одно из ее проявлений.
Суперскалярность может быть как явной для программиста/компилятора, так и прозрачной для них. В первом случае процессор выполняет по несколько команд за такт, игнорируя возможные влияния выполнения предшествующей, по ходу выполнения, команды на аргументы последующей; программист, пишущий в кодах процессора, или автор компилятора обязаны учесть это. Во втором случае процессор учитывает это влияние и блокирует выполнение команд, чьи аргументы еще не вычислены, а игнорирование суперскалярности процессора программистом или автором компилятора приведет не к неправильному выполнению программы, а лишь к потере эффективности.
Пример:
A=B+1
C++ (увеличить C на единицу)