19 December 2012

Баллада о «Мультиклете»

High performanceSystem Programming
Нет, я не раскрою вам загадку, скрывающуюся в названии MCp0411100101, но постараюсь развёрнуто ответить на комментарий nerudo, записанный в топике Процессоры «Мультиклет» стали доступнее:

Читая описание архитектурных новшевств этого мультиклета, мне хочется воспользоваться фразой из соседнего топика: «Я не понимаю».


Если кратко, то MCp — это потоковый (от dataflow) процессор с оригинальной EPIC-архитектурой. EPIC — это Explicitly Parallel Instruction Computing, вычисления с явным параллелизмом инструкций. Я применяю этот термин здесь именно в этом смысле, как аббревиатуру, а не как ссылку на архитектуру Itanium-ов. Явный параллелизм в MCp совсем другого рода.

О преимуществах MCp


Для затравки я скажу, что EPIC в MCp такой, что наделяет процессор рядом заманчивых (по крайней мере лично для меня) свойств.

  1. Хорошей энергоэффективностью. Которая обеспечивается тем, что MCp может:
    • согласовывать своё архитектурное состояние существенно реже, чем процессоры традицинных архитектур;
    • естественным образом совмещать в параллельном асинхронном исполнении инструкции работы с памятью, арифметические инструкции и предвыборку кода.
  2. Нестандартная организация ветвлений даёт интересные возможности реализации, так называемых, managed runtime, или (в классической терминологии) безопасных языков программирования.
  3. Функциональные языки можно положить на архитектуру MCp «естественней», чем на традиционные машины.
  4. Особенность кодирования программ (подразумевается машинный код) и особенность их исполнения позволяют сделать MCp постепенно деградирующим процессором. То есть, он может не ломаться целиком, при отказе функциональных устройств, а переходить в режим работы с другим распределением вычислений, с меньшей производительностью, естественно. В отличии от традиционных отказоустойчивых процессоров, у которых функциональные устройства (или сами процессоры целиком) просто троированы, MCp в штатном режиме, когда аппаратные ошибки не возникают, может эффективнее использовать свои вычислительные мощности.
  5. Ко всему этому MCp ещё относительно легко можно масштабировать (позволял бы техпроцесс) и запускать в режиме многопоточности (имеется в виду SMT — Simultaneous Multi Threading) да ещё и с динамическим разделением ресурсов между нитями.


Теперь я попробую объяснить, откуда эти свойства берутся, и откуда такое название «мултиклеточный», то есть, что такое «клетка». О загадочных цифрах в маркировке мне ничего не известно. Может быть, это ключ какого-нибудь «MultiClet»-квеста? :) Нет, я серьёзно не знаю.

Клетки


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

Любой современный CPU содержит набор неких функциональных устройств. Их можно разделить на несколько типов. Давайте разделим (для пояснения особенностей MCp мне не понадобятся уж очень детальное их описание, поэтому всё поверхностно).

  1. ALU: арифметико-логические устройства, в широком смысле. То есть, устройства, которые выполняют всевозможные преобразования над данными. У них есть входные порты, на которые подаётся код операции и операнды и выходные, на которых формируются результаты.
  2. LSU: устройства доступа в память (Load/Store Unit). Естественно, эта штука данные не преобразовывает, а записывает или считывает из памяти. У неё свои входные и выходные порты.
  3. RF: регистровый файл. Эти устройства сохраняют данные с некоторых шин (не совсем правильное название, но не суть) и выдают их на другие шины, ориентируясь на команды и значения на своих входных портах. Шины эти связаны с портами LSU или ALU. Часто говорят, что регистры — это такая быстрая внутренняя память процессора. Правильнее сказать, что RF — это такая очень эффективная внутренняя память процессора, а семантика регистров — это интерфейсы для доступа к нему. Потому что, существует Его Величество...
  4. CU: устройство контроля. Это устройство, которое управляет множеством ALU, LSU и RF (сейчас модно делать один общий RF на ядро, но не всегда было так; просто уточняю), контролируя передачу сигналов между ними, выполняя программу. В современных традиционных высокопроизводительных процессорах само CU очень сложное, и состоит из других компонент: планировщиков, предсказателей переходов, декодеров, очередей, буферов подтверждения и т.д. Но для целей этого рассказа мне удобнее считать всё это одним устройством. Точно так же я не раскладываю на сумматоры и сдвигатели ALU.


