Форум программистов, компьютерный форум, киберфорум
Наши страницы
Evg
Войти
Регистрация
Восстановить пароль
Рейтинг: 4.50. Голосов: 8.

Базовые принципы запуска пользовательских задач в операционных системах

Запись от Evg размещена 22.10.2017 в 16:09
Обновил(-а) Evg 19.05.2018 в 18:30

Из-за технических ограничений форумного движка статью про hyper-threading пришлось превратить в цикл из четырёх статей. Данная статья является первой статьёй цикла. Четвёртая статья пока отсутствует





  • 1. Предисловие
  • 2. Минимальные вводные данные
    2.1 Терминология
    2.2. Классификация пользовательских задач
    2.3. Железо
  • 3. Работа одноядерных систем
    3.1. Одна счётная задача на одноядерной системе
    3.2. Одна файловая задача на одноядерной системе
    3.3. Несколько счётных задач на одноядерной системе
    3.4. Несколько файловых задач на одноядерной системе
    3.5. Спящие задачи на одноядерной системе
  • 4. Работа многоядерных систем
  • 5. Программный поток
  • 6. Что понимается под загрузкой процессора в диспетчере задач
  • 7. Сравнение производительности процессоров
    7.1. Производительность в длину и в ширину
    7.2. Некоторые неочевидные моменты







1. Предисловие

Изначальная идея состояла в описании hyperthreading'а. Для для его понимания необходимо понимать базовые основы работы операционных систем, потому как hyperthreding не висит в воздухе, он работает совместно с операционной системой. В итоге описание базовой части получилось увесистым, поэтому из-за ограничений форума на размер одной статьи пришлось его вынести в отдельную статью. Таким образом, данная статья по смыслу является вводной частью к статье про hyperthreading (вторая статья цикла), но её можно рассматривать и как самостоятельную - вне контекста hyper-threading'а статья является логически завершённой

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

2. Минимальные вводные данные

2.1. Терминология

В английском языке есть два разных термина, которые на русском языке переводятся одним и тем же словом "ядро": "kernel" - ядро операционной системы, "core" - ядро процессора. Чтобы не путаться, словом "ядро" я буду назвать процессорное ядро, а словом "ОС" (Операционная Система) буду называть ядро операционной системы

В контексте ОС любая запускаемая программа, пользовательская или системная, называют словом "user task", что обычно переводят как "пользовательская задача". Дело в том, что в контексте ОС всё то, что к ОС не относится, считается user'ами. В том числе и все службы, которые мы называем словами "системные службы". В контексте данной статьи словом "пользователь" я буду называть человека, а словом "пользовательские задачи" - программы, запускаемые человеком. Целью статьи является пояснение базовых принципов, а потому здесь совершенно не интересны всякие системные службы и прочие программы. Для простоты будем считать, что на машине установлена простая операционная система (ОС), есть человек (пользователь), который на ней запускает свои программы (задачи). Я не хочу пользоваться термином "программа", потому что для программистов он означает скорее исходные тексты программы (на языке программирования или на ассемблере), чем исполняемый файл

Итого вкратце:
- "ОС" - ядро операционной системы
- "ядро" - процессорное ядро
- "пользователь" - человек
- "задача" - запускаемая человеком программа

2.2. Классификация пользовательских задач

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

Сия классификация НЕ является чем-то общепринятым в мире. Я её ввёл в рамках отдельной статьи исключительно с целью сокращения количества писанины. Висящая в воздухе классификация сама по себе вряд ли будет понятна. Она более-менее раскроется в процессе дальнейшего описания. Конкретные примеры будут приведены в разделе 5

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

2.3. Железо

Внутри каждого процессорного ядра есть специальный таймер, который активируется (выдаёт аппаратное прерывание) порядка 100 раз в секунду. Возможно, точное значение и не такое, но принципиальной роли это не играет. Важно то, что делается это много раз в секунду

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

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

