Форум программистов, компьютерный форум, киберфорум
Evg
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск Заказать работу  

Базовые сведения о микроархитектуре

Запись от Evg размещена 19.05.2018 в 18:23
Показов 12607 Комментарии 6

Из-за технических ограничений форумного движка статью про hyper-threading пришлось превратить в цикл из четырёх статей. Данная статья является третьей статьёй цикла. Четвёртая статья пока отсутствует





  • 1. Предисловие
  • 2. Зависимости RAW и WAR
  • 3. Борьба с WAR-зависимостями
    3.1. Теоретический пример переписывания кода
    3.2. Реальное устройство аппаратной реализации
  • 4. Масштабируемость процессора
  • 5. Условные переходы
  • 6. Предсказатель условных переходов
  • 7. Векторные инструкции
  • 8. Заключение







1. Предисловие

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

Сразу же заострю внимание на том, что в данной статье речь пойдёт исключительно об одном простом процессорном ядре. Т.е. любое распараллеливание, о котором будет идти речь, является внутренней особенностью ОДНОГО процессорного ядра. Ни о какой многоядерности и ни о каких технологиях наподобие hyper-threading в данной статье речи не идёт. Это довольно важный момент, т.к. речь пойдёт о другом, более низком уровне параллельности, о котором кто-то, возможно, даже и не слышал

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

Буквально пару слов скажу о терминологии. Слово "операция" имеет широкий спектр значений. Это может быть как абстрактный термин "арифметическая операция", так и конкретный термин "машинная операция". Чтобы не путаться, я абстрактные операции буду называть словом "операция", а машинные операции - словом "инструкция"

В статье будет рассмотрен абстрактный процессор со своей собственной системой команд. Для тех, кто уже имел дело с программированием на ассемблере под intel'овскую систему команд, будет немного непривычно видеть трёхаргументный ассемблер

2. Зависимости RAW и WAR

Рассмотрим простейший пример на языке программирования:

C
a = a + 100;
b = b + 200;
На уровне машинных команд соответствующий код будет выглядеть следующим образом:

Code
# Листинг 1. Машинный код
 
(1) load  [a] -> r1       # читаем переменную "a" из памяти на регистр r1
(2) add   r1, 100 -> r1   # складываем значение на регистре r1 и константу 100, результат записываем в регистр r1
(3) store r1 -> [a]       # записываем регистр r1 в память в переменную "a"
(4) load  [b] -> r1
(5) add   r1, 200 -> r1
(6) store r1 -> [b]
Цифрами в круглых скобках я пронумеровал инструкции, чтобы было проще на них ссылаться в тексте. Решётка означает комментарий. В первых трёх строках листинга я привёл краткое описание того, что делает инструкция, чтобы не делать подробных описаний синтаксиса. Итого на исполнение двух последовательных операций сложения нужно 6 машинных инструкций, исполнение которых потребует условных 6 шагов

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

В процессоре есть разные устройства (execution unit), которые предназначены для выполнения различных операций. Для простоты будем считать, что у нас есть три устройства. Первое устройство выполняет чтение данных из памяти на регистр, второе устройство выполняет целочисленное сложение над регистрами и константами, третье устройство выполняет запись данных из регистра в память

Если смотреть на старые процессора, то приведённую выше последовательность машинных команд они исполняли строго по очереди. При этом исполнительные устройства так же работали по очереди. Сначала работало устройство чтения из памяти (считывало значение из переменной "a" на регистр r1), затем работало устройство, которое выполняло сложение (r1 + 100 -> r1), затем работало устройство записи в память (записывало регистр r1 в переменную "a"). Затем всё то же самое выполнялось для переменной "b"

Со временем пришло понимание, что строить работу подобным образом не эффективно. Лучше сделать так, чтобы различные устройства могли работать одновременно (параллельно), обрабатывая независимые данные. Например, в то время, пока одно устройство считывает данные из памяти, второе устройство выполняет арифметические действия над другими данными. Предположим, наш процессор научился это делать. Что нам это даст для приведённого выше фрагмента кода?

Сначала выполняется инструкция "(1) load [a] -> r1". Параллельно с ней нельзя выполнять инструкцию "(2) add r1, 100 -> r1", т.к. она работает над регистром r1, который мы ещё не сформировали. Т.е. инструкция (2) имеет зависимость от инструкции (1), а потому не может исполняться одновременно (параллельно) с инструкцией (1). По той же причине инструкция "(3) store r1 -> [a]" зависит от инструкции (2), которая зависит от инструкции (1). Таким образом мы имеем цепочку зависимых инструкций (1)-(2)-(3), которые можно исполнять только последовательно и никак по другому. В этой цепочке имеются две зависимости: инструкция (2) зависит от инструкции (1), инструкция (3) зависит от инструкции (2). Такой вид зависимости называется "Read After Write" (RAW-зависимость). Т.е. мы не можем прочитать значение в регистре, пока его не записали. Если посмотреть на цепочку инструкций (4)-(5)-(6), то она обладает тем же свойством - это цепочка RAW-зависимых инструкций, которые можно исполнять только последовательно

Если рассуждать логически, то две операции "a = a + 100" и "b = b + 100" проводятся над двумя разными (независимыми) переменными, а потому логической зависимостью между собой не обладают. Это означает, что их можно выполнять параллельно друг с другом. Но если мы посмотрим на наш машинный код, то увидим, что между ними всё-таки есть зависимость. Инструкция (3) записывает данные в переменную "a" из регистра r1 (т.е. читает регистр r1), а инструкция (4) считывает данные из переменной "b", но в тот же самый регистр r1 (т.е. записывает в регистр r1). Поэтому мы не можем исполнить инструкции (4) и (3) одновременно, т.к. они работают над одним и тем же регистром. Но это уже другой вид зависимости, который называется "Write After Read" (WAR-зависимость). По своей природе это искусственная (ложная) зависимость. Она присутствует в тех местах, когда различные инструкции логически могут исполняться параллельно, но в реальности им мешает использование общего регистра

Таким образом в нашей последовательности из 6 инструкций мы имеем две RAW-зависимые цепочки инструкций (1)-(2)-(3) и (4)-(5)-(6), которые связаны между собой WAR-зависимостью (3)-(4). Поэтому что-то исполнить параллельно тут не получится

3. Борьба с WAR-зависимостями

