Search
Write a publication
Pull to refresh
70
0.2
Иван Савватеев @SIISII

Микроконтроллеры, цифровая электроника, ОС…

Send message

там есть AMBA для связи с периферией (и вроде как AXI входит в AMBA)

Да, AXI, как и другие шины (AHB, APB, ещё что-то, вроде бы...) -- часть AMBA.

работа с памятью идёт через интегрированные контроллеры

Угу. И наверняка связь ядро-контроллер памяти -- через AXI (нет смысла специально для этого изобретать какую-то другую шину).

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

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

Дело не только в операционке, без когерентности кешей обычный код написанный на C или Фортране (скажем, с распараллеливанием через OpenMP) не будет работать корректно, и барьеры не помогут

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

Т.е. программно это реализовать вполне можно на, скажем, уровне стандартной библиотеки (атомики и барьеры в современных версиях C++ вполне дают возможность это сделать; вот насчёт Фортрана не уверен). Ну и, в любом случае, это будет обеспечиваться при вызове сервисов ОС.

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

Ну, у тех армовских ядер, что я видел, из ядра наружу торчит именно AXI (или AHB, но это на младших и средних микроконтроллерных, на Cortex-M7 -- уже AXI). Полагаю, внутри кристаллов от процессорных ядер до контроллера памяти идёт именно она (на ПЛИС так точно она, но ПЛИС -- всё же специфическая вещь). Вот снаружи микросхемы (кристалла), надо полагать, идёт уже обычная PCI Express, ибо она -- стандарт де-факто для всего современного мира.

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

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

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

Я тоже не один десяток лет работал и работаю. Аппаратура сколько-нибудь существенно не усложняется, все необходимые линии индикации, какие байты, записываются, имеются. См., например, шину AXI -- основную на ARMах (кроме не шибко мощных микроконтроллеров, где основной будет AHB -- но там та же история):

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

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

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

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

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

Плюс, сишный код, полученный таким путём -- он, конечно, сишный, но логика зачастую остаётся непонятной: вполне могут быть if и goto вместо циклов и т.п. вещи.

Так они и не будут видеть изменения, вносимые друг другом -- это нормально. Даже на машинах со строгой моделью (IA-32) "мгновенной" видимости нет, нужно использовать барьеры, ну а на машинах со слабой моделью нужно ещё обеспечить принудительную очистку соответствующих строк кэша, чтобы другие процессоры смогли увидеть изменения.

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

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

Не нужно. Когда производится сериализация, в память записываются не целые строки кэша, а только те байты, которые реально были изменены. Соответственно, если один процессор изменяет байты 0-3, а другой -- 4-7 в одном и том же блоке памяти, занимающем одну строку кэша (скажем, 64-байтовом), то в память всё запишется корректно.

На практике, подобный подход (слабая модель упорядочивания доступов памяти) открывает возможность почти что неограниченного масштабирования машины: можно добавлять сколько угодно процессоров, упор имеет место, скорей, в массо-габаритные характеристики, энергопотребление и т.п. вещи. А у, скажем, IA-32 с его аппаратным поддержанием согласованности кэшей всех процессоров (сильная модель упорядочивания доступов к памяти) такая возможность ограничена необходимостью иметь, фактически, связь процессоров (их кэшей, причём всех уровней) "все со всеми". Очевидно, что чем больше процессоров, тем больше они порождают трафика обновления данных в кэшах -- и это становится проблемой, ведь любая запись, произведённая любым процессором, должна быть передана кэшам всех остальных процессоров машины. Кроме того, насколько понимаю, операции записи, выполняемые периферией с DMA, тоже автоматически попадают в кэши процессоров.