3. Работа одноядерных систем

3.1. Одна счётная задача на одноядерной системе

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


Рисунок 1

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

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


Рисунок 2

Моменты возникновения "прочих" прерываний от аппаратуры обозначены как HW. Таким образом происходит видимость того, что на компьютере одновременно исполняется пользовательская задача и происходит обработка прерываний от всех устройств, хотя в реальности всё это выполняется по очереди. Пользовательская задача в процессе своего исполнения даже не знает о том, что её постоянно прерывают, поскольку ОС в момент постановки задачи на процессор приводит состояние всех регистров и памяти в то же самое состояние, которое было на момент возникновения прерывания (перед уходом в коды ОС)

3.2. Одна файловая задача на одноядерной системе

Управление в ОС может передать и сама пользовательская задача. Например, когда ей нужно выполнить какое-то обращение к аппаратуре. Например, чтение данных из файла. В этом случае пользовательская задача исполняет специальные инструкции процессора, которые приводят к передаче управления в ОС. Это называется системный вызов (system call). При получении системного вызова ОС понимает, что пользовательская задача попросила выдать кусок данных из файла, после чего выдаёт соответствующий запрос диску, на котором расположен нужный файл. Теперь вроде бы пришла пора возвращать управление пользовательской задаче. Но задача предполагает, что после исполнения системного вызова ей будут доступны прочитанные данные с диска. А у нас этих данных пока ещё нет, поскольку по процессорным меркам диск является экстремально медленным устройством, от которого можно ждать ответа миллионы и миллиарды тактов. Т.е. пользовательскую программу пока выполнять нельзя. В этом случае в ОС запускается так называемый цикл холостого хода. Что он из себя представляет, я более подробно расскажу во второй статье цикла, потому что там это важно, а пока в первом приближении можно считать, что процессор просто исполняет бесполезные операции-пустышки в цикле, потребляя при этом минимальное количество электричества. Процессор болтается в цикле холостого хода до тех пор, пока не придёт какое-нибудь прерывание от аппаратуры. Первым прерыванием скорее всего будет прерывание от таймера, т.к. с большой вероятностью за 0.01 секунды диск ещё не успеет ничего прочитать. В этом случае ОС перехватит прерывание, поймёт, что единственную задачу пока нет смысла ставить на исполнение, т.к. она находится в состоянии ожидания данных от диска, после чего возобновится цикл холостого хода. И так будет продолжаться до тех пор, пока от диска не придёт сигнал готовности того, что данные прочитаны, а так же будет сформирован сам блок с прочитанными данными. В процессе обработки прерывания от диска ОС поймёт, что данные уже готовы и уже можно будет передать управление нашей задаче. Схематично это можно изобразить так:


Рисунок 3

Момент системного вызова обозначен буквой S. Идущая рядом с ней чёрная область соответствует работе ОС по передаче команды чтения на диск. Белым цветом на графике изображён цикл холостого хода ОС. Момент возникновения прерывания от диска обозначен буквой D. Идущая рядом с ней чёрная область соответствует работе ОС по чтению данных из буферной памяти диска

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

3.3. Несколько счётных задач на одноядерной системе

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


Рисунок 4

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

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

В нашем простом случае процессорное время делилось между задачами поровну. В современных операционных системах есть такое понятие, как приоритет исполнения задачи. На уровне операционной системы это сводится к тому, что процессорное время делится между задачами НЕ поровну: больше времени достаётся задачам с высоким приоритетом и меньше времени достаётся задачам с низким приоритетом. В предыдущем примере мы исходили того, что процессор на каждую задачу отводит интервал времени в 2 прерывания таймера. В случае, когда имеется две задачи, одна из которых с высоким приоритетом, ОС могла бы поделить процессорное время между задачами в пропорции, например, 3:1. На диаграмме это выглядело бы следующим образом:


Рисунок 5

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

3.4. Несколько файловых задач на одноядерной системе