3.1. Теоретический пример переписывания кода

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

Как можно переписать машинный код, чтобы избавиться от WAR-зависимости? Зависимость у нас создаёт использование одного и того же регистра в двух разных RAW-зависимых цепочках. Но можно в разных цепочках использовать разные регистры, переписав машинный код следующим образом:

Code
# Листинг 2. Машинный код
 
(1) load  [a] -> r1
(2) add   r1, 100 -> r1
(3) store r1 -> [a]
(4) load  [b] -> r2        # здесь заменили r1 на r2
(5) add   r2, 200 -> r2
(6) store r2 -> [b]
Посмотрим, как будет исполняться этот код на улучшенной версии процессора, который умеет одновременно выполнять инструкции, использующие различные устройства. Сначала бы исполнилась инструкция (1). Инструкция (2) использует другое устройство, но из-за RAW-зависимости она не может исполняться параллельно с (1), а потому инструкция (2) начнёт исполняться только после того, как завершит исполнение инструкция (1). Аналогичным образом инструкция (3) начнёт исполняться только после завершения исполнения инструкции (2). Но вот инструкция (4) уже может исполняться параллельно с инструкцией (3), поскольку эти две инструкции используют разные устройства и не имеют между собой зависимостей по регистрам. Ну а дальше по очереди исполнятся инструкции (5) и (6), т.к. они являются RAW-зависимыми. Процесс исполнения в динамике можно отобразить следующим образом:

Code
# Листинг 3. Run-time выполнение
 
(1) load  [a] -> r1
(2) add   r1, 100 -> r1
(3) store r1 -> [a] ;  (4) load  [b] -> r2
(5) add   r2, 200 -> r2
(6) store r2 -> [b]
Помещённые в одну строку инструкции (3) и (4) символизируют собой одновременное исполнение. Такой код исполняется уже не за 6 шагов, а за 5

Наш машинный код мы можем в очередной переписать, чтобы подстроиться под работу улучшенного процессора. Для этого достаточно передвинуть повыше инструкцию (4), чтобы перемешать между собой инструкции из двух RAW-цепочек. Напомню, что после того, как во второй цепочке мы начали использовать другой регистр, цепочки стали независимыми между собой. Это позволяет перемешивать инструкции из разных цепочек, важно лишь то, чтобы внутри цепочки был выдержан правильный порядок инструкций. В последующем машинном коде я поменял местами инструкции, но для удобства сопоставления со старым кодом оставил старую нумерацию

Code
# Листинг 4. Машинный код
 
(1) load  [a] -> r1
(4) load  [b] -> r2
(2) add   r1, 100 -> r1
(3) store r1 -> [a]
(5) add   r2, 200 -> r2
(6) store r2 -> [b]
Порядок выполнения такого кода будет следующим. Сначала исполняется инструкция (1). Затем исполняется инструкция (4), она не может исполниться одновременно с инструкций (1), т.к. использует то же самое исполнительное устройство. Инструкция (2) использует другое устройство, в отличие от инструкции (4), и использует другой регистр, а значит может исполняться параллельно с инструкцией (4). Инструкция (3) имеет RAW-зависимость от инструкции (2), а потому может исполняться только после инструкции (2). Инструкция (5) может исполняться одновременно с инструкцией (3) т.к. использует другое устройство и работает с другим регистром. Инструкция (6) может быть исполнена только после инструкции (5), т.к. имеет RAW-зависимость. Итого в динамике исполнение кода будет выглядеть следующим образом:

Code
# Листинг 5. Run-time выполнение
 
(1) load  [a] -> r1
(4) load  [b] -> r2 ; (2) add   r1, 100 -> r1
(3) store r1 -> [a] ; (5) add   r2, 200 -> r2
(6) store r2 -> [b]
Это же самое перепишу в более удобной для восприятия форме

Code
# Листинг 6. Run-time выполнение
 
(1) load  [a] -> r1
(2) add   r1, 100 -> r1 ; (4) load  [b] -> r2
(3) store r1 -> [a] ;     (5) add   r2, 200 -> r2
                          (6) store r2 -> [b]
Здесь мы уже сократили время исполнения до 4 шагов. На листинге хорошо видно, как идёт параллельное исполнение различных RAW-зависимых цепочек. Но и это ещё не предел. Мы рассматривали наш процессор, как состоящий из трёх устройств: устройство для чтения из памяти, устройство для выполнения целочисленного сложения и устройство для записи в память. Но ведь в процессоре можно сделать по два устройства каждого вида. Т.е. можно научить процессор одновременно выполнять два чтения из памяти, два сложения или две записи в память. Чтобы наша программа смогла эффективно работать на таком процессоре, нам потребуется в очередной раз внести в неё небольшое изменение - закинуть повыше инструкцию (5)

Code
# Листинг 7. Машинный код
 
(1) load  [a] -> r1
(4) load  [b] -> r2
(2) add   r1, 100 -> r1
(5) add   r2, 200 -> r2
(3) store r1 -> [a]
(6) store r2 -> [b]
В динамике мы получим следующую картину:

Code
# Листинг 8. Run-time выполнение
 
(1) load  [a] -> r1 ;     (4) load  [b] -> r2
(2) add   r1, 100 -> r1 ; (5) add   r2, 200 -> r2
(3) store r1 -> [a] ;     (6) store r2 -> [b]
В этом случае мы видим полностью параллельное исполнение двух независимых цепочек инструкций. При желании можно нарисовать программу, в которой выделяется 6 логически независимых цепочек таким образом, что процессор будет исполнять аж 6 инструкций одновременно: два чтения из памяти, два сложения и две записи в память. И все эти одновременно исполняемые инструкции будут принадлежать различным цепочкам

Таким образом, научив процессор параллельно исполнять инструкции, требующие различные устройства, и увеличив количество исполняющих устройств, мы смогли вдвое ускорить исполнение нашей простой программы. Т.е. программа стала выполняться быстрее за счёт того, что процессор научился работать с внутренней параллельностью нашей программы. При этом ускорение работы программы НЕ сопровождалось увеличением частоты процессора. Ускорение программы явилось следствием улучшения "подкапотной" части процессора. Хочется ещё раз подчеркнуть, что здесь речь идёт об ускорении одного-единственного программного потока, т.е. о работе исключительно внутри одного процессорного ядра. Ни о какой многоядерности или hyperthreading'е тут речи не идёт

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

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

