Как оценить издержки переключения контекста потока?

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

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

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

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

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

8 ответов

Решение

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Atmapuri

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

Другие вопросы по тегам