Подтвердить что ты не робот

Почему бы весь процесс, связанный с ЦП, работал лучше с гиперпотоком?

Дано:

  • Полностью процессор привязан к очень большому (т.е. более чем несколько циклов ЦП) и
  • Процессор с 4 физическими и всего 8 логическими ядрами,

Возможно ли, что потоки 8, 16 и 28 работают лучше, чем 4 потока? Я понимаю, что 4 потока будут иметь меньшие контекстные переключатели для выполнения и будут иметь меньшие служебные данные в любом смысле, чем 8, 16 или 28 потоков будут иметь на 4-х физических ядрах, Тем не менее, тайминги -

Threads    Time Taken (in seconds)
   4         78.82
   8         48.58
   16        51.35
   28        52.10

Код, используемый для проверки получения таймингов, указан в разделе Оригинальный вопрос ниже. Спецификации CPU также указаны внизу.


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

Оригинальный вопрос

Что это значит, когда мы говорим

Работа с гиперпотоками путем дублирования определенных разделов процессор - те, которые хранят архитектурное состояние, но не дублируют основные ресурсы выполнения. Это позволяет использовать процессор с гиперпотоками появляться как обычный "физический" процессор и дополнительный "логический" процессор к операционной системе хоста

?

Этот вопрос задан сегодня на SO, и в основном он проверяет производительность нескольких потоков, выполняющих ту же работу. Он имеет следующий код:

private static void Main(string[] args)
{
    int threadCount;
    if (args == null || args.Length < 1 || !int.TryParse(args[0], out threadCount))
        threadCount = Environment.ProcessorCount;

    int load;
    if (args == null || args.Length < 2 || !int.TryParse(args[1], out load))
        load = 1;

    Console.WriteLine("ThreadCount:{0} Load:{1}", threadCount, load);
    List<Thread> threads = new List<Thread>();
    for (int i = 0; i < threadCount; i++)
    {
        int i1 = i;
        threads.Add(new Thread(() => DoWork(i1, threadCount, load)));
    }

    var timer = Stopwatch.StartNew();
    foreach (var thread in threads) thread.Start();
    foreach (var thread in threads) thread.Join();
    timer.Stop();

    Console.WriteLine("Time:{0} seconds", timer.ElapsedMilliseconds/1000.0);
}

static void DoWork(int seed, int threadCount, int load)
{
    var mtx = new double[3,3];
    for (var i = 0; i < ((10000000 * load)/threadCount); i++)
    {
         mtx = new double[3,3];
         for (int k = 0; k < 3; k++)
            for (int l = 0; l < 3; l++)
              mtx[k, l] = Math.Sin(j + (k*3) + l + seed);
     }
}

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

Я запустил этот код на своей машине для репликации проблемы. Моя машина имеет 4 физических ядра и 8 логических. Метод DoWork() в приведенном выше коде полностью связан с ЦП. Я чувствовал, что гиперпоточность может способствовать, возможно, 30% -ному ускорению (потому что здесь у нас так много связанных с процессором потоков как физические ядра (т.е. 4)). Но он почти достигает 64% производительности.. Когда я запускал этот код для 4 потоков, это заняло около 82 секунд, и когда я запускал этот код для 8, 16 и 28 потоков, он выполнялся во всех случаях примерно 50 секунд.

Подводя итоги таймингов:

Threads    Time Taken (in seconds)
   4         78.82
   8         48.58
   16        51.35
   28        52.10

Я видел, что использование ЦП составляло ~ 50% с 4 потоками. Разве это не должно быть 100%? Ведь у моего процессора всего 4 физических ядра. И использование процессора было ~ 100% для 8 и 16 потоков.

Если кто-то может объяснить цитируемый текст с самого начала, я надеюсь лучше понять его с гиперпотоком и, в свою очередь, надеюсь получить ответ на вопрос: почему бы весь процесс с привязкой к процессору работать лучше с гиперпотоком?