3.2. Реальное устройство аппаратной реализации

В предыдущем разделе мы уже поняли, что недостаточно добавить в процессор возможность параллельного исполнения инструкций. Нужно это сделать таким образом, чтобы коды, собранные под "старые" процессоры, на новом процессоре по возможности исполнялись эффективно, с учётом возможностей нового процессора. В наших примерах мы занимались тем, что переписывали код. Но переписывание по большому счёту сводилось только к двум примитивным вещам: переставить инструкции местами и использовать другие регистры. Этого было достаточно для того, чтобы процессор смог исполнять код с учётом внутренней параллельности программного потока. Единственной проблемой был сам факт того, что программу нужно переписать, т.к. это практически сводило на "нет" всю пользу от нового процессора. А что если код будет переписывать не программист, а сам процессор? Т.е. построить процессор таким образом, чтобы он, получив на вход "плохой" код, соответствующий листингу 1, сам внутри себя преобразовал его в код, похожий на листинг 7, чтобы в динамике получилось исполнение, соответствующие листингу 8. При таком раскладе мы получили бы процессор, который сам по себе умеет работать быстрее, и который автоматически ускоряет исполнение старых программ, не требуя их переписывания

Старые процессоры содержали конвейер, который осуществлял полное исполнение машинных инструкций, начиная от чтения инструкции из памяти и заканчивая её полным исполнением. В современных процессорах можно считать, что конвейер поделён на две последовательные стадии, между которым расположен специальный буфер (reorder buffer), который может содержать несколько инструкций. Первая стадия (front-end) выполняет чтение инструкций из памяти и их дешифрацию, а затем дешифрированную инструкцию помещает в буфер. Вторая стадия (back-end) забирает инструкции из буфера и исполняет их. В качестве простой аналогии можно привести пример, когда нужно ящики из машины перенести на склад и расставить по полкам. Между машиной и складом установлен промежуточный стол. Один грузчик достаёт ящики из машины и ставит их на стол. Второй грузчик забирает ящики со стола и расставляет их по полкам. Важным моментом является то, что второй грузчик может забирать ящики со стола в произвольном порядке, более удобном для расстановки их по полкам

Итак, после чтения инструкции и её дешифрации, инструкция попадает в reorder buffer, который, как уже говорилось, может хранить несколько инструкций. В процессе записи инструкции в буфер происходит процесс, называемый "переименование регистров" (register renaming). Во время переименования регистров все физические регистры системы команд (r1, r2, r3, ...) переименовываются в так называемые "виртуальные регистры" (обозначим их мнемониками vr1, vr2, vr3, ...). В первом приближении можно считать, что виртуальные регистры - это такие же регистры, но в отличие от реальных регистров системы команд, виртуальных регистров в процессоре имеется очень много. Процесс переименования выглядит довольно просто. Как только прочитали инструкцию, записывающую в регистр, например, r1, то этот регистр переименовывается, например, в vr1. Далее в последующих инструкциях, которые читают регистр r1, он везде будет переименован в vr1. Но как только встретилась повторная запись в регистр r1, то регистр vr1 как бы умирает (становится не нужным), а в этом месте рождается новый виртуальный регистр vr2, и регистр r1 будет переименован уже в vr2. После этого момента все чтения регистра r1 будут также переименованы в чтение регистра vr2. При очередной записи в регистр r1, виртуальный регистр vr2 умрёт, родится новый виртуальный регистр vr3, регистр r1 будет переименован в vr3 и так далее

Продублирую "плохой" код из листинга 1, чтобы он был перед глазами:

Code
# Листинг 9. Машинный код
 
(1) load  [a] -> r1
(2) add   r1, 100 -> r1
(3) store r1 -> [a]
(4) load  [b] -> r1
(5) add   r1, 200 -> r1
(6) store r1 -> [b]
После того, как этот код прошёл через front-end и попал в reorder buffer с одновременным переименованием регистров, он стал выглядеть следующим образом:

Code
# Листинг 10. Содержимое reorder buffer'а
 
(1) load  [a] -> vr1
(2) add   vr1, 100 -> vr2
(3) store vr2 -> [a]
(4) load  [b] -> vr3
(5) add   vr3, 200 -> vr4
(6) store vr4 -> [b]
Таким образом, каждая запись в любой физический регистр r порождает новый виртуальный регистр vr. Виртуальные регистры нужны того, чтобы связать две или более RAW-зависимые инструкции: одна инструкция записывает в виртуальный регистр, одна или несколько инструкций его считывает. Как только встретилась повторная запись в физический регистр r, то виртуальный регистр vr, соответствующий предыдущей реинкарнации физического регистра r, становится больше ненужным. Важным моментом является то, что после переименования регистров в коде не остаётся ни одной WAR-зависимости. Это происходит за счёт того, что каждый виртуальный регистр записывается ровно один раз, и все чтения виртуального регистра гарантированно идут по коду после записи в него. Таким образом, в коде остаются только инструкции, охваченные RAW-зависимостями или не зависимые друг от друга

Для полноты картины нужно сказать, что количество виртуальных регистров ограничено. Но процессор умеет их переиспользовать по кругу, после того, как они закончатся

Теперь посмотрим на back-end. На входе back-end'а есть reorder buffer, наполненный инструкциями, содержащими виртуальные регистры. Задача back-end'а заключается в том, чтобы из этого буфера выбрать инструкции, готовые к исполнению, т.е. те инструкции, у которых все регистры уже вычислены. В нашем случае логика работы back-end'а на первом шаге будет такая. Смотрим на инструкцию (1), у неё нет регистров на чтение, значит её уже можно исполнить. Смотрим на инструкцию (2), она читает регистр vr1, значение которого ещё не готово, значит пока её исполнять нельзя. Смотрим на инструкцию (3), она читает регистр vr2, значение которого ещё не готово, значит пока её исполнять нельзя. Смотрим на инструкцию (4), у неё нет регистров на чтение, значит её уже можно исполнить. Аналогичным образом инструкции (5) и (6) пока исполнять нельзя. Итого, back-end, проанализировав текущее состояние reorder buffer'а, нашёл там инструкции (1) и (4), которые уже готовы к исполнению. Далее эти две инструкции одновременно отправляются на исполнение и удаляются из reorder buffer'а. Таким образом после первого шага получим следующую динамику исполнения и состояние буфера:

