Каковы общие причины высокой загрузки процессора? - PullRequest
15 голосов
/ 14 февраля 2012

Справочная информация:

В моем приложении, написанном на C ++, я создал 3 потока:

  • AnalysisThread (или Producer): он считывает входной файл, анализирует его, генерирует шаблоны и помещает их в std::queue 1 .
  • PatternIdRequestThread (или Consumer): он извлекает шаблоны из очереди и отправляет их один за другим в базу данных через клиент (написанный на C ++), который возвращает шаблон uid , который затем назначается соответствующий шаблон.
  • ResultPersistenceThread: он делает несколько вещей, общается с базой данных и работает нормально, как и ожидалось, с точки зрения использования процессора.

Первые два потока занимают 60-80% загрузки ЦП, каждый в среднем занимает 35%.

Вопрос:

Я не понимаю, почему некоторые потоки нагружают процессор.

Я анализирую это следующим образом: если ОС принимает решения, такие как переключатель контекста , прерывание и планирование относительно того, какой поток должен быть Если у вас есть доступ к системным ресурсам, таким как время процессора, то почему некоторые потоки в процессе используют больше ресурсов процессора, чем другие? Похоже, что некоторые потоки принудительно отнимают ЦП от ОС под дулом пистолета , или ОС имеет реальную уязвимость для некоторых потоков и поэтому смещена к ним с самого начала, давая им все ресурсы, которые у него есть. Почему это не может быть беспристрастным и дать им все одинаково?

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

Интересно, каковы причины высокой загрузки процессора? Как мы можем их идентифицировать? Можно ли их идентифицировать, просто взглянув на код? Какие инструменты?

Я использую Visual Studio 2010.

1. Я тоже сомневаюсь в std::queue. Я знаю, что стандартные контейнеры не являются потокобезопасными. Но если ровно один поток ставит в очередь элементы в очередь, то безопасно ли, чтобы ровно один поток исключал из него элементы? Я представляю, что это похоже на канал: с одной стороны вы вставляете данные, с другой - удаляете данные, тогда почему это будет небезопасно, если это будет сделано одновременно? Но это не реальный вопрос в этой теме, однако, вы можете добавить примечание в своем ответе, обращаясь к этому.

Обновление:

После того, как я понял, что мой потребительский поток использует занятое вращение, которое я исправил с помощью Sleep в течение 3 секунд. Это временное исправление, и скоро я буду использовать Event . Но даже с Sleep загрузка ЦП снизилась до 30-40%, а иногда и до 50%, что не представляется желательным с точки зрения удобства использования, поскольку система этого не делает. не отвечает на другие приложения, с которыми в данный момент работает пользователь.

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

Ответы [ 8 ]

24 голосов
/ 14 февраля 2012

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

OS / планировщик в значительной степени нене предсказывать объем работы, которую будет выполнять поток.Поток (для упрощения) находится в одном из трех состояний:

  1. заблокирован в ожидании чего-либо (сон, мьютекс, ввод-вывод и т. Д.)
  2. работоспособен, но нев данный момент работает, потому что другие *
  3. запущены.

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

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

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

Для вашего примера цикла ваш код на самом деле ничего не делает, поэтому вам нужно проверить, как он был оптимизирован, прежде чем судить, имеет ли смысл 5-7% CPU.В идеале, на двухъядерной машине поток с большой нагрузкой должен занимать 50% ЦП.На 4-х ядерном компьютере 25%.Таким образом, если у вас не менее 16 ядер, то ваш результат на первый взгляд будет аномальным (а если у вас будет 16 ядер, то один поток, занимающий 35%, будет еще более аномальным!).В стандартной настольной ОС большинство ядер простаивают большую часть времени, поэтому, чем больше доля ЦП, занимаемая вашими программами при запуске, тем лучше.

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

, если ровно один поток ставит в очередь элементы в очередь, тогда безопасно ли, если ровно один поток удаляет из него элементы?

