Это очень долгий, плотный и технический набег на архитектуру ЦП. Я попытался сделать это доступным для энтузиастов-непрофессионалов, но если вы не знаете примерно, как работают процессоры, вам придется нелегко.

СОДЕРЖАНИЕ

Introduction
The fundamental work of an instruction
  — Coarse-Grained Out-of-Order (CG-OoO)
   The Mill
Dependencies, not order
  1. False dependencies
  2. Control Flow
      1. Branch instructions impose ordering constraints ...
          Idea: Branch Predict and Branch Verify
      2. Branches outside the current context are opaque ...
  3. Variable-latency instructions
Memory
  Baseline approaches
  Mill Computing's approach
      Issue 1: Function boundaries still prevent ...
      Issue 2: Loads potentially in the critical ...
      Issue 3: Converging dataflow
      Issue 4: It interacts badly with loop pipelining
  Mill Computing's approach, cont.
  Ideas for a better load
      Idea 1: Prodigy-Mill hybrid approach
      Idea 2: Restricted Asynchronous Dataflow
      Idea 3: Source-agnostic argument pickups
      Idea 4: Standard prefetch
  Security
Concluding Remarks

Вступление

Я устал от засилья вышедшего из строя процессора. Они велики и расточительны, особенно популярный x86 беден, и их трудно понять. Их колдовство было бы более оценено, если бы они лучше использовали границы вычислений, но очевидно, что проблемы, которые решают люди, имеют скрытый недоступный параллелизм, намного превышающий то, что может извлечь вышедшее из строя ядро. Будущее вычислений, конечно же, не должно основываться на тераваттах энергии, потраченных на то, чтобы притвориться, что процессор проще, чем он есть на самом деле.

Есть некоторая надежда в идеях выскочек, таких как Mill Computing и Tachyum, а также в исследовательских идеях типа CG-OoO. Не знаю, добьются ли они когда-нибудь успеха. Я бы не стал на это ставить. Черт возьми, Мельница может никогда не уйти достаточно далеко, чтобы иметь возможность потерпеть неудачу. Тем не менее, я нахожу их захватывающими, и большая часть навязчивых слов звучит как Itanium, на мой взгляд, неинтересна.

Эта статья посвящена архитектуре пропорционально тому, сколько творческих и интересных работ они продемонстрировали публично. Это означает, что большая часть этой статьи комментирует архитектуру Mill, достаточно много о CG-OoO, а Tachyum упоминается лишь вскользь. Я мог бы обсудить еще кое-что, например TRIPS (более старая архитектура потока данных), Denver (новая архитектура ARM от NVIDIA, использующая двоичную трансляцию) или VISC (архитектура «threadlet» Soft Machine, убитая Intel), но я решил взглянуть на архитектуры, которые я считаю наиболее перспективными для рабочих нагрузок общего назначения.

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

Основная работа инструкции

Типичная инструкция в типичном наборе инструкций выполняет примерно следующее:

  • Прочтите операнды из глобального регистрового файла.
  • Выполните операцию.
  • Запишите вывод в файл глобального регистра.

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

  • Прочтите 16 операндов из файла глобального регистра одновременно в произвольных местах.
  • Выполните 8 одновременных операций.
  • Запишите 8 результатов в файл глобального реестра одновременно в произвольных местах.
  • Прочитать 16 новых операндов из файла глобального регистра одновременно, подмножество которых может быть любой комбинацией только что записанных результатов.
  • Выполните 8 одновременных операций.

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

Такой подход помогает в ситуации, но масштабирование все еще может быть проблемой. ЦП Skylake имеет 348 физических регистров на ядро, разделенных на два разных файла регистров. Напротив, существует 8 конвейеров выполнения, только половина из которых имеет общие ALU, поэтому путь пересылки намного меньше.

Крупнозернистый вне очереди (CG-OoO)

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

CG-OoO - это дизайн, предназначенный для разрушения этого набора глобальных зависимостей, в то же время позволяющий выполнять длительное внеочередное выполнение. Ключевое наблюдение, позволяющее это сделать, заключается в том, что кажется возможным обрабатывать зависимости иерархически и обходить недостатки иерархического подхода - неэффективное распределение ресурсов - за счет избыточного выделения ресурсов выполнения.

Каждое окно блока (BW) на диаграмме выше отвечает за выполнение одного базового блока, области кода без внутреннего потока управления, которая обычно имеет длину в среднем 5–20 инструкций. Каждое окно блока содержит два небольших файла регистров. Один из них - это файл локальных регистров с низкой пропускной способностью (LRF), который виден только выполняющемуся базовому блоку, а другой - сегмент более крупного файла глобального реестра (GRF), который глобально общается со всеми другими окнами блоков.