Code
# Листинг 11
 
# Run-time выполнение
(1) load  [a] -> vr1 ;      (4) load  [b] -> vr3
 
# Содержимое reorder buffer'а
(2) add   vr1, 100 -> vr2
(3) store vr2 -> [a]
(5) add   vr3, 200 -> vr4
(6) store vr4 -> [b]
На втором шаге всё повторяется по той же схеме. Предположим, что инструкции (1) и (4) уже исполнились, т.е. в регистры vr1 и vr3 уже записаны прочитанные из памяти значения. В итоге после анализа reoder buffer'а выяснится, что по текущему состоянию можно исполнять инструкции (2) и (5), т.к. регистры, которые читаются этими инструкциями, уже готовы для использования. Соответственно, инструкции (3) и (6) пока исполнять нельзя, т.к. они имеют ещё не вычисленные регистры. Таким образом после второго шага получим такую динамику исполнения и состояние буфера:

Code
# Листинг 12
 
# Run-time выполнение
(1) load  [a] -> vr1 ;      (4) load  [b] -> vr3
(2) add   vr1, 100 -> vr2 ; (5) add   vr3, 200 -> vr4
 
# Содержимое reorder buffer'а
(3) store vr2 -> [a]
(6) store vr4 -> [b]
А после третьего шага будет вот так (совпадает с листингом 8):

Code
# Листинг 13
 
# Run-time выполнение
(1) load  [a] -> vr1 ;      (4) load  [b] -> vr3
(2) add   vr1, 100 -> vr2 ; (5) add   vr3, 200 -> vr4
(3) store vr2 -> [a] ;      (6) store vr4 -> [b]
 
# Содержимое reorder buffer'а
<пусто>
Таким образом мы получили технологию, которая исполняет инструкции не в том порядке, в котором они были написаны в исходной "плохой" программе, а в том порядке, в котором можно добиться "хорошей" производительности за счёт параллельного исполнения переупорядоченных инструкций. Эта технология называется "внеочередное исполнение" (out-of-order execution, OOE). Вся эта технология в совокупности с переименованием регистров очень похожа на JIT-компилятор, только не программный, а аппаратный

Здесь я всё описал очень упрощённо, на программе, состоящей всего из 6 инструкций, опустив при этом большое количество тонких моментов. В реальности reorder buffer постоянно находится в работе с обеих сторон. Front-end постоянно записывает в буфер инструкции, прочитанные из памяти, а back-end постоянно извлекает из буфера инструкции, готовые к исполнению в данный момент. Сам буфер имеет ограниченный размер, т.е. вполне может получиться так, что front-end не может ничего записать в буфер, т.к. инструкции в back-end'е исполняются медленно. При этом процесс заполнения буфера почти всегда происходит быстрее, чем исполнение инструкций, а потому состояние пустого буфера, как это было в простом примере, в реальной жизни практически не бывает. В нашем примере я предположил, что инструкции (1) и (4) исполнились одновременно, но вполне могло получиться так, что инструкция (1) прочитала данные из кэша, а потому отработала быстро, а для инструкции (4) данных в кэше не оказалось, и она застряла на чтении данных из памяти. В итоге две цепочки у нас могут начать исполняться совсем не параллельно. Точно так же арифметические инструкции могут исполняться за разное время - например, деление выполняется намного медленнее, чем сложение. Есть ещё миллион всяких тонких моментов, из-за которых реальная динамика исполнения может сильно отличаться от теоретической динамики на бумаге. Но общий смысл в том, что back-end постоянно анализирует буфер и пытается поставить на одновременное исполнение максимально возможное количество инструкций. Разумеется, с оглядкой на то, сколько и каких устройств имеется в процессоре

4. Масштабируемость процессора

В современных процессорах имеется большое количество исполнительных устройств, которые могут работать одновременно. Многие устройства могут быть продублированы. Например, в процессорном ядре может быть несколько устройств для чтения из памяти или несколько устройств для вычисления арифметических операций. При таком раскладе процессор умеет исполнять N инструкций одновременно. Но чему конкретно равно значение N для конкретного процессора - эту информацию найти довольно сложно. Проблему создаёт то, что число N - маркетинговое. Существуют различные методы того, как посчитать это самое значение, а так же есть проблемы того вида, что не всегда понятно, что именно можно считать полезной инструкцией. Очевидным образом разработчик процессора выберет тот метод подсчёта, который выдаст наибольшее число (немного шире этот момент будет раскрыт в разделе 7). Насколько я себе представляю, в современных процессорах число N имеет величину порядка десяти одновременно исполняемых инструкций за такт, но большой уверенности в этом нет

Важно понимать, что число N означает лишь то, сколько инструкций процессор в принципе МОЖЕТ исполнить одновременно. А сколько он будет выполнять в реальности - сильно зависит от конкретной программы. Допустим, процессор может одновременно исполнять 4 целочисленные и 4 вещественные инструкции, т.е. 8 инструкций за такт. Но если программа содержит только целочисленную арифметику, то за такт можно будет исполнить максимум 4 инструкции, а вещественные устройства будут пустовать. Степень загруженности устройств в том числе определяется и тем, насколько эффективно работает процесс переупорядочивания инструкций. Если программа представляет собой длинную последовательность зацепленных друг за друга инструкций (вторая инструкция читает результат первой инструкции, третья инструкция читает результат второй инструкции и т.п.), то такая программа представляет собой длинную RAW-зависимую цепочку, а потому не имеет внутренней параллельности. Т.е. в такой программе попросту нечего переупорядочивать, как итог все инструкции будут выполняться строго по очереди, а большинство устройств будет простаивать

С виду кажется, что чем больше поставить исполнительных устройств и чем больше сделать размер reorder buffer'а, тем больше вероятности, что среднестатистическая программа ускорится за счёт того, что у процессора будет больше возможностей для поиска внутренней параллельности и её реализации в виде одновременного исполнения инструкций. Однако всю радужную картину очень сильно портят условные переходы

5. Условные переходы

Рассмотрим простой пример на языке программирования:

C
if (c != 50)
  a = a + 100;
else
  b = b + 200;
и соответствующий ему машинный код:

Code
# Листинг 14. Машинный код
 