Нет, это не безопасно для std::queue со стандартным контейнером.std::queue - это тонкая оболочка поверх контейнера последовательности (vector, deque или list), она не добавляет никакой безопасности потока.Поток, который добавляет элементы, и поток, который удаляет элементы, изменяют некоторые общие данные, например поле size нижележащего контейнера.Вам нужна либо некоторая синхронизация, либо безопасная структура очереди без блокировки, которая опирается на атомарный доступ к общим данным.std::queue не имеет ни того, ни другого.

7 голосов
/ 14 февраля 2012

Редактировать : Хорошо, поскольку вы используете вращение в режиме занятости для блокировки очереди, это, скорее всего, является причиной высокой загрузки ЦП. У ОС сложилось впечатление, что ваши потоки выполняют полезную работу, когда их нет, поэтому они получают полное время процессора. Здесь было интересное обсуждение: Какой из них лучше для производительности, чтобы проверить другие потоки boolean в java

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

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

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

4 голосов
/ 26 февраля 2012

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

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

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

Теперь давайте рассмотрим виды, которые вы можете найти, которые потребляют много ресурсов ЦП.

  • Рабочий поток обычно реализуется в виде цикла.В верхней части цикла выполняется проверка, чтобы решить, есть ли работа, и любая доступная работа будет выполнена.Новая итерация цикла начинает цикл снова.

    Вы можете обнаружить, что при такой установке большая часть процессорного времени, выделенного этому потоку, тратится на циклы и проверку, и очень мало тратится на выполнение работы.Это так называемая проблема занятого ожидания.Чтобы частично решить эту проблему, вы можете добавить sleep между итерациями цикла, но это не лучшее решение.Идеальный способ решения этой проблемы - перевести поток в спящий режим, когда нет работы, а когда какой-то другой поток генерирует работу для спящего потока, он отправляет сигнал для его пробуждения.Это практически исключает накладные расходы, т. К. Поток будет использовать процессор только тогда, когда есть над чем работать.Я обычно реализую этот механизм с семафорами, но в Windows вы также можете использовать объект Event.Вот эскиз реализации:

    class MyThread {
    private:
        void thread_function() {
            while (!exit()) {
                if (there_is_work_to_do())
                    do_work();
                go_to_sleep();
            }
        }
        // this is called by the thread function when it
        // doesn't have any more work to do
        void go_to_sleep() {
            sem.wait();
        }
    public:
        // this is called by other threads after they add work to
        // the thread's queue
        void wake_up() {
            sem.signal();
        }
    };
    

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

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

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

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

    Итак, стратегияздесь следует начать с самой дорогой функции согласно отчету о профилировании и попытаться выполнить небольшую оптимизацию.Затем вы снова запускаете профилировщик, чтобы увидеть, как все изменилось.Вы можете обнаружить, что небольшое изменение самой интенсивно загружающей функции ЦП перемещает ее на 2-е или 3-е место, и в результате общее использование ЦП было уменьшено.Поздравив себя с улучшением, вы повторите упражнение с новой функцией top.Вы можете продолжать этот процесс, пока не убедитесь, что ваше приложение максимально эффективно.

Удачи.

3 голосов
/ 01 марта 2012

Потоки потребляют ресурсы, такие как память.Блокирующий / разблокирующий поток требует единовременной скидки.Если поток блокирует / разблокирует десятки тысяч раз в секунду, это может привести к потере значительных ресурсов ЦП.

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

Однако я часто этим занимаюсь, и мой метод таков: http://www.wikihow.com/Optimize-Your-Program%27s-Performance

3 голосов
/ 24 февраля 2012

Хотя другие уже правильно проанализировали проблему (насколько я могу судить), позвольте мне попытаться добавить некоторые подробности к предлагаемым решениям.

Во-первых, суммируем проблемы: 1. ЕслиВы заставляете свой потребительский поток работать в цикле for или подобном, это ужасная трата мощности процессора.2. Если вы используете функцию sleep () с фиксированным числом миллисекунд, это либо пустая трата ЦП (либо слишком мало времени), либо вы излишне задерживаете процесс (если он слишком велик).Невозможно правильно установить количество времени.

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

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

Шаг 1: Вам нужен один мьютекс и один сигнал:

#include <pthread.h>
pthread_mutex_t *mutex  = new pthread_mutex_t;
pthread_cond_t  *signal = new pthread_cond_t;