Можно сказать, что CU — это устройство, определяющее тип процессора. Почти во всех современных традиционных процессорах ALU, LSU и RF с функциональной точки зрения устроены примерно одинаково (если не вдаваться в тонкие детали реализации и не делать различий между векторными и скалярными ALU; да, согласен высказывание получилось условным). Всё многообразие моделей CPU, GPU, PPU, SPU и прочих xPU обеспечивается разницей в логике работы разных вариантов CU (и эта разница гораздо существенней, чем разница между векторными и скалярными ALU).

Это может быть логика простого стекового процессора, CU которого должен работать по тривиальному циклу. Прочитать из своего RF, состоящего из двух регистров IP (указатель на текущую инструкци) и SP (вершину цикла), оба регистра. Выставить на входные порты LSU код операции чтения и содержимое IP (скорее всего, CU в этом случае просто скоммутирует выход RF и вход LSU), получить ответ — код инструкции. Если, допустим, это код инструкции перехода, то CU должен выставить на портах LSU запрос на чтение значения из вершины стека, изменить SP на единичку, отправить это значение в RF, а на следующем такте скоммутировать выходной порт LSU с входным портом RF (на другой порт выставив значение, соответствующее записи в IP). Затем, повторить цикл. На самом деле, очень просто. Думается, что зря в наших профильных вузах не разрабатывают стековые процессоры в качестве упражнения.

Это может быть логика навороченного суперскалярного и многонитевого POWER8 с внеочередным исполнением, который за каждый такт выбирает по несколько инструкций, декодирует предыдущую выборку, переименовывает регистры, рулит огромным регистровым файлом (даже у i686 с его видимыми 16-ю регистрами, регистровые файлы могли быть размером в 128x64 битов), предсказывает ветвления и т.д. Такой процессор уже не сделаешь в виде домашнего задания.

Или это может быть тоже достаточно простой RISC-подобный CU, который в GPU раздаёт всем ALU, LSU и RF, упакованным в мультипроцессор, одну и ту же команду.

В современном высокопроизводительном CPU именно CU является самым сложным устройством, которое занимает большую часть чипа. Но это пока не важно. Главное, что во всех перечисленных случаях CU — одно, хоть и может при этом загружать работой и контролировать множество других функциональных устройств. Что можно эквивалентно сформулировать и так: в современных процессорах можно выполнять несколько потоков управления (цепочек инструкций) при помощи одного CU (SMT, например, Hyper Threading); но нельзя выполнять один поток управления при помощи нескольких CU.

Ага! Мой юный падаван (мы же все молоды духом и знаем, что ничего не знаем :) разгадка тайны Мультиклета близка. Естественно, сейчас я скажу, что дизайн мультиклеточного процессора таков, что он содержит в себе несколько CU, работающих по определённому протоколу и формирующих при этом некое распределённое CU, которое может исполнять один поток исполнения (одну нить, то есть, thread) на нескольких ядрах. Но сперва я скажу другое.

Так вот, клетка — это аналог ядра в привычном CPU. Она содержит своё CU, ALU (одно, но достаточно продвинутое даже в ранней версии процессора, способное выполнять операции с float[2] значениями, в том числе и операции комплексной арифметики; разрабатываемая сейчас версия будет поддерживать вычисления с double). Клетки могут иметь доступ к общим RF и LSU, или могут иметь свои собственные, которые могут работать в режиме зеркал или даже RAID-5 (если потребуется; помните, самое важное на данном этапе развития проекта слово — «отказоустойчивость»). И одно из самых приятных мест в архитектуре MCp — то, что хотя в таких режимах RF и будет работать заметно медленней, производительность MCp существенно это не снизит, так как основной обмен данными в ходе вычисления идёт не через RF и шунты (bypass), а через другое не являющееся памятью устройство — коммутатор.