(1) load  [c] -> r1
(2) cmp   r1, 50     # сравниваем значения в регистре r1 и константу 50
(3) beq   L1         # если значения равны, то перейти на адрес L1, иначе продолжить исполнение с инструкции (4)
(4) load  [a] -> r1
(5) add   r1, 100 -> r1
(6) store r1 -> [a]
(7) b     L2         # безусловно перейти на адрес L2
L1:                  # инструкция (8) имеет адрес в памяти L1
(8) load  [b] -> r1
(9) add   r1, 200 -> r1
(10) store r1 -> [b]
L2:                  # инструкция (11) имеет адрес в памяти L2
(11) ...
В процессе исполнения front-end складывает инструкции в reorder buffer. С инструкциями (1) и (2) всё понятно, но проблема возникнет на инструкции (3) из-за того, что после неё процесс исполнения может пойти по одному из двух путей - либо случится переход на инструкцию (8), либо перехода не произойдёт и случится провал на инструкцию (4). Пока мы не вычислим результат инструкции (2), мы не сможем узнать, по какому из двух путей пойдёт исполнение нашей программы. Казалось бы, процесс наполнение reorder buffer'а front-end'ом придётся застопорить и подождать, пока станет известным результат выполнения инструкции (2), а это может случиться совсем не скоро, если для инструкции (1) не окажется данных в кэше, в итоге ожидание может оказаться очень долгим

Но современные процессоры идут другим путём. В момент дешифрации инструкций условных переходов, а в нашем случае это инструкция (3), процессор пытается угадать, по которому из двух путей пойдёт дальнейшее исполнение. Как выглядит процесс угадывания, напишу в разделе 6. Предположим, процессор решил, что последующее исполнение пойдёт по переходу. В этом случае после того, как в reorder buffer были помещены инструкции (1) и (2), следом за ними в reorder buffer попадут инструкции (8), (9) и (10). Ну а затем по порядку инструкция (11). В момент обработки инструкции (3) процессор мог решить, что исполнение пойдёт по провалу. В этом случае после инструкций (1) и (2) в reorder buffer попадут инструкции (4), (5) и (6). Дальнейшая инструкция безусловного перехода (7) не вызовет никаких затруднений, т.к. у неё есть только одно направление дальнейшего исполнения, а потому в redorder buffer следом попадёт инструкция (11)

Таким образом в reorder buffer'е будут лежать инструкции, которые находятся в коде до условного перехода и инструкции по какому-то одному из направлений после условного перехода. А back-end уже будет искать внутреннюю параллельность в этой мешанине и строить одновременное исполнение инструкций

Если к моменту вычисления инструкции (2) выяснится, что процессор правильно угадал направление перехода, то всё хорошо. Процессор уже начал исполнение тех инструкций, которые нужно было исполнить. Т.е. мы сэкономили время на том, что не стали ждать результат исполнения инструкции (2). То, что мы выполнили заранее, оказалось реально нужным и можно спокойно продолжать дальнейшую работу. Но если окажется, что направление перехода не угадали (mispredict, branch misprediction), то всё плохо. Мы выполнили инструкции, которые в нормальной работе программы не должны были исполняться. Это означает, что нам нужно отменить результаты выполнения этих инструкций. Т.е. по сути дела нужно откатить состояние процессора на тот момент времени, когда мы начали дешифрацию инструкции (3). В данной статье я вообще ничего не говорил о конвейеризированном исполнении инструкций, но для тех, кто знает об этом, скажу, что нужно в том числе отменить исполнение тех инструкций, которые уже поставлены на конвейер, но ещё не выполнены. А внутри конвейера могут вперемешку находиться "хорошие" инструкции, которые не нужно отменять, и "плохие" инструкции, которые нужно отменить

Я не знаю, как технически выглядит процесс отмены исполнения инструкций в случае mispredict'а. Но могу сказать как факт, что этот процесс очень дорогостоящий (т.е. долгий по времени). Конкретные цифры я назвать не могу, но могу сказать, что по некоторым статьям конца 1990'ых годов выходило, что вероятность угадывания в 90% находится на грани рентабельности. Другими словами, если мы угадали направление условного перехода в девяти случаях из десяти, и в одном случае ошиблись, то в среднем мы не получим никакого прироста по скорости исполнения. Возможно, что на сегодняшний день соотношение уже не девять к одному, а меньше, но в любом случае оно очень далеко от соотношения один к одному. Исходя из этого несложно понять, насколько важным является механизм, который позволяет угадывать направление условного перехода. Этот механизм называется "предсказатель переходов"

6. Предсказатель условных переходов

Как работает предсказание условных переходов (branch prediction)? В процессоре есть специальная ассоциативная таблица, которая хранит историю того, как работал условный переход. Индексом в таблицу является адрес инструкции условного перехода. Значением является специальный счётчик, описывающий наиболее вероятное направление перехода. Например, изначально для условного перехода было записано нулевое значение счётчика. Как только происходит вычисление точного направления перехода, то процессор модифицирует счётчик. Например, если случился переход, то счётчик увеличивается на единицу. Если случился провал, то счётчик уменьшается на единицу. Если случилось несколько переходов или несколько провалов подряд, то значение счётчика отдаляется от нулевого значения. Таким образом, зная адрес инструкции условного перехода можно быстро получить его наиболее вероятное направление, прочитав значение счётчика из таблицы. Если значение положительное, то вероятнее всего будет переход, если отрицательное, то вероятнее всего будет провал. Такая технология базируется на том, что статистическое большинство условных переходов в программе обладают тем свойством, что несколько раз подряд имеют одно и то же направление перехода. Я расписал один из возможных алгоритмов реализации предсказания переходов. Вполне возможно, что какие-то процессоры используют другие алгоритмы. Но суть от этого принципиально не меняется. Предсказание переходов делается на основании истории поведения отдельной инструкции условного перехода. По умному это называется "на основании профильной информации"