Теперь включим в эту схему задачи, которые, например, работают с файлами. Здесь уже есть небольшое, но важное отличие от случая с чисто счётными задачами. Как только, например, задача N1 выполнила системный вызов для чтения данных из файла, ОС отправит запрос на диск, после чего НЕ будет возвращать на процессор задачу N1, поскольку данные с диска ещё не готовы. Но, в отличие от варианта с одной задачей, здесь уже не запустится цикл холостого хода, поскольку у нас есть задача N2, которую ОС и поставит исполняться на процессоре. Если в какой-то момент времени задаче N2 так же потребуются данные с диска, то ОС снимет её с процессора, отправит запрос на диск, а на исполнение поставит задачу N3 и т.д. И только в том случае, когда все исполняемые задачи окажутся в режиме ожидания данных от диска, случится ситуация, когда исполнять нечего, и только тогда запустится цикл холостого хода. На диаграмме это будет выглядеть следующим образом:


Рисунок 6

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

3.5. Спящие задачи на одноядерной системе

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

В момент запуска происходит прорисовка диалогового окна с чатом, и это занимает какое-то время у процессора. После этого задача переходит в ждущий режим - она ничего не рисует, ничего по сети не посылает, никакие вычисления не проводит. Она просто ждёт, пока случится какое-нибудь событие, которое по своей сути связано с аппаратурой - нажатие клавиши на клавиатуре, нажатие кнопки мыши, появление сообщения из сети и т.п. Режим ожидания выражается в то, что в коде программы выполняется системный вызов, который сообщает ОС, что наша программа ничего не будет делать до тех пор, пока не придёт какое-нибудь ожидаемое ею событие. ОС снимает задачу с процессора. Если никаких других активных задач нет, то ОС запускает цикл холостого хода. Если нажать на клавишу на клавиатуре, то клавиатура выдаст аппаратное прерывание, ОС это прерывание перехватит и поймёт, что его ожидал наш чат-клиент. ОС поставит нашу задачу на исполнение. Чат-клиент обработает событие от клавиатуры: отобразит букву на экране в окне чата или отправит сообщение по сети, может быть, что-то ещё. После чего чат-клиент опять переходит в режим ожидания до появления какого-нибудь события. Следующим событием, например, будет появление данных из сети. ОС перехватит прерывание от сетевой карты, поймёт, что данные нужно отправить нашему чат-клиенту и поставит его на процессор. Чат-клиент обработает событие от сети, отобразит пришедший текст на экране, после чего опять перейдёт в ждущий режим. И так далее по кругу. На диаграмме это будет выглядеть примерно так:


Рисунок 7

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


Рисунок 8

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


Рисунок 9

4. Работа многоядерных систем

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

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


Рисунок 10

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

Но если у нас имеется две задачи, то здесь уже ситуация будет иная:


Рисунок 11

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

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

Например, для трёх запущенных задач диаграмма может быть такой:


Рисунок 12

В нашем случае три счётные задачи исполняются одновременно с точки зрения пользователя. Но с точки зрения процессора одновременно исполняются только две задачи. ОС постоянно распределяет задачи по ядрам таким образом, чтобы их исполнение во времени шло примерно одинаково. Таким образом у пользователя будет создаваться иллюзия одновременного и равномерного исполнения трёх задач

А ещё диаграмма может быть такой:


Рисунок 13

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

5. Программный поток

Пока мы рассматривали только отдельно взятые независимые задачи. Но многие программы устроены таким образом, что в процессе свое работы они как бы порождают несколько независимых друг от друга задач. Такие задачи внутри задачи принято называть термином "программный поток" (thread). Сразу же хочется заметить, что программный поток, о котором тут пойдёт речь, НЕ имеет отношения к слову "поток" из маркетинговых терминов типа "4 ядра/8 потоков" (4 cores/8 threads), с которыми почти все сталкивались. Здесь просто одна задача ведёт себя как несколько независимых задач. Такие задачи называют "многопоточные задачи". С точки зрения ОС несколько потоков одной задачи ничем принципиально не отличаются от нескольких разных задач. Все рассмотренные ранее примеры, в которых фигурировали несколько отдельных задач, ведут себя ровно таким же образом, как и одна многопоточная задача. Т.е. в контексте нашей статьи для ОС нет никакой разницы, что именно она раскидывает по ядрам - разные задачи или разные потоки одной задачи. По большому счёту это одно и то же