Главной особенностью клеток является то, что их CU могут, работая по особому протоколу и с особым представлением программы, вместе составлять один распределённый CU, который может выполнять одну нить (в смысле, thread, в смысле, поток управления). И выполнять они эту нить могут в параллельном, асинхронном, совмещённом режиме, когда одновременно происходят: выборка инструкций, работа с памятью и RF (это очень няшным способом делается), арифметические преобразования, вычисление цели перехода (а от этого я лично вообще балдею, ибо pattern-matching из высокоуровневых языков на это отлично укладывается). И, что ещё более замечательно, эти CU получились намного более сильно существеннее :) реально проще, чем CU современных суперскалярных процессоров с внеочередным исполнением. Они тоже способны на такое параллельное исполнение программы (уточню: но не за счёт своей простоты и распределённости, а наоборот, за счёт своей сложности и централизованности, которые нужны для формирования особых знаний об исполняемой программе; подробнее в следующей части текста).

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

Энергоэффективность. MCp — параллельный процессор, способный исполнять 4 инструкции за такт, это совсем не плохо. И для этого ему не нужно сложное и большое (по своим размерам) центральное CU, он обходится сравнительно небольшими локальными для каждой клетки устройствами. Маленькие, значит потребляют меньше энергии. Локальные, значит, можно обойтись более короткими проводками для передачи сигналов, значит, меньше энергии будет рассеиваться, и выше частотный потенциал. Это всё +3 к энергоэффективности.

Отказоустойчивость. Если в традицонном процессоре погибает CU, то погибает весь процессор. Если в MCp погибает один из CU, то погибает одна из клеток. Но процесс вычисления может продолжаться на оставшихся клетках, хоть и медленней. Обычные процессоры, традиционно, троируют для обеспечения надёжности. То есть, ставят три процессора, которые выполняют одну и ту же программу. Если один из них начинает сбоить, это обнаруживается и его отключают. Архитектура MCp позволяет процессору работать в таком режиме самому по себе, и это можно контролировать программно: если необходимо, можно считать в высокопроизводительном режиме, когда надо, можно считать в режиме с перекрёстной проверкой результатов, не тратя на это дополнительные аппаратные ресурсы, которые тоже могут отказать. Возможны и другие режимы (пока, насколько мне известно, их не запатентовали, поэтому не буду распространяться).

Рождение нелинейности


Теперь я попытаюсь объяснить, почему такой распределённый CU возможен, что он действительно может быть простым, почему для этого нужен другой способ кодирования программы, и почему этот способ, предложенный авторами MCp, клёвый. Мне снова проще начать с описания традиционных (GPU и VLIW тоже считаются традиционными) архитектур.

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

cat test-habr.c && gcc -S test-habr.c && cat test-habr.s

typedef struct arrst Arrst;

struct arrst
{
	void * p;
	char a[27];
	unsigned x;
};

struct st2
{
	Arrst a[23];
	struct st2 * ptr;
};

struct st2 fn5(unsigned x, char y, int z, char w, double r, Arrst a, Arrst b)
{
	int la[27];
	char lb[27];
	double lc[4];
	struct st2 ld[1];

	return ((struct st2 *)b.p)[a.a[((Arrst *)b.p)->a[13]]].ptr->ptr->ptr[lb[10]];
}

	.file	"test-habr.c"
	.text
	.globl	fn5
	.type	fn5, @function
fn5:
.LFB0:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	subq	$1016, %rsp
	movq	%rdi, -1112(%rbp)
	movl	%esi, -1116(%rbp)
	movl	%edx, %eax
	movl	%ecx, -1124(%rbp)
	movl	%r8d, %edx
	movsd	%xmm0, -1136(%rbp)
	movb	%al, -1120(%rbp)
	movb	%dl, -1128(%rbp)
	movq	56(%rbp), %rdx
	movq	56(%rbp), %rax
	movzbl	21(%rax), %eax
	movsbl	%al, %eax
	cltq
	movzbl	24(%rbp,%rax), %eax
	movsbq	%al, %rax
	imulq	$928, %rax, %rax
	addq	%rdx, %rax
	movq	920(%rax), %rax
	movq	920(%rax), %rax
	movq	920(%rax), %rdx
	movzbl	-134(%rbp), %eax
	movsbq	%al, %rax
	imulq	$928, %rax, %rax
	leaq	(%rdx,%rax), %rcx
	movq	-1112(%rbp), %rax
	movq	%rax, %rdx
	movq	%rcx, %rsi
	movl	$116, %eax
	movq	%rdx, %rdi
	movq	%rax, %rcx
	rep movsq
	movq	-1112(%rbp), %rax
	leave
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE0:
	.size	fn5, .-fn5
	.ident	"GCC: (GNU) 4.7.2"
	.section	.note.GNU-stack,"",@progbits