Что же касается производительности... Судя по всему, именно производительность у мэйнфреймов вполне себе на уровне. Однако скорость реакции на прерывания для них никогда не была определяющей (несмотря на рекламу ИБМ про "360 градусов" охвата задач, которые они могут решать -- это было, естественно, неверным даже в 1964-м, когда она их анонсировала): исторически они рассчитывались на пакетную обработку данных. В классической OS/360 оно ухудшалось ещё больше самой системой: изрядное время её код выполнялся при полностью запрещённых прерываниях, что, понятное дело, не ускоряло реакцию на их появление. Так что, думаю, обработкой срочных запросов занимаются машины других архитектур, а на мэйнфреймах лежит, так сказать, бэкенд -- гигантские базы данных и всё такое, где важней общая производительность, а не время ответа. Ещё одним вариантом может быть обработка прерываний (шире -- организация всего ввода-вывода) на одних процессорах и обработка данных -- на других, причём для уведомления других процессоров о появлении для них работы использовать не прерывания, а изменение всяких семафоров и прочих мьютексов в памяти: система команд располагает командами, осуществляющими лишь частичную сериализацию и поэтому позволяющими реализовывать быстрые примитивы синхронизации без необходимости каждый раз дёргать систему. Но это создаёт дополнительные проблемы для программиста (впрочем, они есть у любой архитектуры со слабой моделью -- на тех же ARM, где в определённых случаях нужно ручками вставлять барьеры, в то время как на IA-32 это требуется намного реже).

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

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

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

Мне кажется, автор просто что-то там недопонял. Недостаточное понимание видно, например, из "описания" MMU -- ведь принципиально и на ARM, и на IA-32, и почти в любых других архитектурах MMU работают одинаково -- точно так же, как ещё в Системе 370 (несколько уровней таблиц переадресации в памяти, базовый адрес таблицы верхнего уровня -- в некоем специальном регистре, а для ускорения преобразования -- TLB).

Инструкции работают со скалярными или векторными регистрами, если надо обработать большой массив, нужен цикл. И да, сейчас видимые отличие CISC/RISC скорее в наличии/отсутствии аргументов в памяти в арифметических инструкциях. Хотя сейчас в ARM есть, скажем, CISC-style атомики, инкрементирующие значение в памяти одной инструкцией, в спецификации (в железе пока не видел) есть и инструкции копирования памяти по типу REP MOVS в x86.

Кстати говоря, для реализации атомарных операций вроде И, ИЛИ шина AXI имеет соответствующие навороты, чтобы операции эти фактически выполнялись контроллером памяти, а не процессором. Откопал, когда смотрел сию шину (использую в своих мегапроектах на ПЛИС, поскольку она используется для подключения контроллера памяти).

В общем, похоже, от изначальных RISCов теперь не осталось ничего. Ну а микрокод высокой производительности ни разу не противоречит, как и я говорил, и всякие там Интелы на практике доказывали. Как по мне, бороться с ним не надо -- он даёт гибкость (медленная, но компактная реализация тоже может быть ценной -- а использование микропрограммного управления даёт возможность легко подстраиваться под потребности). В общем, исходить, смотря по задаче.

  1. Про RISC-V не знал -- по большому счёту, я с ним не знаком, хотя всё собираюсь заняться. В известных же мне типа RISCах подобные операции (шифрование, например) выполняются железными блоками, которые расположены отдельно от процессора и, по сути, выступают в роли внешних устройств, т.е. частью соответствующей архитектуры не являются. Таковы, например, многие микроконтроллеры ARM: сама система команд никаких тебе шифраций или, скажем, преобразования UTF-8 в UTF-32 или обратно не содержит.

  2. Ну и опять повторюсь, что от изначальной идеи RISC (а она -- именно что небольшое количество простых команд) отказались, считай, полностью -- она оказалась неконкурентоспособной с CISCами, как только совершенствование технологий дало возможность конвейерный (а позже и суперскалярный) CISC впихнуть на один кристалл. Так что лично для меня сейчас RISC -- это архитектура, не имеющая команд обработки данных прямо в памяти, ибо это, по сути, единственное, что нынешние RISCи отличает от нынешних CISCов, хотя и это различие, похоже, постепенно уходит (ведь, полагаю, RISC-V шифрует данные в памяти? Регистров у него не так уж много, и большой объём в них не впихнуть. Надо как-нибудь изучить сей вопрос...)

