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

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

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

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

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

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

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

4b9b3361

Ответ 1

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

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

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

Ответ 2

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

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

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

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

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

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

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

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

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

Ответ 3

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

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

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

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.

Все, что сказал, вы обнаружите, что переключатели контекста потока внутрипроцессного ПО на самом деле очень быстрые. Это процесс, который стоит дорого, поскольку он требует замены активного процесса в ОЗУ и последующего переноса.

Ответ 4

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

Ответ 5

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

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

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

Ответ 7

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

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

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

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

Atmapuri

Ответ 8

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