Это традиционный ассемблер для регистровой машины. То, что такой код можно выполнить только при помощи одного CU, очень хорошо демонстрирует вот эта цепочка (напомню, что в ассемблере AT&T запись происходит в правый операнд):
	imulq	$928, %rax, %rax
	addq	%rdx, %rax
	movq	920(%rax), %rax
	movq	920(%rax), %rax
	movq	920(%rax), %rdx
	movzbl	-134(%rbp), %eax


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

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


Для ответа на эти вопросы нужно анализировать код согласно его последовательной семантике, когда следующая инструкция применяется к результатам работы всех предыдущих, и собирать информацию об этом анализе в одном месте. То есть, для этого нужна некая единая точка анализа, один CU. Всеми любимые суперскалярные с внеочередным исполнением Core iX, или AMD FX, или POWER8 умеют проводить такой анализ и планировать исполнение кода на его основе. CU этих процессоров способен на такие «чудеса», то есть, на разбор цепочки последовательного кода в независимые параллельные кусочки, и на сборку его обратно в последовательную цепочку завершения операций. Но ничто не получается само собой. Такие CU — самые затратные как по транзисторному бюджету, так и по энергопотреблению устройства в современных высокопроизводительных CPU. Это очень сложные схемы. Можно даже сказать, шедевральные. Кажется, за изобретение первой такой была присуждена премия Тьюринга.

Наверное, лучше один раз увидеть. Это фотография кристалла VIA Isaiah (не самый сложный процессор) с разметкой функциональных блоков. Всё, что не относится к кэшам, PLL, FP, IU, Load/Store — это схемы управления исполнением.

image

Именно поэтому ARM играет в big.LITTLE, потому что они не могут сделать одновременно и экономный, и производительный процессор, опираясь на традиционную архитектуру с регистрами. И именно поэтому, NVIDIA может стучать себя пяткой в грудь и сообщать, что в одном ядре их процессора в 4 раза больше арифметических устройств, чем в одном ядре Sandy Bridge. У SB остальное место занято схемами анализа и планирования исполнения потока линейно-упорядоченных инструкций.

В VLIW и в варианте EPIC от Intel и HP (Itanium) семантика инструкций точно такая же: они должны исполнятся друг за дружкой, изменяя архитектурное состояние процессора (грубо говоря, значения в регистрах). Да, инструкции у таких процессоров сложные и кодируют выполнение большого числа операций. Но необходимость в линейной упорядоченности этих сложных инструкций сохраняется. Поэтому CU в VLIW-процессорах и Itanium-ах не может быть распределённым. Конечно, в этих процессорах CU существенно проще, чем в процессорах с внеочередным исполнением инструкций. Но и у этих архитектур есть свои недостатки, которых нет в MCp. Например, для поддержания высокого темпа исполнения кода VLIW процессоры должны обладать сложными регистровыми файлами: на каждом такте в них одновременно может записываться аж до 6-ти значений (одна из моделей Itanium) одновременно и вычитываться, соответственно, 12. Есть проблема и при работе с памятью: если инструкция содержит операцию доступа к области памяти, ещё не отображённой в кэш (или другую быструю память), то весь VLIW процессор останавливается и ожидает завершения этой операции. В Itanium эту проблему пытались решить принудительными программными prefetch-ами, но получилось хорошо только для научных вычислений. У них структура доступа в память регулярна, предсказуема и выводится на этапе компиляции. На других же высокопроизводительных рынках царствуют (пока) либо RISC SMT-процессоры, либо процессоры с внеочередным исполнением, которые могут выполнять другую работу при задержках во время доступов к памяти.

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

В этом месте инженеры «Мультиклет» делают изящный финт ушами и ставят перед общественностью вопрос: а зачем нам привязываться к линейному порядку при кодировании программы? А потом ещё более изящно и гениально отвечают на него: а не за чем!

Параграфы и переходы


Ура! Вновь compilation time! Посмотрим на ту же самую программу детальками (нет, глаза мы к нему ещё не приделали) MCp.

cat test-habr.c && rcc -target=mcp < test-habr.c

typedef struct arrst Arrst;

struct arrst
{
	void * p;
	char a[27];
	unsigned x;
};