Есть ещё один важный момент, который нужно понимать. Потоки - это сущность, которую должен явно запрограммировать человек (программист, автор задачи). Программист должен сам приложить усилия к тому, чтобы найти внутри задачи отдельные независимые подзадачи и явным образом их запрограммировать в виде отдельных потоков. А дальше уже начинается работа ОС, которая раскидает потоки по процессорным ядрам, обеспечив по возможности их параллельное исполнение. Программист, к примеру, может свою задачу реализовать в 4 потока. А дальше эту задачу можно запускать на процессоре с любым количеством ядер. Если 4-поточную задачу запускать на 1-ядерном процессоре, то эффекта от многопоточности не будет никакого - все потоки будут псевдопараллельно работать на одном ядре, не давая никакого прироста в скорости работы. Но если ядер больше, чем одно, то ОС раскидает потоки по разным ядрам, и это приведёт к ускорению работы всей программы в целом. Т.е. вычисления будут проведены за меньшее время по сравнению с вариантом, когда программист не стал бы строить программу как многопоточную. Здесь так же важно понимать, что если есть "медленная" задача, реализованная в один поток, то покупка многоядерного процессора эту задачу никак не ускорит. Просто потому, что задача сама собой не разобьётся на потоки, это должен явным образом выполнить программист

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

Программы типа 3d-studio или видеокодеров устроены таким образом, что умеют внутри себя эффективно создавать произвольное количество потоков. Обычно они выясняют, сколько ядер имеется в системе, и в режиме рендеринга создают такое же количество потоков. В режиме моделирования/предпросмотра столько вычислительных мощностей не нужно и программы работают в один поток. Аналогичным образом устроены программы типа photoshop'а - непосредственно в процессе работы преобразований изображения создаётся произвольное количество потоков, но в процессе непосредственно редактирования потоки не нужны. Такой класс приложений ведёт себя как однопоточная ждущая задача в те моменты, пока человек что-то редактирует, и как многопоточная счётная задача с условно бесконечной возможностью распараллеливания в те моменты, когда запускается процесс рендеринга/обработки

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

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

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

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

6. Что понимается под загрузкой процессора в диспетчере задач

Программы мониторинга загрузки процессора обычно выдают результат в виде процентов, но не всем и не всегда понятно, от чего вычисляются проценты. Эти проценты обозначают загрузку процессора во времени. Берётся интервал времени, например, предыдущая одна секунда, и показывается, какую долю этого времени процессор был занят исполнением полезной нагрузки. Загрузка в 100% на одноядерном процессоре соответствует тому, что на интервале времени в одну секунду на наших диаграммах не было ни одной белой зоны. Загрузка в 0% соответствует тому, что на интервале времени в одну секунду были только белые зоны. Загрузка в 30% на одноядерном процессоре означает, что на интервале времени в одну секунду на диаграммах суммарная длина всех цветных и чёрных участков составляет 0.3 секунды

На многоядерных системах за 100% времени обычно считают время всех ядер. Т.е. загрузка в 100% означает, что на всех ядрах в течение 1 секунды не было ни одной белой зоны. Если на двухъядерной системе в течение одной секунды одно ядро было постоянно загружено, а второе ядро постоянно находилось в цикле холостого хода, то это соответствует загрузке в 50%. Это в каком-то смысле символизирует, что процессор во времени был загружен на 50% от теоретически возможного. Т.е. "время" здесь употребляется не в физическом смысле, а в смысле "помноженное на количество ядер". Видя показатель загрузки в 50% на двухядерной системе, нельзя достоверно сказать, как загрузка распределена по ядрам. Это может быть и вариант, когда одно ядро загружено полностью, а второе ничего не делает. Но это может быть и вариант, когда на обоих ядрах работают файловые задачи, которые постоянно впадают в режим ожидания данных от диска (т.е. то работают, то ожидают)