Файл локальных регистров используется примерно для 1 из 3 операндов и поддерживает BW с минимальной амортизированной пропускной способностью, всего 2 чтения и 2 записи за цикл. LRF короткий, но достаточно обширный по отношению к длине базового блока, 20 записей для их моделей, что позволяет избежать опасностей без переименования. По словам авторов, это делает LRF в 25 раз дешевле на доступ, чем 256-длинный файл регистров с высокой степенью мультипортируемости.

Глобальный файл регистров является общим и требует переименования, то есть создания сопоставления индексов исходного файла регистров с индексами GRF таким образом, чтобы все семанские записи выполнялись в разные индексы GRF, с повторным использованием только после более поздней записи в исходный файл регистров. индекс отмечен как неспекулятивный. При переименовании карты записываются блоком в локальный сегмент GRF, поэтому глобальными являются только чтения регистров.

Выполнение вне очереди также приводит к затратам на выдачу инструкций и пробуждение: в полностью вышедшем из строя процессоре любая запись в LRF может сделать любую локально поставленную в очередь команду готовой к выполнению, а любая запись GRF может сделать любую Инструкция готова к выдаче с любого ЧБ. Следовательно, CG-OoO иерархически выполняет лишь небольшую часть внепланового исполнения. Внутри каждого BW только из первых трех или около того невыпущенных инструкций являются кандидатами на отправку. Удаленный параллелизм извлекается только путем параллельного выполнения BW.

Соответствующее снижение затрат стоит перечислить в явном виде.

  1. Переупорядочивание локальных инструкций обходится дешево, требуя оборудования для проверки опасностей и пробуждения только через три-пять локальных инструкций.
  2. Глобальное пробуждение влияет только на те инструкции, которые принимают глобальные регистры и находятся в небольшом наборе операций-кандидатов для каждого BW.
  3. Количество зависших инструкций достаточно ограничено, чтобы они могли кэшировать операнды внутри проблемного оборудования. После этого они смогут читать любые недостающие операнды в последующих циклах из обхода, поэтому только недавно поставленные в очередь инструкции могут выполнять явное чтение GRF.
  4. Для каждого сегмента GRF требуется несколько портов, поскольку чтение GRF будет в основном распределяться между сегментами.

Мельница

Принимая во внимание, что CG-OoO допускает значительную совокупную пропускную способность за счет ограничения локальной пропускной способности до одной инструкции на амортизируемый цикл и отстает, когда локальная ILP необычно велика, Mill сначала пытается стереть локальные ограничения пропускной способности и использует вторичные методы для извлечения нелокального параллелизма. .

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

Мы можем установить приоритет использования обхода, сделав операнды более доступными из него, используя небольшую буферизацию выходных данных в конце каждого конвейера. В синергии мы можем сделать регистрацию файловых операций подпиской; Mill вызывает чтение и запись в свой регистровый файл подстановки fill и spill.

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

  1. Как построить обход, который масштабируется с увеличением буферизации в системе?
  2. Как избежать этого влияния на задержку критического пути по сравнению с меньшим и более быстрым обходом в стандартном процессоре?
  3. Какой объем буферизации требуется и как не допустить, чтобы это стало узким местом?
  4. Как программа взаимодействует с оборудованием?

У фабрики прямолинейный подход к этой задаче. Поперечная планка, аппаратное обеспечение, которое соединяет N источников с M стоками, легко масштабируется до указанных здесь размеров. К сожалению, большие поперечные панели имеют большую задержку, чем исходный байпас, что снижает тактовую частоту. Решение этого вопроса возможно, проложив быстрый путь в перекладину.

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

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

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

Последний вопрос о программном интерфейсе оборудования остается открытым и будет затронут в следующих разделах.

Зависимости, а не порядок

Изучение подхода CG-OoO пролило некоторый свет на компромиссы, необходимые при создании вышедшего из строя процессора. Однако предыдущее обсуждение намеренно не проясняло, был ли подход комбината к исправному или неработающему ядру. Фактически, кажется, что очень мало в подходе Милля напрямую улучшает соотношение затрат и выгод от вышедшего из строя оборудования, поскольку, казалось бы, вся сложная логика пробуждения все еще должна существовать.

У кого-то может возникнуть соблазн рассмотреть гибрид CG-OoO-Mill, хотя бы для перспективы. Однако это сложно представить; пояс оптимизирован для пропускной способности значений с коротким сроком службы, поэтому независимо от того, поддерживает ли ядро ​​выполнение вне очереди, мы должны решить проблему прямого извлечения статического параллелизма.