struct st2
{
	Arrst a[23];
	struct st2 * ptr;
};

struct st2 fn5(unsigned x, char y, int z, char w, double r, Arrst a, Arrst b)
{
	int la[27];
	char lb[27];
	double lc[4];
	struct st2 ld[1];

	return ((struct st2 *)b.p)[a.a[((Arrst *)b.p)->a[13]]].ptr->ptr->ptr[lb[10]];
}


Далее немного почищенный ассемблер: я убрал для удобства наши отладочные технические коментарии и директивы .local/.global (они сути не меняют). Директива .alias, играет роль #define. Она используется для повышения читаемости кода. Все неоптимальности и глупости компилятора сохранены. Он у нас ещё некоторое время будет на стадии beta-версии (полезной уже, тем не менее, для работы с процессором и выдающей корректный код). Поэтому не судите уж слишком строго. О различных техниках оптимизации мы знаем и будем их постепенно внедрять. А пока это действительно наивный и неоптимальный код, чего уж скрывать. Но мы же обсуждаем архитектуру самого процессора, а не компилятор.

.alias SP 39	; stack pointer
.alias BP 38	; function frame base pointer
.alias SI 37	; source address
.alias DI 36	; destination address
.alias CX 35	; counter

.text

fn5:
	.alias fn5.2.0C #BP,8
	.alias fn5.x.4C #BP,12
	.alias fn5.y.8C #BP,16
	.alias fn5.z.12C #BP,20
	.alias fn5.w.16C #BP,24
	.alias fn5.r.20C #BP,28
	.alias fn5.a.24C #BP,32
	.alias fn5.b.60C #BP,68

	.alias fn5.2.0A #BP,8
	.alias fn5.x.4A #BP,12
	.alias fn5.y.8A #BP,16
	.alias fn5.z.12A #BP,20
	.alias fn5.w.16A #BP,24
	.alias fn5.r.20A #BP,28
	.alias fn5.a.24A #BP,32
	.alias fn5.b.60A #BP,68

	.alias fn5.lb.27AD #BP,-27
	.alias fn5.1.32RT #BP,-32
	.alias fn5.2.36RT #BP,-36
	.alias fn5.3.40RT #BP,-40
	.alias fn5.4.44RT #BP,-44
	.alias fn5.5.48RT #BP,-48

	jmp	fn5.P0
	getl	#SP
	getl	#BP
	subl	@2, 4
	subl	@3, 56
	wrl	@3, @2
	setl	#SP, @2
	setl	#BP, @4
	complete

fn5.P0:
	jmp	fn5.P1
	rdsl	fn5.y.8C
	wrsb	@1, fn5.y.8A
	complete

fn5.P1:
	jmp	fn5.P2
	rdsl	fn5.w.16C
	wrsb	@1, fn5.w.16A
	complete

fn5.P2:
	jmp	fn5.P3
	getsl	0x340
	wrsl	@1, fn5.1.32RT
	complete

fn5.P3:
	jmp	fn5.P4
	rdsb	fn5.lb.27AD + 10
	rdsl	fn5.1.32RT
	mulsl	@1, @2
	wrsl	@1, fn5.2.36RT
	complete

fn5.P4:
	jmp	fn5.P5
	rdl	fn5.b.60A
	wrl	@1, fn5.3.40RT
	complete

fn5.P5:
	jmp	fn5.P6
	rdl	fn5.3.40RT
	addl	@1, 0x11
	rdsb	@1
	exa	fn5.a.24A + 4
	addl	@2, @1
	rdsb	@1
	rdsl	fn5.1.32RT
	mulsl	@1, @2
	wrsl	@1, fn5.4.44RT
	complete

fn5.P6:
	jmp	fn5.P7
	getsl	0x33c
	wrsl	@1, fn5.5.48RT
	complete

fn5.P7:
	jmp	fn5.P7.blkloop
	rdl	fn5.3.40RT
	rdsl	fn5.4.44RT
	rdsl	fn5.5.48RT
	addl	@2, @3
	addl	@1, @2
	rdsl	fn5.5.48RT
	rdl	@2
	addl	@1, @2
	rdsl	fn5.5.48RT
	rdl	@2
	addl	@1, @2
	rdl	@1
	rdsl	fn5.2.36RT
	addl	@1, @2
	rdl	fn5.2.0A