Также важно понимать, что глядя на эти самые проценты в принципе невозможно оценить загрузку реальных мощностей процессора. Загрузить процессор на 100% (во времени) может задача, в которой вертится пустой цикл, который ничего не делает. В этом случае в процессоре будет задействовано очень мало исполнительных устройств, процессор будет работать с низким энергопотреблением и с низкой температурой. На те же самые 100% (во времени) процессор может загрузить и какая-нибудь счётная задача, активно работающая с векторными инструкциями процессора (SIMD). В этом случае уже будут задействованы чуть ли не все исполнительные устройства, процессор будет работать на максимальных значениях потребления электричества и температуры. Поэтому нельзя придавать большое значение словам, если кто-то вам рассказывает про то, что при 100% загрузке процессора он нагревается всего до 50 градусов, а следовательно, очень хорошо работает система охлаждения. Какие-то выводы можно делать только тогда, когда известно, чем конкретно загружен процессор

7. Сравнение производительности процессоров

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

7.1. Производительность в длину и в ширину

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

Для изготовления такого процессора можно пойти двумя путями. Первый путь заключается в том, чтобы выпустить процессор с одним ядром, которое в два раза быстрее первоначального (имеет производительность в два попугая). При таком раскладе один поток будет исполняться за 5 секунд, а два потока в режиме псевдопараллельного исполнения выполнятся за 10 секунд. Этот вариант я буду называть термином "одно ядро в два попугая". Второй путь заключается в том, что процессорное ядро оставить без изменений, но в процессор поместить два ядра. Итоговая производительность такого процессора равна двум попугаям (два ядра по одному попугаю). Два потока, запущенные параллельно на двух ядрах, отработают за 10 секунд. Этот вариант я буду называть термином "два ядра по одному попугаю". Таким образом мы имеем два процессора с заявленной производительностью в два попугая, но устроенных по разному. Который из процессоров лучше?

Как мы уже убедились, оба процессора в состоянии выполнить два потока, затратив на это 10 секунд времени. Но что будет в случае, когда у нас потоков не два, а только один? В этом случае процессор "одно ядро в два попугая" проведёт вычисления за 5 секунд, поскольку это ядро работает в два раза быстрее первоначального. При этом процессор "два ядра по одному попугаю" проведёт вычисления за те же самые 10 секунд, просто из двух ядер будет работать только одно, а второе будет заниматься бесполезным циклом холостого хода

В итоге мы приходим к интересному выводу: процессор "одно ядро в два попугая" в общем случае более выгоден, чем процессор "два ядра по одному попугаю". В двухпоточном (трёхпоточном и т.п.) приложении процессоры покажут одинаковую производительность, но в однопоточном приложении процессор "одно ядро в два попугая" покажет более высокую производительность. Точно так же процессор с двумя ядрами с производительностью в два попугая в общем случае более выгоден, чем процессор с четырьмя ядрами с производительностью в одного попугая