Если говорить про архитектуры (в первом приближении -- про системы команд), то никаким гибридом i486 не является -- самый натуральный (и плохой) CISC.

Если же говорить про внутреннее устройство, то, как я уже написал, конвейеризация появилась задолго до появления RISC, и одно с другим не связано от слова "совсем".

Ну, это именно ошибка в чистом виде, а не "фишка" (особенность микроархитектуры), которая превращается в уязвимость.

  • Так как инструкции могли быть очень длинными, они требовали больше транзисторов, что повышало энергопотребление процессора, что было большой проблемой для отрасли 3д-моделирования и рендеринга того времени;

  • Для декодирования и выполнения нескольких сложных операций процессору требовалось много памяти, которая на тот момент стоила очень дорого (к примеру, один мегабайт оперативной памяти стоил около 5000 долларов). Также при обращении декодера к памяти скорость обработки инструкций становилась меньше;

  • Большинство возможностей CISC (например, строковые операции) редко использовались, но усложняли декодер.

Строго говоря, неверны все три пункта.

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

С декодированием тоже далеко не всё однозначно. Когда автор статьи пишет про длину команды от 1 до 15 байт, он, несомненно, имеет в виду IA-32 aka x86. Эта кодировка не самая длинная (у VAX-11, если память не изменяет, длина команды могла достигать вообще 36 байт), но одна из самых бредовых и сложных для декодирования -- в первую очередь, из-за наличия префиксов, ни количество, ни порядок которых никак архитектурой не ограничены. Однако CISC отнюдь не обязан быть столь безумным. Скажем, такой классический CISC, как IBM System/360 (и все её потомки, включая современную z/Architecture), имеет длину команды 2, 4 или 6 байт, причём длина однозначно устанавливается анализом двух старших битов первого байта -- т.е. даже проще, чем, скажем, для команд набора Thumb-2 архитектуры ARM (там всего две длины -- 2 или 4 байта, -- но анализ длины сложнее).

Насчёт памяти написана вообще полная ерунда -- CISC как раз требует меньше памяти, поскольку в одной команде может закодировать куда более сложную операцию. Сколько места займёт подпрограмма шифрования AES на ARM или RISC-V? Ну а на мэйнфреймах z/Architecture это делается одной командой. А если необходима десятичная арифметика? (а она необходима для финансового сектора -- недаром соответствующие вещи есть, например, в Жабе) На большинстве архитектур, включая IA-32, будут пляски с бубном, чтобы добиться того же результата, используя целочисленную двоичную арифметику, ну а мэйнфреймы ещё с Системы 360 имеют и команды десятичной арифметики. Да, и операция пересылки строк на них тоже используется весьма и весьма часто -- пересылать ведь не только строки в узком смысле приходится, а произвольные блоки данных.

(Замечу, что есть по крайней мере одно типа исследование, которое "доказывает", что плотность кода CISC ниже, чем RISC: но, во-первых, сами авторы там между строк замечают, что, фактически, их сравнение неправомерно, поскольку сравнивают современные 64-разрядные архитектуры ARMv8 и RISC-V с древними CISCами, где 64-разрядные расширения были добавлены много позже -- а соответственно, все короткие коды операций уже попросту были заняты; а во-вторых, они начисто игнорируют случаи, когда сложные операции выполняются на CISCах одной-двумя командами, а на RISCах требуют подпрограмм значительного размера -- т.е., попросту говоря, подбирают тесты под требуемые результаты)

Усложнение декодера -- тоже далеко не факт. В частности, декодер для системы команд Thumb-2 будет сложней, чем даже для современных мэйнфреймов z/Architecture, не говоря о классической Системе 360