Разумеется, таблица счётчиков предсказателя условных переходов обладает конечным размером. Поэтому одновременно историю можно хранить далеко не для всех переходов. Это чем-то напоминает кэш. Если мы взялись за исполнение условного перехода, истории которого ещё нет в таблице, а таблица уже полная, то придётся из таблицы выкинуть историю какого-то другого перехода и на его место записать историю нового перехода (с начальным нулевым значением счётчика). Диапазон значений счётчика не должен быть слишком большим, чтобы не было эффекта залипания. Допустим, 10 раз подряд исполнение перехода шло в одну сторону. В итоге значение счётчика увеличится от 0 до 10. Затем, например, 10 раз подряд исполнение перехода пошло в противоположную сторону. При этом значение счётчика будет уменьшаться от 10 до 0. И во всём этом диапазоне счётчик остаётся положительным, а потому направление перехода будет ошибочно предсказываться в "старую" сторону, т.е. будем иметь многократный mispredict. Из этих соображений разрядность счётчика ограничивается величиной порядка 2 или 3 бит, т.е. с диапазонами значений [-2;+1] или [-4;+3] соответственно

Нетрудно догадаться, что при таком эвристическом подходе к предсказанию переходов всегда можно написать тест, в котором можно подогнать динамику исполнения условных переходов таким образом, чтобы предсказатель переходов всегда или почти всегда выдавал mispredict'ы, таким образом, сильно замедляя исполнение кода из-за постоянных откатов конвейера. Здесь ситуация чем-то напоминает hyper-threading - при определённых условиях технология ускорения может давать обратный эффект. К счастью, в подавляющем большинстве реальных случаев мы получаем именно ускорение, а не замедление

Я знаю, что в системе команд процессора SPARC у инструкций условных переходов есть специальный бит, при помощи которого программист может подсказать аппаратуре наиболее вероятное направление перехода. Возможно, нечто подобное есть и на других процессорах. Такая подсказка с одной стороны повышает вероятность выбора правильного направления в предсказателе переходов, с другой стороны экономит одну запись в таблице счётчиков, т.к. для таких переходов нет необходимости копить профильную информацию

7. Векторные инструкции

В современных процессорах есть инструкции, которые работая на целиковом регистре, логически выполняют одновременно несколько мелкоформатных операций

Попробую пояснить на простом примере. Например, у нас имеется процессор, который имеет 64-битные целочисленные регистры. При обработке графических изображений, алгоритмы как правило работают над 8-битными значениями, хранящими цветовые компоненты отдельно взятого пикселя. При этом зачастую приходится выполнять одни и те же действия над различными 8-битными значениями. Для ускорения процесса вычисления в процессор вводят специальные инструкции, называемые векторными инструкциями или SIMD (single instruction multiple data). Значение, записанное в 64-битном регистре, можно трактовать, например, как 8 подряд идущих значений по 8 бит каждое. В процессор вводятся специальные инструкции, работающие над 64-битным значением, но трактующим это значение именно как 8 независимых 8-битных значений и выполняющих одну и ту же операцию над каждым 8-битным куском независимо от остальных 8-битных кусков. Например, инструкция векторного умножения получает на вход два 64-битных значения и трактует их как группы из 8-битных значений, над каждым из которых нужно провести операцию 8-битного умножения. Таком образом создаётся эффект того, что за одну машинную инструкцию над 64-битными значениями логически производится 8 отдельных 8-битных умножений, к тому же всё это делается за то же время, за которое выполняется одно умножение

Принципиально такие векторные 64-битные инструкции ничем не отличаются от скалярных (т.е. "обычных" невекторных) 64-битных инструкций. Например, инструкция 64-битного скалярного умножения на вход принимает два 64-битных значения и проводит над ними некоторые манипуляции, которые логически можно трактовать как операцию 64-битного умножения. Можно немного изменить проводимые манипуляции, и это приведёт к тому, что их логически можно будет трактовать как, например, восемь 8-битных операций умножения. Другими словами, векторная инструкция с точки зрения реализации в процессоре принципиально ничем не отличается от скалярной, за исключением того, что это отдельная инструкция и требует дополнительного количества транзисторов

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

Развитие процессоров пошло по тому пути, что векторные инструкции обычно позиционируются НЕ как часть основной системы команд процессора, а как расширение системы команд. Это означает, что их наличие в процессоре НЕ обязательно. Программа, желающая работать с векторными инструкциями, должна самостоятельно проверять, что текущий процессор, на котором происходит исполнение, их поддерживает. Поэтому многие программы имеют в своём составе условно два комплекта кода, выполняющих одни и те же действия: один комплект написан с использованием векторных инструкций (быстрый код), а другой - без их использования (медленный код). Далее при исполнении на конкретном процессоре в run-time выбирается, который из комплектов кода будет работать. В итоге внешне всё выглядит так, как будто при исполнении задачи на процессоре, имеющем векторные инструкции, задача автоматически ускоряется. Но это не так, задача ускоряется НЕ автоматически, об этом позаботился программист при написании программы. Таких расширений имеется несколько разных наборов, появившихся в разные времена: MMX, 3DNow!, SSE, AVX и т.п. - все они являются наборами расширений, включающих в себя те или иные комплекты векторных инструкций и дополнительных регистров для работы с ними

В разделе 4 я уже упоминал о некоем числе одновременно исполняемых инструкций N. Векторные инструкции часто любят считать как несколько одновременно выполняемых арифметических операций, поскольку логически это именно так. Т.е. при вычислении числа N считать, что одна векторная инструкция выполняет, например, 8 операций. Однако с точки зрения процессора исполняется именно одна инструкция, которая технически принципиально ничем не отличается от скалярной. Но по маркетинговым соображениям удобно расписать так, что якобы процессор в этот момент одновременно выполняет 8 операций

Для чего векторные инструкции оформляются именно как необязательное расширение системы команд? Это позволяет выпускать процессоры в разной "комплектации". Например, в серверных процессорах, специализированных под какую-нибудь сетевую маршрутизацию, вполне можно обойтись без векторных инструкций, которые критичны для программ обработки графики. Это позволяет сократить количество транзисторов, позволяя на той же площади кристалла разместить больше кэша или больше процессорных ядер

8. Заключение

В данной статье я описал лишь небольшую часть того, что есть "под капотом" у процессора, да и то описал всё это в сильно упрощённой форме. В конечном итоге статья преследовала две цели: дать вводный материал для четвёртой статьи и наглядно показать, что производительность процессорного ядра определяется далеко не только одной лишь частотой

После прочтения статьи, надеюсь, станет понятно, что можно выпустить разные процессоры, которые на вход будут принимать один и тот же машинный код, на выходе будут формировать одинаковый результат, но при этом всю внутреннюю работу будут проводить разными техническими способами. Вся совокупность внутренних "подкапотных" технологий, которые реализуют разные способы получения требуемого результата для входного машинного кода, называется словом "микроархитектура". Sandy Bridge, Coffee Lake, Bulldozer, Zen - всё это названия различных микроархитектур