В реальной жизни выбор между двумя вариантами обычно выглядит немного по другому. Допустим, у нас есть первый процессор, на котором 4 ядра с производительностью по 10 попугаев каждое, и есть второй процессор, на котором 6 ядер с производительностью по 8 попугаев. Итоговая производительность первого процессора равна 40 попугаям, а второго - 48 попугаям. Какой же выбрать процессор? Ответ неожиданный - это зависит от того, какие будут решаться задачи. Если мы знаем, что у нас будет одновременно исполняться не более 4 потоков, то нам нужен 4-ядерный процессор с более быстрыми ядрами. Если у нас бывают ситуации, когда нужно исполнять 5 потоков, то с виду производительность будет одинаковая. 4-ядерный процессор (40 попугаев), поделённый в режиме псевдопараллельного исполнения на 5 потоков будет раздавать по 8 (40 / 5) попугаев на каждый поток. Ту же производительность будет выдавать и 6-ядерный процессор, где 5 потоков будут работать чисто параллельно на 5 ядрах, получая таким образом те же 8 попугаев на поток (оставшиеся 8 попугаев от последнего ядра, будут болтаться в цикле холостого хода). Как мы уже показали выше, при равной итоговой производительности в общем случае более выгодным оказывается вариант с более быстрыми ядрами, поэтому при 5 потоках по прежнему мы выбираем 4-ядерный процессор. Но при 6 потоках получится так, что на 4-ядерном процессоре в режиме псевдопараллельного исполнения будем иметь 6.66 (40 / 6) попугая на один поток против 8 попугаев на 6-ядерном процессоре. И в этом случае более выгодным вариантом окажется 6-ядерный процессор. При большем количестве потоков 6-ядерный процессор по прежнему будет более выгоден, т.к. обладает бОльшим суммарным количеством попугаев

Есть ещё один интересный вывод. Если у вас есть 2-ядерный процессор, на котором исполняется 2-поточная задача, а производительности этого процессора вам не хватает, то покупка 4-ядерного процессора (с такой же производительностью одного ядра) проблему НЕ решит. Несмотря на то, что в характеристиках процессора указано в два раза больше попугаев, ваша 2-поточная задача от этого не заработает быстрее. Не дайте маркетологам обмануть вас. Если есть 2-ядерный процессор с производительностью в 20 попугаев и 4-ядерный процессор с производительностью в 40 попугаев, то это НЕ означает, что ваша задача на 4-ядерном процессоре будет исполняться в два раза быстрее, чем на 2-ядерном. Это всего лишь означает, что задача может исполняться в два раза быстрее, но только при условии, что она будут реализована в 4 и больше потоков. Только при таком условии из 4-ядерного процессора процессора можно будет выжать 40 попугаев

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

7.2. Некоторые неочевидные моменты