/* Initialize the mutex and the signal as below.
   Both functions return an error code. If that
   is not zero, you need to react to it. I will
   skip the details of this. */
pthread_mutex_init(mutex,0);
pthread_cond_init(signal,0);

Шаг 2: Теперь внутри потока потребителя дождитесь отправки сигнала.Идея состоит в том, что производитель отправляет сигнал всякий раз, когда он добавляет новое задание в очередь:

/* Lock the mutex. Again, this might return an error code. */
pthread_mutex_lock(mutex);

/* Wait for the signal. This unlocks the mutex and then 'immediately'
   falls asleep. So this is what replaces the busy spinning, or the
   fixed-time sleep. */
pthread_cond_wait(signal,mutex);

/* The program will reach this point only when a signal has been sent.
   In that case the above waiting function will have locked the mutex
   right away. We need to unlock it, so another thread (consumer or
   producer alike) can access the signal if needed.  */
pthread_mutex_unlock(mutex);

/* Next, pick a task from the queue and deal with it. */

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

Шаг 3: Включить поток производителя вотправлять сигнал всякий раз, когда он добавляет задачу в очередь:

/* We assume we are now in the producer thread and have just appended
   a task to the queue. */
/* First we lock the mutex. This must be THE SAME mutex object as used
   in the consumer thread. */
pthread_mutex_lock(mutex);

/* Then send the signal. The argument must also refer to THE SAME
   signal object as is used by the consumer. */
pthread_cond_signal(signal);

/* Unlock the mutex so other threads (producers or consumers alike) can
   make use of the signal. */
pthread_mutex_unlock(mutex);

Шаг 4: Когда все закончится и вы закроете свои потоки, вы должны уничтожить мьютекс и сигнал:

pthread_mutex_destroy(mutex);
pthread_cond_destroy(signal);
delete mutex;
delete signal;

В заключение позвольте мне еще раз повторить одно, что уже говорили другие: вы не должны использовать обычный std::deque для одновременного доступа.Один из способов решения этой проблемы - объявить еще один мьютекс, заблокировать его перед каждым доступом к deque и разблокировать сразу после него.

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

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

0 голосов
/ 28 февраля 2012

Как уже говорили, правильным способом синхронизации передачи обслуживания между потоками производителя и потребителя было бы использование условной переменной.Когда производитель хочет добавить элемент в очередь, он блокирует переменную условия, добавляет элемент и уведомляет официантов о переменной условия.Потребитель ожидает той же переменной условия и, когда получает уведомление, потребляет элементы из очереди, а затем снова блокируется.Я лично рекомендую использовать для них boost :: interprocess, но это можно сделать достаточно простым способом, используя и другие API.

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

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

Если вы хотите разумно ограничить поток производителя ... это немного больше работы, но вы могли бы сделать так, чтобы поток производителя добавил элементы кОчередь, пока не достигнет определенного порога (скажем, 10 элементов), затем подождите другую переменную условия.Когда потребитель потребляет достаточно данных, что приводит к тому, что количество элементов в очереди становится ниже порогового значения (скажем, 5 элементов), он уведомляет эту вторую переменную условия.Если все части системы могут быстро перемещать данные, то это все равно может потреблять много ресурсов ЦП, но они будут относительно равномерно распределены между ними.Именно в этот момент ОС должна отвечать за то, чтобы другие несвязанные процессы получили свою справедливую (ish) долю ЦП.

0 голосов
/ 28 февраля 2012
  1. использовать асинхронный (файловый и сокетный) ввод-вывод для сокращения бесполезного времени ожидания ЦП.
  2. используйте модель вертикальной резьбы, чтобы по возможности уменьшить переключение контекста
  3. используйте безблокировочную структуру данных
  4. используйте инструмент профилирования, такой как VTune, для определения горячей точки исделать оптимизацию
0 голосов
/ 14 февраля 2012

Использование ЦП потока зависит от многих факторов, но в основном ОС может назначать время обработки только на основе точек, в которых она может прервать поток.

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

Если бы в вашем цикле этого не было, то он, скорее всего, использовал бы почти 100% ЦП.

...