Для завершения,

  • У меня есть процессор Intel Core i7-4770 с частотой 3,40 ГГц, 3401 МГц, 4 ядра (я), 8 логических процессоров.
  • Я запускал код в режиме Release.
  • Я знаю, что время измерения измеряется плохо. Это даст время для самой медленной нити. Я взял код, как и из другого вопроса. Однако, каково оправдание использования 50% CPU при запуске 4 связанных с процессором потоков на 4-х физических ядрах?
4b9b3361

Ответ 1

Я видел, что использование ЦП составляло ~ 50% с 4 потоками. Не должно быть ~ 100%?

Нет, это не должно быть.

Какое оправдание для использования 50% CPU при запуске 4 связанных потоком процессора на 4-х физических ядрах?

Это просто то, как загрузка процессора сообщается в Windows (и, по крайней мере, по какой-то другой ОС, кстати). Процессор HT отображается в виде двух ядер в операционной системе и сообщается как таковой.

Таким образом, Windows видит восьмиъядерную машину, когда у вас есть четыре HT-процессора. Вы увидите восемь разных графиков CPU, если вы посмотрите на вкладку "Производительность" в диспетчере задач, а общее использование ЦП вычисляется при 100% использовании, поскольку это полное использование этих восьми ядер.

Если вы используете только четыре потока, то эти потоки не могут полностью использовать доступные ресурсы ЦП и объясняют тайминги. Они могут, самое большее, использовать четыре из восьми ядер, и, конечно, ваше использование будет максимальным на 50%. Как только вы пройдете число логических ядер (8), время выполнения снова увеличивается; вы добавляете накладные расходы на планирование без добавления новых вычислительных ресурсов в этом случае.


Кстати, & hellip;

HyperThreading значительно улучшился с давних времен совместного использования кеша и других ограничений, но он все равно никогда не обеспечит того же пропускную способность, что и полный процессор, поскольку в процессоре остается некоторое противоречие. Поэтому даже игнорируя накладные расходы ОС, ваше 35-процентное улучшение скорости кажется мне очень хорошим. Я часто вижу не более 20% ускорения, добавляя дополнительные HT-ядра к вычислительно-узкому процессу.

Ответ 2

Конвейер CPU

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

Зная о конвейере, мы можем задаться вопросом: как процессор может работать так быстро, если мы будем писать чисто последовательный код, и каждая инструкция должна пройти через столько этапов конвейера? Вот ответ: процессор выполняет инструкции в out-of-order. Он имеет большой буфер переупорядочения (например, для 200 инструкций), и он выполняет множество инструкций через его конвейер параллельно. Если в любой момент какая-либо команда не может быть выполнена по какой-либо причине (ждет данных из медленной памяти, зависит от другой инструкции еще не закончена, что бы то ни было), то она задерживается на некоторые циклы. В течение этого времени процессор выполняет некоторые новые инструкции, которые расположены после отложенных команд в нашем коде, учитывая, что они никак не зависят от отложенных инструкций.

Теперь мы можем увидеть проблему latency. Даже если команда декодирована и все ее входы уже доступны, для ее выполнения потребуется всего несколько циклов. Эта задержка называется латентностью команды. Однако мы знаем, что в этот момент процессор может выполнять многие другие независимые команды, если они есть.

Если инструкция загружает данные из кеша L2, она должна ждать около 10 циклов для загружаемых данных. Если данные располагаются только в ОЗУ, для загрузки его на процессор потребуется сотни циклов. В этом случае мы можем сказать, что инструкция имеет высокую задержку. Для максимальной производительности важно выполнить некоторые другие независимые операции в данный момент. Это иногда называется скрытием скрытия.

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

PS Возможно, вы захотите прочитать руководство Agner Fog, чтобы лучше понять внутренности современных процессоров.

Hyper-Threading

Когда два потока выполняются в режиме гиперпоточности на одном ядре, процессор может чередовать свои инструкции, позволяя заполнять пузырьки из первого потока инструкциями второго потока. Это позволяет лучше использовать ресурсы процессора, особенно в случае обычных программ. Обратите внимание: HT может помочь не только в том, что у вас много доступа к памяти, но также и в сильно повторяющемся коде. Хорошо оптимизированный вычислительный код может полностью использовать все ресурсы процессора, и в этом случае вы увидите нет прибыль от HT (например, dgemm от хорошо оптимизированного BLAS).

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

