Реклама:

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

Компьютерные программы можно разбить на базовые блоки, каждый из которых представляет собой линейную последовательность команд с точкой входа в начале и точкой выхода в конце. Базовый блок не содержит никаких управляющих структур (например, условных операторов if или операторов цикла while), поэтому при трансляции на машинный язык не создается никаких ветвлений. Базовые блоки связываются операторами управления.

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

Спекулятивное исполнение

Рис. 4.30. Граф базового блока для фрагмента программы, приведенного в листинге 4.6

Листинг 4.6. Фрагмент программы

evesum=0; oddsum=0; 1=0;

while (I<limit) { k=i*i*i;

if(((i/2)*2)-=i)

evensum=evensum+k;

else

oddsum=oddsum+k; i=i+l;

}

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

Посмотрите еще раз на рис. 4.30. Представим, что все переменные, кроме evensum и oddsum, были помещены в регистры. Тогда имело бы смысл переместить команды LOAD в начало цикла до вычисления переменной к, чтобы выполнение этих команд началось раньше, а полученные результаты были доступны в момент, когда они понадобятся. Естественно, при каждой итерации требуется только одно значение, поэтому остальные команды LOAD будут отбрасываться, но если кэш-память и основная память конвейеризированы, то подобная процедура имеет смысл. Выполнение команды раньше того, как станет известно, понадобится эта команда или нет, называется спекулятивным исполнением. Чтобы использовать эту технологию, требуется поддержка компилятора, аппаратного обеспечения, а также некоторое усовершенствование архитектуры. В большинстве случаев переупорядочение команд за пределами одного базового блока находится вне возможностей аппаратного обеспечения, поэтому компилятор должен перемещать команды явным образом.

В связи со спекулятивным исполнением команд возникают некоторые интересные проблемы. Например, очень важно, чтобы ни одна из спекулятивных команд не давала результата, который невозможно отменить, поскольку позднее может оказаться, что эту команду не нужно было выполнять. Обратимся к листингу 4.6 и рис. 4.30. Очень удобно производить сложение, как только появляется значение к (даже до условного оператора if), но при этом нежелательно сохранять результаты в памяти. Чтобы предотвратить перезапись регистров до того, как станет известно, нужны ли полученные результаты, нужно подменить все выходные регистры, которые используются спекулятивной командой. Как вы можете себе представить, счетчик обращений для отслеживания всех этих ситуаций очень сложен, но при наличии соответствующего аппаратного обеспечения его вполне можно создать.

Однако при наличии спекулятивных команд возникает еще одна проблема, которую нельзя решить путем подмены регистров. Что произойдет, если спекулятивная команда вызовет исключение? В качестве примера можно привести команду LOAD, которая вызывает кэш-промах в компьютере со строкой кэша достаточно большого размера (скажем, 256 байт) и памятью, которая работает гораздо медленнее, чем центральный процессор и кэш. Если нам требуется команда LOAD и работа машины останавливается на несколько циклов, пока загружается строка кэша, то это не так страшно, поскольку данное слово действительно нужно. Но если машина простаивает, чтобы вызвать слово, которое, как окажется позднее, нам ни к чему, это совершенно нерационально. Если подобных "оптимизаций" слишком много, то центральный процессор будет работать медленнее, чем если бы "оптимизаций" вообще не было. (Если машина содержит виртуальную память, о которой рассказывается в главе 6, то спекулятивное выполнение команды LOAD может даже вызвать обращение к отсутствующей странице. Подобные ошибки могут значительно повлиять на производительность, поэтому важно их избегать.)

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

Более сложную ситуацию можно проиллюстрировать следующим оператором:

if (х > 0) z = у/х;

Здесь х, у и z - переменные с плавающей точкой. Предположим, что все эти переменные поступают в регистры заранее, а команда деления с плавающей точкой (эта команда выполняется медленно) перемещается вверх по графу и выполняется еще до условного оператора if. К сожалению, если значение х равно 0, то программа завершается в результате попытки деления на 0. Таким образом, спекулятивная команда приводит к сбою в изначально правильной программе. Еще хуже то, что программист изменяет программу, чтобы предотвратить подобную ситуацию, но сбой все равно происходит.

Одно из возможных решений - специальные версии тех команд, которые могут вызывать исключения. Кроме того, к каждому регистру добавляется так называемый бит отравления (poison bit). Если спекулятивная команда дает сбой, она не инициирует перехват исключения, а устанавливает бит отравления в регистр результатов. Если затем этот регистр используется обычной командой, выполняется перехват исключения (как и должно быть в случае исключения). Однако если этот результат не используется, бит отравления сбрасывается и никак не влияет на ход выполнения программы.

Исполнение с изменением последовательности и подмена регистров || Оглавление || Примеры уровня микроархитектуры