На отдельно взятой однопоточной задаче может наблюдаться такой эффект, что при запуске на 4-ядерном процессоре она работает быстрее, чем при запуске на 2-ядерном процессоре с таким же ядром и такой же частотой. С виду это противоречит написанному в предыдущем разделе. Но хитрость тут кроется в том, что 2- и 4-ядерные процессоры отличаются не только количеством ядер, но и количеством кэша - чем больше ядер, тем больше требуется кэша для сбаллансированной работы. Если кэш устроен таким образом, что он целиком доступен всем ядрам (как это имеет место быть на Intel'овских процессорах с кэшем L3), то мы получим, что задача работает в разных условиях. В обоих запусках используется одно и то же ядро, но в распоряжении имеется в два раза различающееся количество кэша. Это в конечном итоге и приводит к разной скорости работы, причём на отдельно взятых задачах разница может оказаться приличной

На процессорах от AMD кэш устроен по другому - каждое ядро имеет свой собственный кэш, не доступный другим ядрам. Поэтому такого эффекта там не будет. Но тут появляется другой интересный эффект. Допустим, запуск однопоточного приложения на 4-ядерном Intel'овском процессоре показал по замерам производительность в 10 попугаев. Из этого хочется сделать вывод о том, что одновременный запуск четырёх таких же потоков покажет производительность в 40 попугаев. Но в реальности всё может быть и не так из-за того, что каждому потоку теперь достанется меньше кэша, что в конечном итоге может привести к реальной производительности, например, в 38 попугаев. В случае процессора AMD такого эффекта уже не будет. Такая разница в устройстве кэша может привести к неправильной оценке сравнения производительности одного ядра на процессорах от Intel и AMD

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

Ещё одним неочевидным фактором является то, что один и тот же процессор на различных задачах может вести себя совершенно по разному. Например, Процессор-1 может показывать на Задаче-П производительность в 10 попугаев, а Процессор-2 на этой же задаче может показывать производительность в 11 попугаев. Из этого вроде бы можно сделать вывод о том, что Процессор-2 на 10% производительнее. Однако, если мы теперь проведём замеры на Задаче-М, то можем обнаружить, что Процессор-1 показывает производительность в 20 мартышек, а Процессор-2 на этой же задаче показывает производительность лишь в 19 мартышек, т.е. оказывается медленнее, чем Процессор-1 (и результат с виду противоречит результату, полученному на Задаче-П). Но так устроены современные процессоры - их производительность зависит от конкретных задач. Поэтому если вы выбираете процессор для работы с видеомонтажом, то нужно смотреть результаты замеров именно на видеомонтажных программах. Выбор процессора для видеомонтажа по результатам замеров в играх может оказаться ошибочным

Нажмите на изображение для увеличения
Название: npic03.gif
Просмотров: 568
Размер:	9.2 Кб
ID:	4387Нажмите на изображение для увеличения
Название: npic04.gif
Просмотров: 529
Размер:	7.3 Кб
ID:	4388Нажмите на изображение для увеличения
Название: npic05.gif
Просмотров: 585
Размер:	7.3 Кб
ID:	4389Нажмите на изображение для увеличения
Название: npic01.gif
Просмотров: 536
Размер:	5.9 Кб
ID:	4390Нажмите на изображение для увеличения
Название: npic02.gif
Просмотров: 590
Размер:	8.0 Кб
ID:	4391Нажмите на изображение для увеличения
Название: npic06.gif
Просмотров: 516
Размер:	13.8 Кб
ID:	4392Нажмите на изображение для увеличения
Название: npic07.gif
Просмотров: 542
Размер:	9.6 Кб
ID:	4393Нажмите на изображение для увеличения
Название: npic08.gif
Просмотров: 543
Размер:	10.5 Кб
ID:	4394Нажмите на изображение для увеличения
Название: npic09.gif
Просмотров: 552
Размер:	13.5 Кб
ID:	4395Нажмите на изображение для увеличения
Название: npic10.gif
Просмотров: 513
Размер:	7.3 Кб
ID:	4396Нажмите на изображение для увеличения
Название: npic11.gif
Просмотров: 521
Размер:	8.5 Кб
ID:	4397Нажмите на изображение для увеличения
Название: npic12.gif
Просмотров: 537
Размер:	9.5 Кб
ID:	4398Нажмите на изображение для увеличения
Название: npic13.gif
Просмотров: 518
Размер:	12.5 Кб
ID:	4399
Просмотров 866 Комментарии 5
Всего комментариев 5
Комментарии
  1. Старый комментарий
    Аватар для Croessmah
    Спасибо.
    Запись от Croessmah размещена 26.10.2017 в 10:50 Croessmah вне форума
  2. Старый комментарий
    Аватар для bedvit
    Хороший материал. Спасибо!
    Запись от bedvit размещена 31.10.2017 в 18:44 bedvit вне форума
  3. Старый комментарий
    Аватар для Evg
    Наконец родил вторую статью
    Запись от Evg размещена 30.12.2017 в 19:02 Evg вне форума
  4. Старый комментарий
    Аватар для Evg
    Родил третью статью
    Запись от Evg размещена 19.05.2018 в 18:34 Evg вне форума
  5. Старый комментарий
    Аватар для Captain Maxee
    Невероятно полезно и познавательно!
    Большое спасибо.
    Запись от Captain Maxee размещена 20.05.2018 в 01:55 Captain Maxee вне форума
 
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2018, vBulletin Solutions, Inc.
Рейтинг@Mail.ru