В качестве небольшого резюме по трём статьям попробую описать небольшой (и далеко не полный) список того, что влияет на производительность процессора:
  • Частота. Параметр более-менее очевидный. Программисту, как правило, никак НЕ нужно учитывать изменение частоты, ускорение программы произойдёт автоматически
  • Количество ядер. Этот вопрос разбирался в первой статье. Ускорение можно получить только в многопоточных программах. Программисту нужно прикладывать дополнительные усилия к тому, чтобы программа умела работать в несколько потоков
  • Технологии компенсации неполной загрузки наподобие Hyper-threading. Этот вопрос разбирался во второй статье. Ускорение можно получить только в многопоточных программах. Программисту как правило НЕ нужно прикладывать никаких усилий для ускорения программы, поскольку всю работу берёт на себя операционная система. Для того, чтобы достичь пиковой производительности, программисту обычно нужно правильно рассчитать количество программных потоков, учитывая особенности работы операционной системы с виртуальными ядрами, а так же правильно привязать программные потоки к физическим ядрам
  • Количество кэша. В большинстве случаев программисту НЕ нужно прикладывать никаких усилий. Для того, чтобы достичь пиковой производительности, нужно учитывать объём кэша и/или особенности его работы
  • Наличие расширений (для простоты ограничимся различными наборами векторных инструкций). Программисту нужно прикладывать дополнительные усилия к тому, чтобы написать код, использующий векторные инструкции, а так же обеспечить дополнительный "медленный" вариант кода без использования векторных инструкций в рамках отдельно взятого набора расширений - чтобы можно было исполнять код в том числе и на процессорах, на которых отсутствует нужный набор расширений
  • Микроархитектура: количество исполнительных устройств, размер reorder buffer'а, количество виртуальных регистров, размер таблицы счётчиков условных переходов и ещё миллион тонких вещей, непосредственно влияющих на скорость исполнения входного машинного кода. Косвенно сюда можно включить в том числе и техпроцесс (количество нанометров). Чем меньше нанометров, тем, например, больше исполнительных устройств или большего размера reorder-buffer можно впихнуть в одно процессорное ядро. В большинстве случаев программисту НЕ нужно прикладывать никаких усилий. Для того, чтобы достичь пиковой производительности, зачастую нужно хорошо знать, какой в точности машинный код будет построен компилятором, а так же понимать, как на него среагирует "подкапотная" часть конкретной модели процессора. К сожалению, в эту категорию попадает в том числе и вредоносный код. Достаточно вспомнить недавно нашумевшую уязвимость Spectre, которая является ни чем иным, как использованием особенностей микроархитектуры для кражи данных из памяти, напрямую недоступной вредоносному коду

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

