Информационные технологииStfw.Ru 🔍

Каким ты будешь, процессор?

Ну вроде бы ясно - будет введена 64-разрядность, поддержка многопоточности и параллельного выполнения кода, конвейеры будут удлиняться.
🕛 06.09.2006, 10:27
Однако все ли наследственные признаки хороши, не несут ли они в себе последствий "родовой травмы"? Рассмотрим их по порядку.
64-разрядность

А зачем она нужна? Зачем надо повышать разрядность процессора, ведь еще 486-й мог работать с 64-битными числами, а с добавлением в Pentium инструкций MMX процессоры приобрели возможность манипуляции и с 128 разрядными данными. Как ни странно, 64-битные процессоры работают... медленнее 32-битных. Да-да. 64-битные приложения занимают в ОЗУ гораздо больше места, а значит, и обрабатываются медленней. А если учесть то, что оперативная память по сравнению с кэшами процессора - ресурс медленный и конечный, по крайней мере, на современном этапе... Зачем же нас чуть ли не силком тащат в светлое 64-разрядное будущее, ведь придется переписывать не только все программы, но и операционные системы, и драйверы к ним?

Дело в том, что 64-битный процессор - это не только тот, который выполняет с числами данного разряда все базовые арифметические операции (а не только зашитые в набор дополнительных инструкций). Это еще и тот процессор, который способен этими числами "нумеровать" ячейки памяти. И фактически из-за памяти все и затевалось - чтобы перевалить через барьер в 4294967295 байт (4Гбайт). Но только барьер этот надо преодолеть не в ОЗУ, а в процессоре. Ведь все современные процессоры работают не с физической, а с виртуальной оперативной памятью, то есть программная адресация памяти в реальности не совпадает с действительным расположением ячеек в модулях памяти. Зачем же нужна разрядность 64 бита? А затем, чтобы в многозадачных ОС на логическом уровне можно было сохранить линейность и стройность данных, по-разному пронумеровав ячейки и раздав их разным приложениям. А на самом деле в памяти образуется каша из разных кусков программ и данных, лежащих вперемешку.

Впрочем, все затеивалось не только ради перенумерации ячеек, а ради возможности добавления различных атрибутов для разных ячеек - "только для чтения", "данных нет, загрузить их из свопа на винчестере" и пр. И что, 4 Гбайт не хватало? Увы, да. Во-первых, Windows без разговоров резервирует под свои нужды 2 Гбайт (даже если они ей нафиг не нужны). Во-вторых, некоторые программы некорректно освобождают после своей работы память - в результате в ОЗУ остаются "дырки", непригодные к использованию другими программами. В-третьих, программа может держать файл не в свопе на винчестере, а в ОЗУ - работа с такими данными ускоряется, но если это какая-нибудь база данных, то остальные программки резко переходят на положение "бедных родственников".

Но 64-битный режим - это не просто улучшенная версия 32-битного, а фактически следующая версия архитектуры процессоров x86. Наконец-то удалось удвоить количество регистров общего назначения (из которых собственно и состоит тело инструкции - скармливается процессору и говорит, что делать с данными) с 8 до 16 штук. Раньше в этом отношении i8086 ничем не отличался от Pentium 4/Athlon ХР, и было это не прихотью разработчиков, а фундаментальным ограничением самого набора инструкций x86 (каждая инструкция состояла из 4 частей - 2 трехбитных и 2 однобитных, отсюда и восьмерка). Затем исключили давно не используемые режимы типа имитации современным процессором древнего i8086 (режим Virtual 8086). Теоретически новые регистры добавлены так, чтобы сохранилась возможность выполнять 32-битный код, но на практике это не всегда оказывается реально выполнимым.
Конвейер