Mill Computing утверждает, что обычные вычисления можно делать быстро, и они предоставляют довольно много инструментов для этого. Прежде чем приступить к их анализу, необходимо понять причинно-следственные связи конкретных преимуществ внеочередного подхода. Следующий список, хотя и краткий, должен быть почти исчерпывающим.

  1. Неисправное оборудование обрабатывает ложные зависимости, например, посредством кодов условий и регистрации опасностей.
  2. Аппаратные средства, вышедшие из строя, позволяют планировать поток управления, близкий к оптимальному.
  3. Аппаратное обеспечение, вышедшее из строя, эффективно планирует в контексте инструкций с переменной задержкой, особенно для доступа к памяти.

1. Ложные зависимости

Исключения при целочисленном делении на ноль и коды условий создают случайные зависимости: исключения не позволяют компилятору переупорядочивать инструкции вне циклов и «если» или между глобально видимыми записями в память, а коды условий затрудняют спекуляции на оборудовании. Решение обеих проблем состоит в том, чтобы поместить эти условия в программное обеспечение, при котором деление на ноль приводит к некоторому значению регистра, а условия обрабатываются с помощью логических значений и сравнений. Это решение настолько бесспорно, что даже консервативная архитектура RISC-V использует этот подход. Еще одна проблема в этой категории - исключения при неудачной загрузке памяти, но память лучше делегировать в отдельный раздел из-за ее сложности.

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

  1. Опасности регистрации заставляют элементы памяти использоваться в программном порядке, препятствуя динамическому переупорядочиванию без переименования.
  2. Пределы имен регистров представляют собой верхнюю границу данных, которые могут храниться на первичном носителе без переименования.

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

Хотя переименование неизбежно связано с подобным несоответствием времени выполнения и выполнения, далеко не очевидно, что полное неструктурированное переименование - лучшее, что мы можем сделать. Фактически, похоже, что вариант архитектуры CG-OoO может позволить полностью удалить этап переименования и связанную с ним специальную глобальную связь.

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

В архитектуре CG-OoO каждое окно блока, а не каждая инструкция, имеет собственное представление файла логического глобального регистра. Каждое окно блока выполняет запись в любой заданный регистр не более одного раза, и каждый базовый блок имеет заголовок, который объявляет, в какие регистры будет производиться запись. Это делает реестры совместного использования менее необходимыми.

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

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

В этом подходе каждый регистр обменивается данными точка-точка, и нет никаких конфликтов, но фиксированный порядок BW ограничивает общую глубину выполнения с опережением.

2. Поток управления

Спасительная изящество вычислений во многих отношениях было предсказателем переходов. Если мы предположим, что предсказатель имеет точность 97%, что не является необоснованным, а инструкции перехода (как принятые, так и невыполненные) выполняются один раз каждые 5 инструкций, предсказатель точно проецирует путь в среднем на 5 / 0,03 ≈ 167 инструкций в будущее. Сила процессора, работающего вне очереди, заключается в том, что он может переупорядочивать инструкции по своему усмотрению в пределах той части этого окна, которая обрабатывается в данный момент. В неупорядоченном процессоре инструкции ветвления завершают цепочки зависимостей, то есть никакая инструкция никогда не зависит или, таким образом, не ожидает от правильно спрогнозированной инструкции перехода. То же самое и с CG-OoO, где поток управления завершает базовые блоки, и многие последующие базовые блоки могут оцениваться параллельно по мере разрешения их зависимостей.

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

Подходы к попытке извлечь такое поведение из машины упорядочивания выглядят на первый взгляд весьма ограниченными. Прогнозирование переходов по-прежнему используется для сокрытия задержки выборки и декодирования инструкций, и Mill имеет убедительный аргумент, что его предсказатель может быть лучше, чем стандартный подход, так что это не должно вызывать беспокойства и не будет здесь подробно описываться. Слоты задержки перехода, когда ветвь ожидает несколько циклов, прежде чем произойдет, в основном бесполезны для правильно спрогнозированных ветвей, но у Mill действительно есть связанная и интересная идея с их концепцией `` фазирования '' (хотя они продают это как повышение производительности в целом, чего нет). Эта идея по своей природе эквивалентна слоту задержки ветвления длиной в долю цикла, где инструкции приемника в выходящем блоке могут чередоваться с инструкциями не приемника во введенном блоке. Это «разрывает» ветвь на несколько циклов, избегая редких инструкций «разогрева» и «охлаждения», когда единицы используются недостаточно.

 standard VLIW         phasing (program)     phasing (execution)