Контекстные коммутаторы

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

Однако в случае гиперпоточности каждое ядро ​​имеет два состояния внутри: два набора регистров, разделяемые кеши, один набор исполнительных блоков. В результате ОС не нужно делать какие-либо переключатели контекста при запуске 8 потоков на 4 физических ядрах. Когда вы запускаете 16 потоков на четырехъядерном процессоре, переключатели контекста выполняются, но они занимают небольшую часть общего времени, как описано выше.

Менеджер процессов

Говоря об использовании процессора, которое вы видите в диспетчере процессов, он не измеряет внутренности конвейера CPU. Windows может заметить только, когда поток возвращает выполнение ОС, чтобы: спать, ждать мьютекса, ждать HDD и делать другие медленные вещи. В результате, он думает, что ядро ​​полностью используется, если на нем работает поток, который не спит и не ждет чего-либо. Например, вы можете проверить, что запуск бесконечного цикла while (true) {} приводит к полному использованию CPU.

Ответ 3

Я не могу объяснить, какой объем ускорения вы наблюдали: 100% кажется слишком большим для улучшения Hyperthreading. Но я могу объяснить принципы на месте.

Основное преимущество Hyperthreading - это когда процессор должен переключаться между потоками. Всякий раз, когда есть больше потоков, чем есть ядра ЦП (правда 99,9997% времени), и ОС решает переключиться на другой поток, он должен выполнить (большинство из) следующие шаги:

  • Сохранение состояния текущего потока: это включает в себя стек, состояние регистров и счетчик программ. где они сохраняются, зависит от архитектуры, но, вообще говоря, они либо будут сохранены в кеше, либо в памяти. В любом случае для этого шага требуется время.
  • Поместите Thread в состояние "Ready" (в отличие от состояния "Running" ).
  • Загрузите состояние следующего потока: снова, включая стек, регистры и счетчик программ, что еще раз - шаг , требующий времени.
  • Переверните поток в состояние "Запуск".

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

В CPU HyperThreading, с другой стороны, приведенное выше верно, но, кроме того, Каждое ядро ​​имеет дублированный набор регистров, счетчиков программ, счетчиков стека и (иногда) кеша. Это означает, что 4-ядерный процессор может иметь только 4 потока, работающих одновременно, но процессор может иметь "предварительно загруженные" потоки в дублированных регистрах. Таким образом, выполняется 4 потока, но на процессор загружено 8 потоков, 4 активных, 4 неактивных. Затем, когда время для CPU переключает потоки, вместо того, чтобы выполнять загрузку/выгрузку в тот момент, когда потоки должны переключаться, он просто "переключает", какой поток активен, и выполняет разгрузку/загрузку в фоновом режиме на новые "неактивные" регистры. Помните два шага, которые я приписывал "эти шаги требуют времени"? В системе Hyperthreaded этапы 2 и 4 являются единственными, которые должны выполняться в режиме реального времени, тогда как этапы 1 и 3 выполняются в фоновом режиме в аппаратном обеспечении (в отрыве от любой концепции потоков или процессов или процессорных ядер).

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

Сообщите мне, если вам нужны какие-либо разъяснения. Прошло несколько лет с CS250, поэтому я могу смешивать терминологию здесь или там; сообщите мне, если я использую неправильные термины для чего-то. Я 99.9997% уверен, что все, что я описываю, является точным с точки зрения логики того, как все это работает.

Ответ 4

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

Причина, по которой вы получаете такое большое ускорение, состоит в том, что в вашем методе DoWork нет логики ветвления. Это все большая петля с очень предсказуемой последовательностью выполнения.

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

Вы можете обнаружить, что если вы поместите выражения if в свой метод DoWork, вы не получите 100% ускорения...