Даже очень древний процессор i8086 содержал своеобразный двухстадийный конвейер - выборка новых инструкций и их исполнение осуществлялись независимо друг от друга, что значительно ускоряло обработку инструкций. Плюс подъем тактовой частоты, а значит, еще большее ускорение исполнения кода. Хорошо? Замечательно. Однако конвейерное выполнение команд вносит свои проблемы: процессор делится на блоки, занимающиеся декодированием инструкций, и собственно блоки их исполнения. Поскольку время исполнения инструкций может сильно варьироваться, приходится создавать специальный механизм "диспетчеров", которые могли бы блокировать (или накапливать в специальном месте) выборку и декодирование новых блоков инструкций. Плюс когда в программном коде происходит разветвление (условный переход) и неизвестно, какая из веток кода будет правильной, - вычисляются обе, а потом "лишняя" сбрасывается. Дополнительно вводится блок предсказания переходов, который хранит в специальном кэше результаты ранее правильно вычисленных переходов, чтобы с большей вероятностью предсказать правильный путь. Появилась необходимость специального планировщика, который просматривает код, находит зависимые друг от друга участки...

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

А что же в реальности (если не рассматривать случаи оптимизированного кода)? У AMD в процессорах с архитектурой K8 за такт исполняется в среднем 3 инструкции. Исполнение каждой инструкции разбивается на 10-20 стадий - сначала идет выборка 3 инструкций, одновременно инструкции "тегируются" (расставляются пометки о том, в каком порядке они стоят) и отправляются в блоки декодирования - простые в простой (DirectPath), сложные - в сложный (VectorPath). Эти блоки, декодируя команды, заодно их еще и перетасовывают наиболее эффективным для исполнения образом. Далее декодированные во внутренние микроинструкции данные тройками внутреннего микрокода поступают к Instructions Control Unit, который накапливает данные в очередь для безостановочной поставки на конвейер. Происходит вычисление.

У Intel в процессорах с архитектурой NetBurst за такт исполняется в среднем 2 инструкции. Исполнение каждой разбивается на 20-30 стадий. Сначала инструкции попадают в декодер, который вынесен за пределы конвейера и работает на половинной частоте ядра (для его загрузки требуется дополнительно 15-30 тактов, то есть фактически конвейер в 2 раза длиннее, чем пишут в пресс-релизах). Тут еще на стадии копирования кода в кэш-память убираются безусловные и предсказываются условные переходы, а также выполняется множество других операций по преобразованию исходного кода во внутренний оптимизированный к исполнению микрокод. В результате в теории процессор может за такт исполнять до 4 инструкций, но стоит ему вылететь на незакэшированный участок, и удастся выполнить в лучшем случае одну.

А дальше работа конвейера - еще раз проверяется правильность выбора направления условного перехода, затем полученный внутренний микрокод по 6 инструкций (дважды по 3 инструкции за такт) складывается в очередь выборки, чтобы сгладить неравномерность поступления данных. Но из-за большой длины конвейера и латентности кэшей, подготовки/переименования регистров на это уходит 5-10 тактов. И только после этого тройки микроинструкций начинают поступать на выполнение - сначала к планировщикам, работу которых переупорядочивают так называемые диспетчеры (аж 7 штук). Их задача состоит в том, чтобы на исполняющее устройство одновременно пришли данные для обработки и связанная с ними микрооперация. А если не сложилось - тогда происходит replay - информация отправляется раз за разом в подобие специального кэша, пока данные и нужная для них операция не прибудут одновременно. В результате реплеи могут забить все время работы диспетчера, почти блокируя поступление новых данных. Более того, информация, циркулирующая в реплеях, может вообще зациклиться.

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

На серверном рынке, где как раз и распространены многопроцессорные системы, можно заметить поразительную картину. Если брать среднюю стоимость на один процессор (при равном объеме ОЗУ, одинаковой поддержке 32/64-разрядных приложений и т.д.), то выяснится, что процессор в истинной многопроцессорной системе стоит в 5-8 раз дороже, чем такой же его собрат в кластере (к тому же имеющий частоту, вдвое большую). Неужели такая высокая плата берется за престиж? И как еще такие монстры не вымерли в наш век бурного роста сетевых технологий и распределенных вычислений? Не вымрут - и для этого есть веские причины, которые объясняются большей производительностью многопоточных вычислений над параллельными. Поясню:

1. Симметричные многопроцессорные системы (SMP) - все процессоры объединены быстрой межпроцессорной шиной, общее для всех ОЗУ плюс крайне сложная и дорогая система софта и железа, заставляющая работать этого монстра как единое целое. Вдобавок головная боль с охлаждением - шкаф серверной стойки, нагруженный процессорами и ОЗУ (вроде BlueGene от IBM), греет воздух с производительностью тепловой пушки для обогрева складских помещений. Вычисления многопоточные, параллельными они становятся лишь вынужденно, и этого пути старательно избегают.

