Как оценить издержки переключения контекста потока? - PullRequest
57 голосов
/ 20 ноября 2008

Я пытаюсь улучшить производительность многопоточного приложения в режиме реального времени. Он работает на Windows Mobile и написан на C / C ++. У меня есть подозрение, что высокая частота переключения потоков может вызывать ощутимые издержки, но не может ни доказать это, ни опровергнуть. Как известно, отсутствие доказательств не является доказательством обратного:).

Таким образом, мой вопрос состоит из двух частей:

  • Если вообще существует, где я могу найти какие-либо фактические измерения стоимости переключения контекста потока?

  • Не тратя время на написание тестового приложения, как можно оценить издержки переключения потоков в существующем приложении?

  • Кто-нибудь знает способ узнать количество переключений контекста (вкл / выкл) для данного потока?

Ответы [ 9 ]

26 голосов
/ 20 ноября 2008

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

  • ЦП, так как необходимые операции могут быть проще или сложнее на разных типах ЦП
  • Системное ядро, так как разные ядра должны будут выполнять разные операции на каждом коммутаторе

Другие факторы включают в себя то, как происходит переключение. Переключение может иметь место, когда

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

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

  3. потоку больше не требуется процессорное время, потому что он блокирует какую-то операцию или просто вызывает sleep () (или подобный), чтобы остановить работу.

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

Я думаю, что если вы хотите знать наверняка, вы должны тестировать. Проблема в том, что вам обычно приходится либо усыплять потоки, либо синхронизировать их с помощью мьютексов. Спящая или блокирующая / разблокирующая мьютексы сама по себе накладные расходы. Это означает, что ваш тест будет включать и эти накладные расходы. Не имея мощного профилировщика, позже трудно сказать, сколько процессорного времени было использовано для фактического переключения и сколько для вызова сна / мьютекса. С другой стороны, в реальном сценарии ваши потоки будут либо спать, либо синхронизироваться через блокировки. Тест, который просто измеряет время переключения контекста, является синтетическим тестом, поскольку он не моделирует сценарий реальной жизни. Тесты намного более «реалистичны», если они основаны на реальных сценариях. Какой смысл использовать тест GPU, который говорит мне, что мой GPU теоретически может обрабатывать 2 миллиарда полигонов в секунду, если этот результат никогда не будет достигнут в реальном 3D приложении? Разве не было бы намного интереснее узнать, сколько полигонов в реальном 3D-приложении может обрабатывать графический процессор в секунду?

К сожалению, я ничего не знаю о программировании Windows. Я мог бы написать приложение для Windows на Java или, возможно, на C #, но C / C ++ в Windows заставляет меня плакать. Я могу предложить вам только исходный код для POSIX.

#include <stdlib.h>
#include <stdint.h>
#include <stdio.h>
#include <pthread.h>
#include <sys/time.h>
#include <unistd.h>

uint32_t COUNTER;
pthread_mutex_t LOCK;
pthread_mutex_t START;
pthread_cond_t CONDITION;

void * threads (
    void * unused
) {
    // Wait till we may fire away
    pthread_mutex_lock(&START);
    pthread_mutex_unlock(&START);

    pthread_mutex_lock(&LOCK);
    // If I'm not the first thread, the other thread is already waiting on
    // the condition, thus Ihave to wake it up first, otherwise we'll deadlock
    if (COUNTER > 0) {
        pthread_cond_signal(&CONDITION);
    }
    for (;;) {
        COUNTER++;
        pthread_cond_wait(&CONDITION, &LOCK);
        // Always wake up the other thread before processing. The other
        // thread will not be able to do anything as long as I don't go
        // back to sleep first.
        pthread_cond_signal(&CONDITION);
    }
    pthread_mutex_unlock(&LOCK); //To unlock
}

int64_t timeInMS ()
{
    struct timeval t;

    gettimeofday(&t, NULL);
    return (
        (int64_t)t.tv_sec * 1000 +
        (int64_t)t.tv_usec / 1000
    );
}


int main (
    int argc,
    char ** argv
) {
    int64_t start;
    pthread_t t1;
    pthread_t t2;
    int64_t myTime;

    pthread_mutex_init(&LOCK, NULL);
    pthread_mutex_init(&START, NULL);   
    pthread_cond_init(&CONDITION, NULL);

    pthread_mutex_lock(&START);
    COUNTER = 0;
    pthread_create(&t1, NULL, threads, NULL);
    pthread_create(&t2, NULL, threads, NULL);
    pthread_detach(t1);
    pthread_detach(t2);
    // Get start time and fire away
    myTime = timeInMS();
    pthread_mutex_unlock(&START);
    // Wait for about a second
    sleep(1);
    // Stop both threads
    pthread_mutex_lock(&LOCK);
    // Find out how much time has really passed. sleep won't guarantee me that
    // I sleep exactly one second, I might sleep longer since even after being
    // woken up, it can take some time before I gain back CPU time. Further
    // some more time might have passed before I obtained the lock!
    myTime = timeInMS() - myTime;
    // Correct the number of thread switches accordingly
    COUNTER = (uint32_t)(((uint64_t)COUNTER * 1000) / myTime);
    printf("Number of thread switches in about one second was %u\n", COUNTER);
    return 0;
}