================================================================
 read                  read modify write     read
 read modify           read modify write     read modify
 read modify write     read modify write     read modify write
                       =====branch======     ======
      modify write     read modify write     read modify write
                                               ===branch===
             write     read modify write     read modify write
 =====branch======                                     =======
 read                  read modify write     read modify write
 read modify                                      modify write
 read modify write                                       write
      modify write
             write

Следует отметить, что задержки в примере VLIW иллюстрируют типичную цепочку зависимостей, но необычные распределения инструкций могут фактически способствовать стандартному VLIW.

Остальные 90% проблемы с потоком управления состоят из двух частей.

2.1 Инструкции перехода накладывают на программу ограничения порядка

Давайте разберемся с первой из этих проблем на примере.

                           a = j + k
                           b = x + y
                           c = m * n
                           if c > a
                         /           \
                  d = a + 7         d = a - b
                  e = b + d         e = d + d

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

Стандартный упорядоченный подход изо всех сил пытается извлечь параллелизм.

       Left branch taken               Right branch taken
    c = m * n     a = j + k         c = m * n     a = j + k
    b = x + y                       b = x + y
      c > a                           c > a
    d = a + 7                       d = a - b
    e = b + d                       e = d + d

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

       Left branch taken               Right branch taken
    c = m * n     a = j + k            c = m * n     a = j + k
    b = x + y     d = a + 7            b = x + y     d = a - b
    e = b + d       c > a              e = d + d       c > a