2. Кластеры - обычные ПК или серверы, поставленные рядком на металлическую стойку и соединенные между собой при помощи высокоскоростных интерфейсов (обычно это простые сетевые 10/100 Gigabit Ethernet-карточки, хотя есть и специализированные среды передачи данных - InfiniBand, Beowulf, Myrinet, Quadrics). Плюс компьютер, который координирует и раскидывает вычисления по разным кусочкам кластера, который должен быть связан с каждым узлом по топологии "звезда" и из-за высокой латентности при передаче данных является самым узким местом. Проблему пытаются решить путем создания узлов, где центральный компьютер связан только с несколькими, и уже эти избранные отдают его приказы дальше, образуя своеобразные узлы подчинения. Это позволяет одновременно передавать множество данных и сокращать время реакции системы на управляющие команды. Вычисления параллельные, лишь в образованных подузлах их удается сделать многопоточными.

3. Распределенные вычисления - используются обычные ПК, связанные обычными же локальными сетями. Наиболее ярким примером служат различные сети GRID-вычислений, когда расчеты ведутся только в то время, когда ПК не нужен его хозяину. Но все плюсы такого подхода уравновешиваются возникающими при этом минусами - компьютеры разнородные (как по железной, так и по программной составляющей), разная пропускная способность и нестабильность каналов, наличие "лишних" (выполняемых одновременно с расчетами) задач, непредсказуемое поведение элементов системы (пользователь может в любой момент отключить свой узел, поэтому одни и те же расчеты приходится дублировать на разных узлах). Вычисления только параллельные.

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

Своеобразный переворот в среде многопроцессорных вычислений готовит неугомонная AMD. Мало ей того, что процессоры Opteron все активнее вытесняют чипы Itanium 2 (по данным последнего Top500 самых производительных компьютеров, продукция Intel получила 46 мест вместо прежних 79, а AMD - 55 вместо 25).

Напомню, что два 2-процессорных сервера AMD могут работать независимо друг от друга или в виде единого 4-процессорного кластера именно благодаря грамотно реализованной шине HyperTransport. Эта шина, изначально заложенная в ядро процессора и позволяющая работать с ОЗУ любого процессора как с общей памятью, обеспечивает фактически столь же высокую производительность, что и SMP-системы, и именно это дало AMD возможность существенно потеснить Intel на серверном рынке. Однако AMD на этом не остановилась - к выпуску готовится чип Chorus, который будет соединять от 2 до 4 процессоров по шине HyperTransport и к трем другим компьютерам по межпроцессорной шине типа InfiniBand. В результате (напомню, ОЗУ - общее) мы получаем производительность истинной SMP-системы при гораздо более легком монтаже и незначительном ценовом бремени.
Многопоточность

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

И что же здесь сложного? Ведь есть специальные библиотеки (вроде OpenMP.dll), которые на этапе компиляции кода расставляют фактически в автоматическом режиме метки распараллеливания кода для процессора -потом на отладчике (Intel Thread Checker) в визуальном режиме можно посмотреть, где куски кода конфликтуют за одни и те же ресурсы ядер процессора, - и многопоточный код готов. Но... Кто ж знал, что на дороге к светлому будущему быстрого исполнения кода столько оврагов?

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

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

Овраг 3. Если компилятором генерируется код, в котором будут одновременно исполняться два потока, то они могут выполнять половину общего объема одного задания (рассчитать растекание капли по оконному стеклу) или обрабатывать данные, связанные с разными объектами (один поток считает растекание капли, а второй - разрушение стекла камнем). Вот в последнем случае и начинаются главные трудности: если все происходит действительно одновременно, то что будет происходить с каплей, зависит только от того, кто последний записывал данные в память. Если раньше просчитается физика разрушения стекла - значит, программа получит приказ рассчитывать пролет капли и растекание ее уже на каком-то осколке. А вот если раньше будет рассчитано растекание, то когда программа попытается рассчитать дальнейшее движение капли по уже несуществующему стеклу... результатом обычно является сообщение об ошибке с фатальном вылетом программы (иногда вместе с ОС). Для защиты от подобной нелепицы вводятся специальные объекты синхронизации, которые блокируют изменение объекта двумя потоками одновременно. В результате исчезает то, за что мы боролись - многопоточность. Ведь "вся власть" отдается одному потоку, а другие желающие получить объект "в расчет" стоят в очередь и ждут, пока нужный объект не освободится.

