Как оценить накладные расходы на переключение контекста потока?


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

таким образом, мой вопрос двоякий:

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

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

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

8 55

8 ответов:

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

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

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

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

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

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

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

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

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

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

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

к сожалению, я ничего не знаю о программировании 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-это не так уж плохо, и это несмотря на то, что у нас есть блокировка и условное ожидание. Я бы предположил, что без всего этого, по крайней мере, в два раза больше переключателей потоков было возможно в секунду.

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

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

во-первых, путь кода (псевдокод):

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, подключившись к событиям планировщика, но это далеко не просто сделать и не очень хорошо документировано. Если вы действительно хотите пойти по этому маршруту, у Sue Loh есть несколько блогов, которые может найти поисковая система.

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

все, что сказал, Вы обнаружите, что ВНУТРИПРОЦЕССНЫЕ переключатели контекста потока CE действительно очень быстры. Это процесс переключатели, которые стоят дорого, так как он требует замены активный процесс в ОЗУ, а затем выполняет миграцию.

мой 50 строк C++ показать для Linux (QuadCore Q6600) время переключения контекста ~ 0.9 us (0.75 us для 2 потоков, 0.95 для 50 потоков). В этом тесте потоки вызывают выход сразу же, когда они получают квант времени.

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

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

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

контекстный переключатель дорог, как правило, он стоит 30 МКС накладных расходов процессораhttp://blog.tsunanet.net/2010/11/how-long-does-it-take-to-make-context.html

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

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

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

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

Atmapuri

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