Есть два общих направления для смягчения этого упорядоченного недостатка, обычно часть того, что в компиляторах называется «планированием суперблоков»: перемещение инструкций после выполнения ветки («подъем ниже») или их выполнение раньше, чем ветвление (« подъемник выше '). Хост ниже позволяет вам перекрывать критический путь инструкций в одном базовом блоке с критическим путем инструкций в другом, улучшая планирование, но добавляет зависимость от ветви во время выполнения. Приведенный выше подъемник устраняет зависимость от ветви, но требует одновременной оценки обеих целей , даже если ядро ​​имеет точные прогнозы фактической цели ветви.

                          Hoist below
       Left branch taken               Right branch taken
    c = m * n     a = j + k         c = m * n     a = j + k
            (stall)                         (stall)
      c > a                           c > a
    b = x + y                       b = x + y
    d = a + 7                       d = a - b
    e = b + d                       e = d + d
                          Hoist above
       Left branch taken               Right branch taken
    a = j + k     b = x + y         a = j + k     b = x + y
    c = m * n     d = a + 7         c = m * n     d = a + 7
    D = a - b     e = d + d         D = a - b     e = d + d
    E = D + D       c > a           E = D + D       c > a

У Hoist-above есть дополнительная проблема, заключающаяся в том, что требуется разрешение неоднозначности между двумя половинами его вычисления после факта (вам нужно выбрать, какие значения следует сохранить), и существует риск экспоненциального взрыва, если вы спекулируете более чем на одну ветвь в глубину. Устранение неоднозначности, требуемое в hoist-above, также заставляет выполнение инструкций зависеть от обоих путей вычисления, независимо от того, является ли одна сторона намного короче и быстрее, чем другая. Компромиссы с меньшими или смешанными уровнями подъема страдают аналогичным сочетанием этих проблем.

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

Идея: прогнозирование переходов и проверка переходов

Edit: Tapabrata Ghosh (deepnotderp) указывает на статью 2015 года, в которой изобрел ту же технику, Авангард ветвей: разложение функциональности ветвей на инструкции по прогнозированию и разрешению.

Кажется, что эту проблему можно частично решить с помощью нескольких важных выводов. Ядро, вышедшее из строя, может начать движение по правильному пути раньше времени не потому, что оно вышло из строя, а потому, что оно доверяет своему предсказателю. Неисправный процессор тоже может это сделать! Позвольте представить две уникальные инструкции.

brp (прогноз ветвления): принимает адрес для перехода и принимает ответвление тогда и только тогда, когда предсказатель ветвления предлагает это сделать. Эта инструкция не принимает в качестве аргумента предикат.

brv (Branch Verify): принимает предикат и тег brp для обновления. Если предикат ложен, выполняется переход к другому адресу в brp, обозначенному битом в инструкции; провал, если по выбранному пути, взятый, если по пути провала. Инструкции по проверке никогда не прогнозируются. Если выполняется проверка, вместо этого обновляется предсказатель ветвления для соответствующей инструкции Branch Predict.

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

       Left branch taken               Right branch taken
              brp                             brp
    c = m * n     a = j + k         c = m * n     a = j + k
    b = x + y     d = a + 7         b = x + y     d = a - b
    e = b + d     brv c ≤ a         e = d + d     brv c > a

Обратите внимание, что brp существует исключительно в потоке управления, поэтому, как и статические ветки, он не занимает слот конвейера и отображается только для ясности порядка инструкций.

По-прежнему существуют проблемы с brp, даже если согласиться с тем, что это улучшает ситуацию.

  • В случае сходящегося потока управления, когда несколько ветвей ведут к одному и тому же блоку, подъем ниже может завершиться ошибкой, поскольку входящие ветки захотят поднять разные последовательности команд. К счастью, с безусловными сходящимися прыжками можно справиться с помощью подъемника наверху.
  • Подъемник ниже дублирует код по обеим сторонам ответвления. Стоимость дублирования кода в глубине экспоненциально.
  • Целевая ветвь должна поддерживать целевые регистры альтернативной ветки в случае неудачной проверки ветвления, которая является линейной по глубине.
  • Все цели Branch Predict становятся сходящимися целями, включая brv, что требует согласованности состояния независимо от того, от какого родителя вы пришли. brv может справиться с этим с помощью специального кода восстановления или аппаратной поддержки, в зависимости от того, что работает лучше всего.
  • Инструкции сохранения не могут быть выполнены спекулятивно без специальной аппаратной поддержки, и инструкции загрузки по-прежнему зависят от них.

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

2.2 Ветви вне текущего контекста непрозрачны для компилятора

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

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

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

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

3. Инструкции с переменной задержкой.

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

  1. Филиалы, которые дороги, когда непредсказуемо. Аппаратные средства справляются с этим, составляя расписание для прогнозируемого сценария и пытаясь избежать слишком медленного получения неверных прогнозов, что очень хорошо работает.
  2. Деление, кроме константы. Команды деления достаточно редки, чтобы строго требовать эффективной обработки, при этом Mill использует необычный подход, просто выполняя деления в программном обеспечении (используя быстрые аппаратные аппроксимации для снижения стоимости).
  3. Инструкции по памяти. Это неизбежно и критично для производительности.

Существуют и другие, менее «стандартные» инструкции, такие как трансцендентальные, которые я пропущу.

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

объем памяти

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

Память - это проблема не только потому, что она сложна - но это так! -; Память является проблемой из-за того, насколько легко она может затмить проблемы остальной части системы. В то время как предсказатель ветвления может давать сбой раз в сто пятьдесят инструкций, каждый раз сжигая за собой десяток циклов, плохо написанный код может пропускать кэш через равные промежутки времени, а чтение DRAM занимает буквально сотни циклов. Для выполнения одной инструкции загрузки может потребоваться больше времени, чем совокупная стоимость 500 других, просто из-за отсутствия кеша. Одна инструкция загрузки, которая вызывает промах TLB, может занять гораздо больше времени.

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

  1. Изменение порядка загрузки и сохранения друг над другом обычно возможно только в том случае, если они не являются псевдонимами (они относятся к разным частям памяти). Несмотря на это, оборудование часто хочет начать загрузку с известного адреса до того, как станут известны другие адреса, которые могут с ним конфликтовать.
  2. Доступ к памяти может быть ошибочным, что обычно приводит к исключению. Это дополнительно предотвращает переупорядочение операций с памятью.
  3. Доступ к памяти занимает от нескольких циклов, обычно 3-4 для кэша верхнего уровня, до сотен в худшем случае. Как правило, невозможно обеспечить гарантированное время, необходимое для статического планирования. Планирование пропускной способности обычно требует предположения, что задержки будут короткими, а планирование длительных задержек требует смешивания удаленных инструкций и может иметь более низкую пропускную способность, поскольку оно расширяет критические пути.
  4. Трудно предсказать, пропустит ли инструкция памяти кэш. Компиляторы редко понимают, какие инструкции будут медленными.
  5. Любой заданный раздел кода может иметь несколько оптимальных расписаний, в зависимости от того, какая инструкция пропускает какие уровни кеша, если таковые имеются. Преобладание инструкций памяти и небольшой размер кэша верхнего уровня означает, что игнорировать этот риск небезопасно.
  6. Аппаратное обеспечение может одновременно обрабатывать несколько промахов в кэше, что называется параллелизмом на уровне памяти. Возможность сделать это увеличивается, когда происходят значительные промахи в кэше, но для извлечения этого параллелизма требуется, чтобы архитектура была способна выполнять операции, соответственно, далеко вперед в программе во время этих промахов кеша.
  7. Спекуляция с памятью может вызвать атаки по стороннему каналу, наиболее известным из которых является Spectre. Эти опасения будут обсуждаться только в конце этой темы.

Базовые подходы

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

У этого подхода много ограничений. Команда загрузки не может быть выдана, как только ее адрес станет доступным, если она потенциально может зависеть от хранилища позже в программе; семантика оборудования означает, что заявленный порядок программ является каноническим. Команда, которая первой использует («принудительно») нагрузку, планируется статически, но оптимальное положение будет зависеть от того, остановится ли нагрузка. Загрузка потребляет регистр, как только он выдается, что увеличивает затраты, связанные с задержкой потребителя результата. Операция с памятью также не может быть произведена спекулятивно в случае ее неисправности. Эта комбинация проблем означает, что расстояние между нагрузкой и форсированием нагрузки обычно должно быть небольшим, поэтому с его помощью можно скрыть только небольшие задержки.

Runahead - гораздо более серьезная попытка решить эту проблему, которая сегодня пользуется некоторой популярностью, например, в архитектуре NVIDIA Denver. Runahead направляет свои усилия в первую очередь на проблему выделения параллелизма на уровне памяти во время длительных простоев памяти. После того, как произошел промах в кэше, процессор предположительно выполняет наиболее вероятный путь инструкций, игнорируя любые значения регистров, которые неизвестны (например, пропущенная загрузка) или которые занимают слишком много времени для оценки (например, другие нагрузки, которые пропускают кэш). Когда первоначальный промах в кэше устранен, эта спекулятивная работа отбрасывается. Целью этого умозрительного первого прохода является ранний запуск загрузки, занесение их значений в кеш. Хотя выполняемые инструкции тратятся впустую, после возобновления стандартного выполнения ядро ​​с меньшей вероятностью пропускает кэш, что приводит к общей более высокой пропускной способности.

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

Подход Mill Computing

Завод использует вариант первого подхода к загрузке с несколькими ключевыми преимуществами и - (хотя компания об этом не упоминает) - несколькими существенными недостатками. Имейте в виду, что они также не объявили о каком-либо выполнении runahead, поэтому их подход должен обрабатывать все длинное хвостовое распределение задержек памяти.

Конкретный подход комбината со временем изменился. Здесь будет обсуждаться первоначально заявленный подход, а внесенные в них изменения - в следующем разделе, посвященном безопасности.

Чтение памяти на станке делится на две части: начальную load, которая должна быть запланирована при первом создании адреса, и списание, которое происходит либо после фиксированного числа циклов, либо при достижении определенной pickup инструкции, которая ссылается на исходный load . Также есть refuse, если нагрузка будет нежелательной. Для того, чтобы загрузка была запланирована раньше, через потенциально опасные магазины, ожидающие загрузки должны проверять («отслеживать») исходящие магазины. Таким образом, нагрузки семантически упорядочиваются по месту их списания, а не по выпуску. Устройство, которое обрабатывает груз и ожидает выхода на пенсию, называется станцией выхода на пенсию. Если возникает ошибка доступа к памяти, возвращается конкретное значение данных «NaR», и исключение не возникает.

Фиксированная задержка load, которая выдает свое значение после фиксированного количества циклов, подходит, когда потребитель находится в известном месте относительно производителя, что является обычным явлением, поскольку завод запланирован статически. Эта операция может проходить через поток управления, но требует, чтобы все ветви принимали прекращающуюся нагрузку в одной и той же точке, и поэтому на самом деле она предназначена только для использования в сегментах или циклах кода без ветвей. Удаление на основе pickup подходит для расходящегося потока управления, но теги выделяются статически, что создает некоторые специфические проблемы для кода с циклами или сходящегося потока управления, которые будут обсуждаться позже.

Возможность поднять начальную нагрузку на другие операции с памятью значительно упрощает раннее планирование. Загрузка также может пересекать полный вызов функции, так что она удаляется только после завершения вызова. Если вызываемая функция использует ограниченное количество станций вывода из эксплуатации (скажем, 16), адреса загрузки из кадров внешнего контекста сохраняются и перевыпускаются при возврате, так что начальная load действовала только как предварительная выборка. Если вызываемая функция не исчерпывает блоки, списываемая станция остается активной до своего естественного спуска.

В этой схеме видны некоторые основные проблемы, хотя некоторые из них требуют определенных усилий для раскрытия.

Проблема 1. Границы функций по-прежнему не позволяют поднимать груз

Рассмотрим следующий код:

int f(int x, int *y) {
    return x + *y;
}
int g(int *x, int *y) {
    return f(*x, y);
}

Если f не встроен, Mill не сможет выполнить два важных преобразования:

  1. Загрузка x внутри g должна выполняться до входа в f.
  2. Загрузка y внутри f не может быть выполнена до тех пор, пока не будет введено f.

Это означает, что Mill никогда не получит параллелизм на уровне памяти из этого примера. Если оба x и y пропускают кэш, вы тратите в два раза больше времени на ожидание в памяти.

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

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

Рассмотрим следующий код:

inf f(int **x, int **y) {
    return **x + **y;
}

Каждому из четырех выходов нагрузки назначен статически определенный цикл. Два пенсионера на левой стороне должны быть в разных циклах, и наоборот для правой. Все грузы потенциально могут заглохнуть. Для данного вывода с левой стороны только одна нагрузка с правой стороны потенциально может быть активной в течение этого цикла. Следовательно, остановка при разыменовании x не всегда сможет перекрыть остановку при разыменовании y, независимо от того, как скомпилирован код.

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

Эта проблема имеет множество форм, а не только каскадную загрузку. Пусть M представляет любое дорогостоящее вычисление.

int g(int *x, int *y) {
    return M(*x) + *M(y);
}

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

Проблема 3. Конвергенция потоков данных

Рассмотрим следующий код:

int f(bool cond, int *x, int y) {
    int ret;
    if (cond) {
        ret = *x;
    } else {
        ret = 0;
    }
    return M(y) + ret;
}

Разыменование x должно выполняться параллельно с оценкой M(y). Однако только одна сторона if выполняет нагрузку. На заводе объединение этих ветвей требует увеличения нагрузки, поскольку ее значение должно быть доступно после схождения ветвей.

Проблема 4. Плохо взаимодействует с конвейерной обработкой петель

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

Для этого предназначено load отключение на основе задержки. Это не удается по двум причинам. Во-первых, что особенно важно, задержка load не может измениться на итерациях цикла. Рассмотрим диаграмму, показывающую итерации цикла, разбитые на несколько циклов.

Optimal schedule when latency is low or loop has few iterations
  read  exec  write
        read  exec  write
              read  exec  write
                    read  exec  write
Optimal schedule when latency is high or loop has many iterations
  read  ... (many cycles) ...  exec  write
        read  ... (many cycles) ...  exec  write
              read  ... (many cycles) ...  exec  write
                    read  ... (many cycles) ...  exec  write

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

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

Подход Mill Computing, продолжение

Mill Computing рекламирует свой подход к доступу к памяти как примерно конкурирующий с вышедшим из строя оборудованием. Было продемонстрировано, что это не так; не в малом смысле, как в случае с потоком управления, где небольшие модификации вернули вещи на правильный путь, а в достижении, казалось бы, абсолютистских условий. Насколько я могу судить, подход Милля к памяти просто не приближается к устранению разрыва в производительности памяти между исправным и неисправным оборудованием.

Идеи для лучшей загрузки

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

Идея 1. Гибридный подход Prodigy-Mill

Tachyum Prodigy заявляет об очень впечатляющих цифрах по результатам их моделирования; Prodigy с тактовой частотой 4,0 ГГц якобы сопоставим с процессором Xeon с тактовой частотой 3,5 ГГц. Их основная раскрытая стратегия уменьшения задержки памяти с длинным хвостом - это неуказанный вариант runahead. Неясно, какие еще методы использовались, но они сообщают о степени успеха, значительно превосходя ядро ​​Itanium по количеству остановок кэша данных.

Runahead - это очень широко применимый метод, и он может быть ответственным за большую часть различий между этими подходами, тем более что инструкции загрузки Itanium сами по себе не были полностью наивными. Runahead должна быть реализована на CPU типа Mill, что делает его достойным кандидатом для включения.

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

Идея 2. Ограниченный асинхронный поток данных

Одна подгруппа проблем, которую можно решить независимо, связана с цепочками из нескольких нагрузок. Это очень часто отчасти связано с обычно иерархической структурой объектов, где для доступа к данным может потребоваться разыменование цепочки. Разыменования типа a->b->c или a[i][j] используют только небольшой набор операций:

  • Загрузка памяти,
  • Добавление указателя с регистрами или небольшими константами,
  • Умножение на маленькие константы, часто равные степени двойки.

Арифметика здесь намного проще, чем даже самый простой арифметический конвейер. Кроме того, нагрузки всегда имеют значительную задержку; Обычно используется 3 или 4 цикла для кэша верхнего уровня. Это позволяет рассматривать этот случай с помощью выделенного оборудования с избыточными выделениями.

Подумайте, каждая ли выведенная из эксплуатации станция выводит данные в специально выделенную цепочку асинхронных ALU, позволяющую заданный набор операций, и что эти ALU могут быть подготовлены основным ядром во время минимальной задержки нагрузки устройства. Кроме того, разрешите этим ALU вернуться в блок загрузки, буферизуя связанную нагрузку, и позвольте этой операции выполняться рекурсивно до предела, определенного аппаратным обеспечением. В качестве необязательного расширения идеи рассмотрите возможность разрешения списанной станции делиться любыми производимыми ею значениями с ALU других устройств. Это позволяет выполнять такие операции, как a->b->c + a->b->d, полностью асинхронно.

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

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

Идея 3: аргументы, не зависящие от источника

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

Первый базовый подход, выполняющий pickup неявно при первом использовании, позволяет избежать этой проблемы, но означает, что единственной известной точкой сериализации является начальная load. Возможен простой гибридный подход. Инструкция preload заменяет текущий load и создает тег, который находится на ремне, ссылаясь на списанный блок в состоянии prefetch. Инструкция load принимает тег к блоку вывода состояния preload и меняет его на состояние load, которое совпадает с preload, за исключением того, что аппаратные средства гарантируют, что порядок loadс сохранен. Инструкция pickup принимает любое значение пояса; если это значение, это значение остается неизменным, если это тег, то соответствующая загрузка принудительно и тег заменяется на месте на значение.

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

Идея 4. Стандартная предварительная выборка

Одна из проблем, упомянутых ранее, связана с инструкциями с большой задержкой в ​​конвейерных циклах. Решение здесь на удивление простое. В статье Программная предварительная выборка для косвенного доступа к памяти показано, как использовать инструкции предварительной выборки для конвейерных циклов. Это следует считать обязательным для исправного оборудования.

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

Безопасность

Компьютеры систематически уязвимы для атак по побочным каналам. Например, операция может потреблять больше энергии с некоторыми входами, чем с другими; поэтому информацию о ценности можно узнать, посмотрев на потребление энергии. Наиболее распространенные и опасные атаки - это атаки по времени. Поток управления является одним из крупнейших участников. Самый большой, память - тема этого раздела.

Каждый раз, когда адрес a используется для доступа к памяти, состояние кеша может быть изменено таким образом, чтобы позволить атакам по побочным каналам обнаруживать a. Поэтому все значения, используемые в качестве адресов, следует считать утекшими.

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

Дважды косвенный доступ к памяти формы **a утечка *a. Для a часто нет доверия и он ограничен заданным диапазоном допустимых значений. Однако, если значения, к которым необходимо получить доступ, охраняются только потоком управления, спекулятивное выполнение может все же начать загрузку на *a раньше, что приведет к утечке данных, выбранных злоумышленником.

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

Что еще более удивительно, подход Милля также подвержен этому. Их инструкции load предназначены для поднятия перед потоком управления, а эффективная компиляция открывает тот же побочный канал, что и в вышедших из строя процессорах. Mill Computing опубликовала ответ на открытие, заявив, что архитектура Mill принципиально невосприимчива к уязвимостям типа Spectre, но вместо этого их уязвимость к Spectre была ошибкой программного обеспечения в цепочке инструментов Mill. Впервые известны мне инструкции условной загрузки loadtr и loadfl, которые позволяют проводить неспекулятивную проверку условий.

Этот ответ вызывает беспокойство. В конце концов, ни один производитель вышедших из строя ядер не ответил Spectre отказом от спекуляций. Ранние меры по смягчению последствий привели к значительному снижению производительности; будущие процессоры решат самые сложные аппаратные проблемы. Похоже, либо вы решаете эту проблему аппаратно, либо становитесь неконкурентоспособными; Кажется, что Мельница идет по второму пути.

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

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

Заключительные замечания

Хотя эта статья закончилась на отрицательной ноте, я не хочу, чтобы это было основным выводом из этой публикации. Лучше подумайте, что пространство возможностей, которое доступно нам, намного больше, чем пространство вещей, которые были опробованы. Эти архитектуры могут быть несовершенными, но они делают реальные шаги к решению сложных проблем. В этом посте я изложил свои собственные творческие идеи, но ни в коем случае CG-OoO, The Mill и мои идеи не охватывают все возможные варианты.

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

А. Пропускная способность

  1. Архитектура должна обеспечивать эффективную маршрутизацию инструкций с минимальной задержкой между большим количеством функциональных блоков.
  2. Архитектура должна быть спроектирована для обработки декодирования и отправки, значительно превышающих доступную арифметическую ILP.

Б. Зависимости и извлечение ILP

  1. Архитектура не должна создавать ложных зависимостей любого рода.
  2. Архитектура должна иметь широкое поле восприятия над программой уровня исходного кода, чтобы видеть весь спектр исполняемых инструкций.

Это серьезный вопрос, но и CG-OoO, и Mill делают хорошие шаги навстречу им, и будущее, хотя и загадочное и неопределенное, многообещающее.