Усложнение управляющей логики, о чём автор почему-то не говорит, -- чуть ли не единственный пункт, где можно было бы согласиться, но и здесь не всё просто. Многие CISCи (а с 1970-х годов -- пожалуй, все или почти все) используют не чисто схемное, а микропрограммное или смешанное микропрограммно-аппаратное управление, что многократно упрощает конструкцию управляющей части процессора (память микропрограмм многократно проще и дешевле, чем управляющая логика). Впервые микропрограммы были использованы на младших и средних моделях Системы 360; старшие модели делались со схемным управлением (и были конвейерными) -- разработчики опасались, что микропрограммное управление будет медленнее. Но, как оказалось, оно может быть столь же быстрым: это зависит от особенностей микроархитектуры; поэтому, набив руку, его стали использовать и в топовых моделях. Скажем, в младших моделях (IBM System360/30 и наша ЕС-1020) АЛУ было 8-разрядным, а соответственно, простейшая 32-разрядная операция требовала для своего выполнения четыре микрокоманды только для собственно вычислений -- не говоря о всяких других потребных действиях; понятно, что это было очень медленно -- но требовало очень мало аппаратуры, почему, собственно, и использовалось. Однако, если не экономить оборудование для выборки команд и для собственно выполнения операций, разницы в скорости с машиной с чисто схемным управлением не будет, а разница в объёме аппаратуры окажется весьма значительной. Скажем, наша ЕС-1130 при заполненном конвейере выполняла простые команды регистр-регистр и регистр-память (!) за один такт, но была микропрограммной, просто вся микропрограмма для этих простых команд состояла из единственной микрокоманды (а вот выборка команды, декодирование, вычисление адресов операндов и их подготовка в ней были сделаны чисто аппаратно; микропрограмма управляла лишь ходом выполнения собственно обработки данных -- была, как я уже сказал, элементарнейшей для простейших операций, но сложной для сложных). Примерно ту же идею, только в значительно более сложном исполнении (в первую очередь, из-за суперскалярности и внеочередного исполнения) используют и современные CISC-процессоры (что у интеловских процессоров есть микрокод, наверное, все знают).

В 1980-х RISC-микропроцессоры (подчёркиваю микро) показывали более высокую производительность над CISC-микропроцессорами за счёт того, что благодаря очень малому числу команд (первый из них насчитывал, насколько помню, всего 31 команду) и другим упрощениям его можно было сделать конвейерным, но всё равно впихнуть на один кристалл; для CISC-процессора на тот момент это было невозможно, поэтому приходилось выбирать: либо медленная микропрограммная реализация на одном кристалле, либо быстрая, но на куче микросхем (в IA-32 первым конвейерным стал 80486; в мэйнфреймах конвейерными давно уже были все модели, а некоторые -- и суперскалярными, но они достаточно долго оставались многокристальными). Однако, если рассматривать все процессоры, а не только микро, RISC по скорости первыми отнюдь не были -- но далеко не каждый может позволить себе поставить мэйнфрейм или, тем более, какой-нибудь Cray с охлаждением жидким азотом (и хорошо, если азотом). Но в 1990-х, когда количество транзисторов на кристалле значительно выросло, конвейерные CISC тоже стали однокристальными -- и догнали (а нередко и перегнали) по скорости RISC. Да и сами современные RISC, прямо скажем, зачастую мало чем отличаются от CISC: имеют сотни, а то и тысячи команд, включают ряд достаточно сложных операций и т.д. Изначальная идея RISC (напомню, у первого из них была 31 команда, и среди них не было даже умножения и деления -- они сложные) оказалась несовместима с высокой производительностью, поэтому от неё отошли; фактически единственное, что осталось -- это отсутствие команд обработки данных вида "регистр-память" или "память-память" -- вся обработка выполняется исключительно в регистрах.

Более того, и конвейер, и суперскалярность появились ещё на машинах 1960-х годов. У нас первой конвейерной машиной была БЭСМ-6 (1967 год), у американцев всё это появилось ещё раньше.

А сколько нафиг пошло систем 60-70-х годов...

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

Вершинный или вертексный нормальный рабочий сленг.

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

Ну а ошибки в русском языке в любом случае имеют место быть, и они никакой текст не красят.

1
23 ...

Information

Rating
3,495-th
Location
Солнечногорск, Москва и Московская обл., Россия
Date of birth
Registered
Activity

Specialization

Embedded Software Engineer
Lead