выход

Number of thread switches in about one second was 108406

Более 100'000 - это не так уж и плохо, хотя у нас есть блокировки и условные ожидания. Я полагаю, что без всего этого было бы как минимум вдвое больше переключателей потоков в секунду.

14 голосов
/ 20 ноября 2008

Вы не можете оценить это. Вы должны измерить это. И это будет варьироваться в зависимости от процессора в устройстве.

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

Сначала кодовый путь (псевдокод):

DWORD tick;

main()
{
  HANDLE hThread = CreateThread(..., ThreadProc, CREATE_SUSPENDED, ...);
  tick = QueryPerformanceCounter();
  CeSetThreadPriority(hThread, 10); // real high
  ResumeThread(hThread);
  Sleep(10);
}

ThreadProc()
{
  tick = QueryPerformanceCounter() - tick;
  RETAILMSG(TRUE, (_T("ET: %i\r\n"), tick));
}

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

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

Не кодовым маршрутом будет использование Remote Kernel Tracker. Установите eVC 4.0 или eval версию Platform Builder, чтобы получить его. Это даст графическое отображение всего, что делает ядро, и вы можете непосредственно измерить переключение контекста потока с предоставленными возможностями курсора. Опять же, я уверен, что у Сью есть запись в блоге об использовании Kernel Tracker.

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

13 голосов
/ 20 ноября 2008

В то время как вы сказали, что не хотите писать тестовое приложение, я сделал это для предыдущего теста на платформе ARM9 Linux, чтобы выяснить, каковы издержки. Это были только два потока, которые повысили бы: thread :: yield () (или, вы знаете) и увеличили некоторую переменную, и через минуту или около того (без других запущенных процессов, по крайней мере, ни одного, которые что-то делают), приложение напечатало сколько переключений контекста он может сделать в секунду. Конечно, это не совсем точно, но дело в том, что оба потока уступили ЦП друг другу, и это было так быстро, что просто не имело смысла думать о накладных расходах. Итак, просто продолжайте и просто напишите простой тест вместо того, чтобы слишком много думать о проблеме, которая может отсутствовать.

Кроме этого, вы можете попробовать 1800 с счетчиками производительности.

О, и я помню приложение, работающее в Windows CE 4.X, где у нас также есть четыре потока с интенсивным переключением, которые никогда не сталкивались с проблемами производительности. Мы также попытались реализовать реализацию многопоточности без потоков и не увидели улучшения производительности (графический интерфейс реагировал намного медленнее, но все остальное было таким же). Возможно, вы можете попробовать то же самое, либо уменьшив количество переключений контекста, либо полностью удалив потоки (только для тестирования).

7 голосов
/ 15 мая 2010

My 50 строк C ++ показывают для Linux (QuadCore Q6600) время переключения контекста ~ 0,9us (0,75us для 2 потоков, 0,95 для 50 потоков). В этом бенчмарке потоки вызывают yield сразу после получения кванта времени.

6 голосов
/ 19 августа 2011

Переключение контекста стоит дорого, как правило, стоит 30 мкс загрузки процессора http://blog.tsunanet.net/2010/11/how-long-does-it-take-to-make-context.html

5 голосов
/ 20 ноября 2008

Я только когда-либо пытался оценить это один раз, и это было на 486! В результате для переключения контекста процессора потребовалось около 70 команд (обратите внимание, что это происходило для многих вызовов API api, а также для переключения потоков). Мы подсчитали, что на DX3 потребовалось около 30 мкс на переключение потоков (включая издержки ОС). Несколько тысяч переключений контекста, которые мы делали в секунду, занимали 5-10% процессорного времени.

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

Обратите внимание, что создание / удаление потоков является более дорогим хоггером ЦП / ОС, чем активация / деактивация потоков. Хорошей политикой для многопоточных приложений является использование пулов потоков и активация / деактивация по мере необходимости.

3 голосов
/ 28 февраля 2010

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

double * a; 
...
for (i = 0; i < 1000; i ++)
{
    a[i] = a[i] + a[i]
}

потому что его время выполнения намного меньше, чем стоимость переключения контекста. На Core i7 этот код занимает около 1 микросекунды (зависит от компилятора). Так что время переключения контекста имеет значение, потому что оно определяет, как можно выполнять небольшие задания. Я предполагаю, что это также обеспечивает метод для эффективного измерения переключения контекста. Проверьте, как долго должен быть массив (в верхнем примере), чтобы два потока из пула потоков начали показывать некоторое реальное преимущество по сравнению с однопоточным. Это может легко стать 100 000 элементов, и, следовательно, эффективное время переключения контекста будет где-то в диапазоне 20us в пределах одного и того же приложения.

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

Atmapuri

1 голос
/ 14 марта 2019

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

При изменении контекста все эти механизмы кэширования сбрасываются, и новый поток запускается из «пустого» состояния.

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

1 голос
/ 20 ноября 2008

Не знаю, но у вас есть обычные счетчики производительности в Windows Mobile? Вы можете посмотреть на такие вещи, как переключение контекста / сек. Я не знаю, есть ли один, который определенно измеряет время переключения контекста.

...