; Этот ужас - настройка на копирование структуры :)
	getl	0x0000ffff
	patch	@1, @3
	patch	@2, @3
	setq	#SI, @2
	setq	#DI, @2
	getl	0xfcc1ffff
	patch	@1, 0
	setq	#CX, @1

	getl	#MODR
	or	@1, 0x38
	setl	#MODR, @1
	complete

; само копирование, регистры с номерами CX, SI и DI меняются автоматически
fn5.P7.blkloop:
	exa	#CX
	jne	@1, fn5.P7.blkloop
	je	@2, fn5.P7.blkclean
	rdb	#SI
	wrb	@1, #DI
	complete

fn5.P7.blkclean:
	jmp	fn5.PF
	getl	#MODR
	and	@1, 0xffffffc7
	setl	#MODR, @1
	complete

fn5.1L:
fn5.PF:
	rdl	#BP, 4
	jmp	@1
	getl	#BP
	rdl	#BP, 0
	addl	@2, 4
	setl	#BP, @2
	setl	#SP, @2
	complete


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

Каждый параграф содержит некий набор инструкций. Инструкции бывают разными, некоторые из них ссылаются на регистры (отмечены #), но некоторые на значения, полученные в результате выполнения предыдущей инструкции. Ссылка имеет вид @N, где N — это расстояние до некоторой предыдущей по коду параграфа инструкции, на результат которой и ссылается такое выражение. Расстояние указывается в инструкциях и, естественно, отсчитывается назад, то есть снизу вверх по тексту от текущей команды.

То есть, фактически, при помощи @-ссылок в параграфе описывается граф потока данных некоторого участка программы. И инструкции, в основном, являются не операторами, которые действуют на предыдущее архитектурное состояние, а простыми операциями, которые нужно выполнить с одним или двумя значениями. Семантика у большинства инструкций MCp «легче» семантики традиционных инструкций. Архитектурное же состояние меняют только инструкции записи в регистры (setX) или записи в память (wrX).

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

Допустим, в процессоре функционирует N клеток. Тогда клетка с номером n, получив адрес следующего для выполнения параграфа начинает считывать команды, которые находятся в позициях N*k+n (k = 0, 1, ...) параграфа, пока не встретит флаг complete (ассемблер позаботится о том, что она его встретит). И начинает выполнять готовые к исполнению инструкции.

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

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

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

Вспомните солидный Load/Store/MOB модуль на диаграмме Isaiah. Так вот, большая его часть — это MOB, то есть Memory Ordering Buffer. Специальное устройство, которое помогает принимать решения о том, а можно ли одну операцию чтения/записи осуществить до (или после) другой. Этот анализ тоже непростой: нужно сравнивать адреса и зависимости. Поэтому схема большая.

Клетка же всегда говорит: ДА! Все выполняющиеся операции с памятью и RF можно в пределах параграфа переставлять как угодно. И это возможно, потому что у программиста и компилятора есть средство выстроить нужный порядок операций явно, при помощи @-ссылок и параграфов. Например, конструкция:
volatile a;
a += a;

транслируется в такой код (немного схематично):
	rdsl	a
	rdsl	a
	addsl	@1, @2
	wrsl	@1, a

запись в котором, выполнится строго после выполнения чтений, даже на не соблюдающих порядок обращения к памяти клетках. Таким образом, необходимый порядок работы с памятью можно задать без помощи сложного MOB, что приносит ещё +1 в копилку энергоэффективности MCp.

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

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

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

Эта особенность MCp уже даёт качественно иные возможности в кодотворчестве. Рассмотрим, например, программу с такой структурой:
	doSomething;
	if(condition)
	{
		doStuff;
	}


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

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

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

Итак, в MCp действительно есть распределённый CU, который образуют несколько взаимодействующих CU клеток. Действительно эти CU простые, и обеспечивают совмещённую параллельную обработку: выборки инструкций, арифметического счёта и операций доступа к памяти и RF.

Трудности роста клеток


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

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

Во-первых, к периферии, чтобы всё работало корректно, в большинстве случаев нужно обращаться в режиме запрета прерываний. Во-вторых, операции записи можно накапливать в специальном WriteBack-буффере, и начинать реальное их выполнение по завершении параграфа. Такая WB-очередь, конечно, ограниченный ресурс, но так как MCp хорошо переносит задержки при работе с памятью и RF (да, да, повторюсь, регистры в MCp могут быть относительно медленными, это, кстати ещё +1 к энергоэффективности), то это не будет большой проблемой. С такой очередью прерывания будут обрабатываться без дополнительных непредсказуемых задержек.

Вторая проблема — это устройство управления виртуальной памятью (MMU). С ним пока вообще ничего не понятно. С одной стороны, существуют традиционные OS, которые хочется: Linux, Plan9 :) С другой, вроде как MMU — это дорогое удовольствие. Считается, что на тестах SPEC на управление MMU тратится 17.5% процессорного времени; а SUN в порыве пропаганды Java на неких нагрузках насчитала аж 40%. С третьей стороны, зачем MMU в управляющих системах и супервычислениях (ближайшие цели для MCp)? Считаем же мы на CUDA без всякой виртуальной памяти. С четвёртой, при созерцании прогресса в области управляемых (безопасных) языков, при росте популярности Java, .Net, JavaScript, Go, возникает вопрос: стоит сосредоточится на поддержке таких языков?