Мало того, роется новая яма, так называемый dead-lock. В описанном выше случае он выглядит так: камень попадает в стекло, и программа определяет, что она должна разбить стекло и размазанную по нему каплю воды. Но капля не дает изменить стекло - она ждет, когда камень изменит стекло, чтобы она могла размазаться не по всему стеклу, а по одному только осколку, и "не отдает" стекло на изменение. Снова вылет программы с ошибкой. А если при этом поток капли (или камня) "забыл" снять блокировку с изменяемого им объекта? Представляете себе процесс отладки такого приложения? А теперь вообразите, что будет, если действие одного потока сопровождается десятком действий другого? А если события не четко детерминированы и проявляются крайне редко?
Так каким же должен быть процессор?

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

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

И этим путем уже шли. Процессор Advanced RISC Machines версия 2 (или ARM2) имел 30 тыс. транзисторов, оперировал 32 разрядами и был более производителен, чем i286, имевший 120 тыс. транзисторов и оперировавший вдвое меньшим количеством разрядов. Но ему и его потомкам достался лишь рынок бытовых устройств и наладонников.

А кто этим путем идет сейчас? Давайте-ка оглянемся. Вышедшая Xbox 360 от Microsoft базируется на PowerPC от IBM (трехядерный, 64-разрядный, 3,2 ГГц, общий кэш L2 1 Мбайт, 165 млн транзисторов, техпроцесс 90 нм). Sony с PlayStation3 обещают выйти в январе, а Nintendo Revolution в мае - но и они основаны на процессорах Голубого Гиганта.

Sun представила восьмиядерный процессор UltraSPARC T1 (ранее Niagara) с очень низким энергопотреблением, но это лишь вариация на тему чипов Athlon и Pentium. А вот Cell (ранее Broadband Processor) - это попытка пересмотреть существующие парадигмы программирования в сторону полной абстракции данных. Здесь нет данных, нет программ, нет процессоров - есть только код в виде данных или код, который их обрабатывает (программный или абстрактно аппаратный).

В результате все написанные программы получаются параллельными по самой своей сути. Мало того, такая система крайне легко масштабируется вплоть до суперкомпьютера. При этом ячейки-процессоры Cell могут быть встроены не только в материнскую плату ПК, но и в любую бытовую технику (главное, чтобы все элементы были взаимосвязаны). Cell - девятиядерный процессор, 8 ядер которого - это на самом деле 286-е процессоры с кусочком собственной ОЗУ, выполненные в современных технологических нормах и разогнанные до частоты в 3,2 ГГц плюс 1 PowerPC-подобное ядро, которое всем этим оркестром и дирижирует.

Что же мешает наступлению этого радужного будущего? Как всегда, грустное настоящее - отсутствие ОС, доступных компиляторов, средств разработки и минимальное количество перенесенного на новую среду ПО. И не только - продукция IBM для реализации своего потенциала требует соответствующих комплектующих. Например, для Xbox 360 поставляются GDDR3-память 512 Мбайт, 700 МГц (самый дорогой компонент приставки), а для двухпроцессорных серверов на базе IBM Cell используется память Rambus eXtreme Data Rate. А это требует денег. И, главное, при таком подходе необходима грамотная и очень тщательная оптимизация кода, когда распараллеливанием занимается не программа в автоматическом режиме, а программист. Причем программист, который "дружит" со своей головой, а не только с мышкой и клавиатурой.

Кто-то может возразить, что переход компьютеров Macintosh на процессоры Intel связан с тем, что у IBM нет высокопроизводительных процессоров. Дело не этом, а в отсутствии у IBM системы "принудительного" управления цифровыми правами типа Palladium и иже с ними.

Компьютеры и периферия   Теги: Процессор

Читать IT-новости в Telegram
Информационные технологии
Мы в соцсетях ✉