В архитектуре компьютера слот задержки — это слот инструкции, выполняемый без эффектов предыдущей инструкции. [1] Наиболее распространенной формой является одна произвольная инструкция, расположенная сразу после инструкции ветвления в архитектуре RISC или DSP ; эта инструкция будет выполнена, даже если будет выполнена предыдущая ветвь. Это заставляет инструкцию выполняться не в том порядке, в котором она находится в исходном коде ассемблера .
Современные конструкции процессоров обычно не используют слоты задержки, а вместо этого выполняют все более сложные формы предсказания ветвлений . В этих системах ЦП немедленно переходит к тому, что, по его мнению, будет правильной стороной ветви, и тем самым устраняет необходимость указания в коде какой-либо несвязанной инструкции, которая не всегда может быть очевидна во время компиляции. Если предположение неверно и необходимо вызвать другую сторону ветви, это может привести к длительной задержке. Это происходит достаточно редко, поэтому ускорение избегания слота задержки легко компенсируется меньшим количеством неправильных решений.
Центральный процессор обычно выполняет инструкции из машинного кода , используя четырехэтапный процесс; инструкция сначала считывается из памяти, затем декодируется, чтобы понять, что нужно выполнить, затем эти действия выполняются, и, наконец, любые результаты записываются обратно в память. В ранних разработках каждый из этих этапов выполнялся последовательно, так что для выполнения инструкций требовалось некоторое кратное машинному такту время . Например, в Zilog Z80 минимальное количество тактов, необходимое для завершения инструкции, составляло четыре, но могло достигать 23 тактов для некоторых (редких) инструкций. [2]
На любом этапе обработки инструкции задействована только одна часть чипа. Например, на этапе выполнения обычно активен только арифметико-логический блок (АЛУ), в то время как другие блоки, например, те, которые взаимодействуют с основной памятью или декодируют инструкцию, простаивают. Одним из способов повышения общей производительности компьютера является использование конвейера инструкций . Это добавляет некоторые дополнительные схемы для хранения промежуточных состояний инструкции по мере ее прохождения через блоки. Хотя это не улучшает синхронизацию цикла какой-либо отдельной инструкции, идея состоит в том, чтобы позволить второй инструкции использовать другие подблоки ЦП, когда предыдущая инструкция перешла на следующий этап. [3]
Например, пока одна инструкция использует АЛУ, следующая инструкция из программы может находиться в декодере, а третья может быть извлечена из памяти. В этой компоновке типа сборочной линии общее количество инструкций, обрабатываемых в любой момент времени, может быть увеличено на количество этапов конвейера. В Z80, например, четырехэтапный конвейер мог бы улучшить общую пропускную способность в четыре раза. Однако из-за сложности синхронизации инструкций это было бы нелегко реализовать. Гораздо более простая архитектура набора инструкций (ISA) MOS 6502 позволила включить двухэтапный конвейер, что дало ему производительность, которая была примерно вдвое выше, чем у Z80 при любой заданной тактовой частоте. [4]
Основная проблема с реализацией конвейеров в ранних системах заключалась в том, что инструкции имели сильно различающиеся количества циклов. Например, инструкция по сложению двух значений часто предлагалась в нескольких версиях или кодах операций , которые различались в зависимости от того, где они считывали данные. Одна версия add
могла брать значение, найденное в одном регистре процессора , и добавлять его к значению в другом, другая версия могла добавлять значение, найденное в памяти, в регистр, в то время как другая могла добавлять значение из одной ячейки памяти в другую ячейку памяти. Каждая из этих инструкций занимает разное количество байтов для представления в памяти, то есть они требуют разного количества времени для извлечения, могут потребовать нескольких проходов через интерфейс памяти для сбора значений и т. д. Это значительно усложняет логику конвейера. Одной из целей концепции дизайна микросхемы RISC было удаление этих вариантов, чтобы упростить логику конвейера, что приводит к классическому конвейеру RISC , который выполняет одну инструкцию за каждый цикл.
Однако в конвейерных системах возникает одна проблема, которая может замедлить производительность. Это происходит, когда следующая инструкция может измениться в зависимости от результатов предыдущей. В большинстве систем это происходит, когда происходит переход . Например, рассмотрим следующий псевдокод:
вершина: считать число из памяти и сохранить его в регистре считать другое число и сохранить его в другом регистре сложить два числа в третий регистр записать результат в память считать число из памяти и сохранить его в другом регистре ...
В этом случае программа линейна и может быть легко конвейеризирована. Как только первая read
инструкция будет прочитана и декодируется, вторая read
инструкция может быть прочитана из памяти. Когда первая переходит к выполнению, считывается add
из памяти, пока вторая read
декодируется, и так далее. Хотя для завершения первой все еще требуется то же количество циклов read
, к моменту ее завершения значение из второй будет готово, и ЦП может немедленно добавить их. В неконвейерном процессоре выполнение первых четырех инструкций займет 16 циклов, в конвейерном — всего пять.
Теперь рассмотрим, что происходит при добавлении ветви:
вершина: считать число из памяти и сохранить его в регистре считать другое число и сохранить его в другом регистре сложить два числа в третий регистр если результат в 3-м регистре больше 1000, то вернуться наверх: (если это не так) запишите результат в память считать число из памяти и сохранить его в другом регистре ...
В этом примере результат сравнения в строке четыре приведет к изменению «следующей инструкции»; иногда это будет следующая инструкция write
в память, а иногда это будет read
из памяти наверху. Конвейер процессора обычно уже прочитал следующую инструкцию, write
, к тому времени, когда АЛУ вычислит, какой путь он выберет. Это известно как опасность ветвления . Если ему нужно вернуться наверх, write
инструкция должна быть отброшена, а read
вместо этого считана инструкция из памяти. Это занимает как минимум один полный цикл инструкции и приводит к тому, что конвейер остается пустым в течение как минимум одного времени инструкции. Это известно как «задержка конвейера» или «пузырь» и, в зависимости от количества ветвей в коде, может оказать заметное влияние на общую производительность.
Одной из стратегий решения этой проблемы является использование слота задержки , который относится к слоту инструкции после любой инструкции, требующей больше времени для завершения. В приведенных выше примерах инструкция, требующая больше времени, — это ветвь, которая, безусловно, является наиболее распространенным типом слота задержки, и их чаще называют слотом задержки ветви .
В ранних реализациях инструкция, следующая за ветвью, заполнялась бы без-операцией или NOP
, просто для заполнения конвейера, чтобы гарантировать правильное время, так что к моменту NOP
загрузки из памяти ветвь была бы завершена, и счетчик программ мог бы быть обновлен правильным значением. Это простое решение тратит впустую доступное время обработки. Более продвинутые решения вместо этого пытались бы идентифицировать другую инструкцию, обычно расположенную поблизости в коде, чтобы поместить ее в слот задержки, чтобы была выполнена полезная работа.
В приведенных выше примерах read
инструкция в конце полностью независима, она не зависит от какой-либо другой информации и может быть выполнена в любое время. Это делает ее пригодной для размещения в слоте задержки перехода. Обычно это автоматически обрабатывается программой ассемблера или компилятором , который переупорядочивает инструкции:
считать число из памяти и сохранить его в регистре считать другое число и сохранить его в другом регистре сложить два числа в третий регистр если результат в 3-м регистре больше 1000, то вернуться наверх считать число из памяти и сохранить его в другом регистре (если это не так) запишите результат в память ...
Теперь, когда ветвь выполняется, она идет вперед и выполняет следующую инструкцию. К тому времени, как эта инструкция считывается в процессор и начинает декодироваться, результат сравнения готов, и процессор теперь может решить, какую инструкцию считывать следующей, сверху read
или write
снизу. Это предотвращает потерю времени и поддерживает конвейер всегда заполненным.
Найти инструкцию для заполнения слота может быть сложно. Компиляторы обычно имеют ограниченное «окно» для проверки и могут не найти подходящую инструкцию в этом диапазоне кода. Более того, инструкция не может полагаться ни на какие данные в пределах ветви; если инструкция add
принимает предыдущее вычисление в качестве одного из своих входных данных, этот входной сигнал не может быть частью кода в ветви, которая может быть принята. Определить, является ли это правдой, может быть очень сложно при наличии переименования регистров , при котором процессор может помещать данные в регистры, отличные от тех, которые указаны в коде, без ведома компилятора об этом.
Другим побочным эффектом является то, что требуется специальная обработка при управлении точками останова на инструкциях, а также при пошаговом выполнении во время отладки в слоте задержки ветвления. Прерывание не может произойти во время слота задержки ветвления и откладывается до окончания слота задержки ветвления. [5] [6] Размещение инструкции ветвления в слоте задержки ветвления запрещено или устарело. [7] [8] [9]
Идеальное количество слотов задержки ветвления в конкретной реализации конвейера определяется количеством стадий конвейера, наличием пересылки регистров , на какой стадии конвейера вычисляются условия ветвления, используется ли целевой буфер ветвления (BTB) и многими другими факторами. Требования к совместимости программного обеспечения диктуют, что архитектура не может изменять количество слотов задержки от одного поколения к другому. Это неизбежно требует, чтобы более новые аппаратные реализации содержали дополнительное оборудование для обеспечения соблюдения архитектурного поведения, несмотря на то, что оно больше не актуально.
Слоты задержки ветвления встречаются в основном в архитектурах DSP и старых архитектурах RISC . MIPS , PA-RISC (можно указать задержанный или незадержанный переход), [10] ETRAX CRIS , SuperH (инструкции безусловного ветвления имеют один слот задержки), [11] Am29000 , [12] Intel i860 (инструкции безусловного ветвления имеют один слот задержки), [13] MC88000 (можно указать задержанный или незадержанный переход), [14] и SPARC — это архитектуры RISC, каждая из которых имеет один слот задержки ветвления; PowerPC , ARM , Alpha , V850 и RISC-V не имеют ни одного. Архитектуры DSP , каждая из которых имеет один слот задержки ветвления, включают μPD77230 [15] и VS DSP. SHARC DSP и MIPS-X используют двойной слот задержки ветвления; [16] такой процессор выполнит пару инструкций, следующих за инструкцией перехода, прежде чем переход вступит в силу. И TMS320C3x [17] , и TMS320C4x [8] используют слот задержки тройного перехода. TMS320C4x имеет как незадержанные, так и задержанные переходы. [8]
Следующий пример показывает отложенные переходы на языке ассемблера для SHARC DSP, включая пару после инструкции RTS. Регистры R0 по R9 очищаются до нуля в порядке номеров (регистр, очищаемый после R6, — это R7, а не R9). Ни одна инструкция не выполняется более одного раза.
Р0 = 0; CALL fn (DB); /* вызов функции, ниже на метке "fn" */ R1 = 0; /* первый слот задержки */ R2 = 0; /* второй слот задержки */ /***** разрыв здесь (ВСТУПЛЕНИЕ вступает в силу) *****/ R6 = 0; /* CALL/RTS возвращается сюда, а не в "R1 = 0" */ конец прыжка (DB); R7 = 0; /* первый слот задержки */ R8 = 0; /* второй слот задержки */ /***** разрыв здесь (вступает в силу ПРЫЖОК) *****/ /* следующие 4 инструкции вызываются сверху, как функция "fn" */фн: R3 = 0; RTS (DB); /* вернуться к вызывающему, пройдя слоты задержки вызывающего */ R4 = 0; /* первый слот задержки */ R5 = 0; /* второй слот задержки */ /***** разрыв здесь (RTS вступает в силу) *****/конец: R9 = 0;
Слот задержки загрузки — это инструкция, которая выполняется сразу после загрузки (регистра из памяти), но не видит и не нуждается в ожидании результата загрузки. Слоты задержки загрузки встречаются очень редко, поскольку задержки загрузки крайне непредсказуемы на современном оборудовании. Загрузка может быть удовлетворена из ОЗУ или из кэша и может быть замедлена из-за конкуренции за ресурсы. Задержки загрузки наблюдались в очень ранних конструкциях процессоров RISC. MIPS I ISA (реализованная в микропроцессорах R2000 и R3000 ) страдает от этой проблемы.
Следующий пример представляет собой ассемблерный код MIPS I, демонстрирующий как слот задержки загрузки, так и слот задержки перехода.
lw v0 , 4 ( v1 ) # загрузить слово из адреса v1+4 в v0 nop # зря потраченный слот задержки загрузки jr v0 # перейти к адресу, указанному v0 nop # зря потраченный слот задержки перехода