Каким ты будешь, процессор?
Ну вроде бы ясно - будет
введена 64-разрядность, поддержка многопоточности и
параллельного выполнения кода, конвейеры будут
удлиняться. Однако все ли наследственные признаки
хороши, не несут ли они в себе последствий "родовой
травмы"? Рассмотрим их по порядку.
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 и иже с
ними.
Автор: Анатолий Ковалевский
Источник: www.listzone.ru
|