С пятой стороны, из-за особенности обработки прерываний, когда точкой отката может быть только начало параграфа, могут появится проблемы. Допустим, кэш трансляций (TLB) в MMU будет рассчитан на 32 трансляции, а в параграфе будет 33 операции чтения из разных страниц памяти. Такой параграф невозможно будет выполнить. Здесь нужно как-то всё специально согласовывать и ограничивать. И т.д. В общем, процесс мозгового штурма этого куба в самом разгаре.

Обоснование преимуществ MCp


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

Энергоэффективностью я Вам уже надоел, наверное :) Но повторюсь, проистекает она из возможности параллельного исполнения кода с хорошим темпом его выборки при помощи простого распределённого CU. Кроме этого, архитектура позволяет ещё и выбирать сценарии работы. Допустим, например, что система приняла решение о том, что она может поработать в режиме малой производительности и с пониженным энергопотреблением. Тогда она может выбрать одну клетку, сообщить ей, что теперь для эта клетка должна выбирать N*k+n (k = 0, 1, ...) инструкции с N=1 и n=0, а остальные клетки отключить. После этого, всё тот же код будет выполняться одной клеткой. Profit? PROFIT! И никакого big.LITTLE не нужно.

Точно тем же механизмом может быть обеспечена постепенная деградация. Если при выполнении некоторого параграфа обнаружится, что какая-то клетка сломалась, то оставшиеся в живых клетки надо перепрограммировать, сменив у каждой N и n. После чего можно пробовать продолжить вычисление (которое, конечно же, должно начать рассылать всем SMS-ки о том, что беда! беда!).

В управляемых средах (managed runtimes) часто нужно проверять поведение кода по ходу его выполнения. Ибо не всегда можно гарантировать корректность программы одним лишь статическим анализом (теоретически, конечно, возможно, но на практике как компилятору проверить что некая зубодробительная физмат функция обладает определёнными свойствами?). Особенности передачи управления в архитектуре MCp позволяют такие проверки (например, выходов за границы массивов) и программирование передачи управления на обработчик исключительных ситуаций подмешивать в параграфы, выполняющие и полезные вычисления. В традиционных архитектурах так перемешивать код нельзя, потому что инструкция передачи управления должна стоять где-то непосредственно перед кодом, корректность которого проверяется.

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

fib :: (Integral t) => t -> t
fib 0 = 1
fib 1 = 1
fib n = fib (n - 1) + fib (n - 2)


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

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

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

Букв было много, спасибо за внимание!


Надеюсь, этот объёмный (уж простите, короче не вышло) текст поможет мне разделить с кем-нибудь то удовольствие, которое я получаю работая с этим процессором.
Tags:процессорывысокая производительностьязыки программированияМультиклетMCp
Hubs: High performance System Programming
+91
21.2k 98
Comments 180
Popular right now
Swift. Основы программирования
January 17, 202117,990 ₽Специалист.ру
Python для анализа данных
December 9, 202024,900 ₽SkillFactory
Профессия Data Scientist
December 9, 2020162,000 ₽SkillFactory
Специализация Data Science
December 9, 2020114,000 ₽SkillFactory
Top of the last 24 hours