Если нужно чисто теоретически сравнить производительность двух процессоров, зная все указанные выше технические характеристики, то подобная задача в общем случае не решаемая, т.к. нужно учесть слишком много различных факторов, как по отдельности, так и в их взаимодействии. Теоретическая прикидка обычно возможна в условиях, когда многие параметры являются близкими по значению. Например, можно грубо прикинуть разницу в производительности между двухъядерным процессором без hyper-threading и четырёхъядерным процессором с hyper-threading, имеющих одну и ту же микроархитектуру (даже с учётом различия частот и объёма кэша). Но практически нереально теоретически оценить разницу в производительности между серверным и ноутбучным процессорами, у которых одинаковая частота. Это как раз тот случай, когда частота не говорит практически ни о чём. У ноутбучных процессоров самым главным критерием является потребление электричества. Поэтому в ноутбучных процессорах очень сильно упрощают микроархитектурную часть. Например, в ноутбучном процессоре может быть упрощённый механизм предсказания переходов, или он может отсутствовать вовсе. При таком раскладе производительность процессора сильно уменьшается, но при этом сильно уменьшается и расход электричества. Поэтому вполне может получиться так, что серверный и ноутбучный процессоры имеют одинаковую частоту, но при этом имеют производительность одного ядра, отличающуюся на порядок. Этот момент важно понимать, чтобы не попасть впросак, оценивая производительности процессора, опираясь лишь на его частоту. Конкретно в данном примере решающим фактором окажется именно различие в микроархитектуре
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 6
Комментарии
  1. Старый комментарий
    Аватар для Croessmah
    Насколько я себе представляю, в современных процессорах число N имеет порядок десяти одновременно исполняемых инструкций за такт, но большой уверенности в этом нет
    Порядок десяти или всё-таки порядка десяти?

    В данной статье я вообще ничего не говорил о конвейеризированном исполнении инструкций, но для тех, кто знает об этом, скажу, что нужно в том числе отменить исполнение тех инструкций, которые уже поставлены на конвейер, но ещё не выполнены
    Возможно, стоит добавить ссылку на другую твою запись в блоге: Влияние конвейера на скорость исполнения кода


    В листинге 14 для разделения второго и третьего аргументов используется запятая, а в предшествующих листингах - стрелочка.
    И еще по листингу 14.
    Code
    1
    2
    
    (4) load  [a] -> r1
    (5) add   r2, 100, r1
    Наверное, (5) add r1, 100 -> r1 ведь a записана в r1 инструкцией (4).




    Спасибо за статью.
    Запись от Croessmah размещена 19.05.2018 в 22:18 Croessmah вне форума
  2. Старый комментарий
    Аватар для Evg
    Опечатки поправил

    Что касается конвейера, тут всё немного сложнее. Уж так получилось, что словом "конвейер" в разных контекстах пользуются с разным смыслом

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

    Ну и второй смысл - это собственно сам процессорный конвейер (fetch, decode, execute, write) и т.п.

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

    А "конвейеризированное исполнение инструкций", о котором я упомянул в текущей статье в конце 5-го раздела - здесь слово "конвейер" используется во втором смысле. Про это самое уже есть много статей и книг, в которых матчасть рассказана довольно хорошо. Простым поиском можно найти что-то типа https://en.wikipedia.org/wiki/... pipelining (перевод в https://ru.wikipedia.org/wiki/... 0%B5%D1%80, но он какой-то кривоватый, а потому по возможности лучше читать оригинал)

    Конвейеризированное исполнение появилось очень и очень давно (наверное, в 1970-х годах). Это ещё более низкий уровень распараллеливания, чем out-of-order execution. Это такое устройство процессора, при котором инструкции исполняются строго по одной штуке строго в порядке очереди, но при этом процесс выполнения одиночной инструкции внутри процессора разбивается на отдельные стадии исполнения: в простейшем случае это чтение инструкции из памяти (fetch), декодирование инструкции (decode), чтение input-регистров (read), исполнение (execute), запись output-регистров (write). Смысл конвейеризированного исполнения заключается в том, что, например, паять последовательных инструкций одновременно находятся в конвейере, но на разных стадиях исполнения. Пока 5-я инструкция только читается из памяти, 4-я уже декодируется, 3-я уже читает регистры, 2-я уже исполняется, 1-я уже записывает результат

    После того, как поработал out-of-order execution, в такой конвейер инструкции попадают в том порядке, в котором back-end выудил их из reorder buffer'а. Дополнительной проблемой предсказания переходов является то, как в случае mispredict'а в таком конвейере отменить выполнение отдельно взятых инструкций. На картинках в конвейере обычно рисуют 4-5 стадий. На современных процессорах количество стадий наверное уже достигает 20. Более того, в back-end'е процессор устроен как VLIW, т.к. имеет широкие инструкции (wide instruction), в которых явным образом закодировано одновременное исполнение (т.е. внутренности устроены так же, как и явная система команд в itanium или elbrus). Это ещё сильнее усложняет механизмы отката после mispredcit'ов

    В общем, в процессоре слишком много всяких сложных технологий, выстроенных одна поверх другой. Наверняка там есть много чего, о чём я даже и не слышал. Поэтому в статье я постарался описать лишь поверхностную часть в упрощённой и немного искажённой форме (по отношению к реальному устройству)
    Запись от Evg размещена 20.05.2018 в 11:15 Evg вне форума
  3. Старый комментарий
    Аватар для Croessmah
    Цитата Сообщение от Evg
    Что касается конвейера, тут всё немного сложнее.
    Цитата про конвейер просто попалась под руку. Я имею ввиду, что после прочтения той статьи, мне кажется, материал этой будет проще. Но это моё субъективное мнение.
    Запись от Croessmah размещена 22.05.2018 в 19:54 Croessmah вне форума
  4. Старый комментарий
    Аватар для Evg
    Да фиг знает, сложно сказать. Старая статья про конвейер ориентирована в чистом виде на программистов. А в этой статье есть шанс, что и непрограммисты что-то поймут, за счёт того, что используются лишь примитивные примеры. Для порядку ссылку надо добавить и добавить описание, что для программистов. Но тут очередная засада с размером текста, он впритык к лимиту. И так много чего пришлось урезать. А аккуратно построенная фраза с ссылкой займёт слишком много драгоценных байтов, которые остались в резерве для исправления ошибок и неточных формулировок (типа тех, которые ты заметил выше). Так что пусть пока ссылка торчит хотя бы из комментария. При случае попробую ещё где-нибудь урезать слова-паразиты, чтобы хоть дышалось свободнее
    Запись от Evg размещена 23.05.2018 в 00:29 Evg вне форума
  5. Старый комментарий
    Аватар для Croessmah
    Когда ждать заключительную часть?
    Запись от Croessmah размещена 23.12.2018 в 22:05 Croessmah вне форума
  6. Старый комментарий
    Аватар для Evg
    Да фиг знает... Надо ждать, пока творческий запал появится. В противном случае родится г-статья
    Запись от Evg размещена 09.01.2019 в 19:49 Evg вне форума
 
Новые блоги и статьи
Dev-c++5.11 Покорение вершины
russiannick 02.06.2025
С утра преследовала одна мысль - вот бы выучить С++. Сказано-сделано. Окончив смену, скачал в интернете бестселлер Дэвиса Dev-C++ для чайников. Книга оказалась интересной и я скачал среду, на примере. . .
Тестирование Pull Request в Kubernetes с GitHub Actions и GKE
Mr. Docker 02.06.2025
Мы все знаем, что тестирование на локальной машине или в изолированном CI-окружении — это не совсем то же самое, что тестирование в реальном кластере Kubernetes. Контекстно-зависимые ошибки, проблемы. . .
Оптимизация CMake для ускорения сборки
bytestream 02.06.2025
Вы когда-нибудь ловили себя на мысле, что пока ваш проект компилируется, можно успеть сварить кофе, прочитать главу книги или даже сбегать в соседний офис? Если да, то добро пожаловать в клуб. . .
JS String.prototype.localeCo­mpare()
mr_dramm 02.06.2025
скопировано из этой темы чтобы не потерялось. localeCompare без указания локали для сравнения строк под капотом использует Intl. Collator , который работает согласно Unicode Collation Algorithm. . .
Облако проектов
russiannick 01.06.2025
Слава Джа, написал прогу для компиляции. Значит написал компилятор? Обьем кода 300+ строк. Язык-яву. Вводим данные, заполняем поля, тычем радиобаттоны. И по итогу в поле результат получам листинг. . .
Rust и квантовые вычисления: интеграция с Q# и Qiskit
golander 01.06.2025
Мир квантовых вычислений традиционно оставался закрытым клубом для высокоуровневых языков типа Python и специализированных DSL вроде Q#. Однако в последние годы Rust начал тихую революцию в этой. . .
Кэш REDIS и C#
UnmanagedCoder 01.06.2025
Redis (Remote Dictionary Server) - это ультраскоростное хранилище данных в оперативной памяти, работающее по принципу "ключ-значение". Суть проста: данные хранятся не на диске, а прямо в RAM, что. . .
Lenovo IH110CX Rev: 1.0 motherboard manual
Maks 01.06.2025
Размещения без повторений
VistaSV30 31.05.2025
Код возвращает список вариантов размещений A^{k}_{n}=\frac{n!}{(n-k)!} from itertools import permutations def pwr(k, n): # Размещение без повторений (Placement without repetition) if k. . .
Redis и Node.js с TypeScript - решения для высоконагруженных систем
Reangularity 31.05.2025
Redis (Remote Dictionary Server) — сверхбыстрое хранилище данных в памяти, способное обрабатывать операции за микросекунды. И что особенно важно для нас — с удивительно простым API. А теперь. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2025, CyberForum.ru
OSZAR »