Что такое накладные расходы контекстного переключателя?

Первоначально я считал, что накладные расходы на контекст-переключатель был сброшен TLB. Однако я только что видел по Википедии:

http://en.wikipedia.org/wiki/Translation_lookaside_buffer

В 2008 году появились Intel (Nehalem) [18] и AMD (SVM) [19] теги как часть записи TLB и выделенного оборудования, которое проверяет тег во время поиска. Несмотря на то, что они не полностью используются, Предполагалось, что в будущем эти теги будут идентифицировать адрес пространство, к которому принадлежит каждая запись TLB. Таким образом, переключатель контекста не будет приведет к смыванию TLB, но просто изменит метку текущее адресное пространство тега адресного пространства новой задачи.

Означает ли выше, что для более новых процессоров Intel TLB не очищается от контекстных переключателей?

Означает ли это, что в контекстном коммутаторе нет реальных накладных расходов?

(Я пытаюсь понять снижение производительности контекстного переключателя)

Ответы

Ответ 1

Как известно из Википедии в статье о переключении контекста, "переключение контекста - это процесс сохранения и восстановления состояния (контекста) процесса, так что выполнение может быть возобновлено с того же момента в более позднее время". Я предполагаю переключение контекста между двумя процессами одной и той же ОС, а не переход режима пользователя/ядра (syscall), который намного быстрее и не требует сброса TLB.

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

Если вы хотите узнать задержку переключения контекста, есть lmbench тестирования lmbench http://www.bitmover.com/lmbench/ с тестом LAT_CTX http://www.bitmover.com/lmbench/lat_ctx.8.html

Я не могу найти результаты для nehalem (есть ли lmbench в наборе phoronix?), Но для core2 и современного Linux переключение контекста может стоить 5-7 микросекунд.

Есть также результаты для некачественного теста http://blog.tsunanet.net/2010/11/how-long-does-it-take-to-make-context.html с 1-3 микросекундами для переключения контекста. Не могу получить точный эффект не-очистки TLB от его результатов.

ОБНОВЛЕНИЕ - Ваш вопрос должен быть о виртуализации, а не о переключении контекста процесса.

RWT говорит в своей статье о Nehalem "Внутри Nehalem: процессор и система Intel для будущего. TLB, таблицы страниц и синхронизация" от 2 апреля 2008 года Дэвида Кантера, что Nehalem добавил VPID в TLB для переключения виртуальной машины/хоста (vmentry/vmexit ) Быстрее:

Записи Nehalems TLB также слегка изменились, введя "ID виртуального процессора" или VPID. Каждая запись TLB кэширует преобразование виртуального адреса в физический... этот перевод специфичен для данного процесса и виртуальной машины. Старые процессоры Intel сбрасывали TLB всякий раз, когда процессор переключался между виртуализированным гостем и экземпляром хоста, чтобы гарантировать, что процессы обращаются только к памяти, к которой им разрешено обращаться. VPID отслеживает, с какой ВМ связана данная запись трансляции в TLB, поэтому при выходе и повторном входе ВМ TLB не нужно сбрасывать для безопасности.... VPID полезен для производительности виртуализации благодаря снижению накладных расходов на переходы виртуальных машин; По оценкам Intel, задержка перехода ВМ в оба конца в Nehalem составляет 40% по сравнению с Merom (то есть 65 нм Core 2) и примерно на треть ниже, чем 45 нм Penryn.

Также вы должны знать, что во фрагменте, указанном вами в вопросе, ссылка "[18]" была на "Дж. Нейгер, А. Сантони, Ф. Люн, Д. Роджерс и Р. Улиг. Виртуализация Intel Технология: аппаратная поддержка эффективной виртуализации процессора. Intel Technology Journal, 10 (3). ", Так что это функция для эффективной виртуализации (быстрые коммутаторы гостевой хост).

Ответ 2

Если мы учитываем недействительность кэша (который мы обычно должны и который является крупнейшим вкладчиком в затраты на переключение контекста в реальном мире), снижение производительности из-за переключения контекста может быть ОГРОМНЫМ:

https://www.usenix.org/legacy/events/expcs07/papers/2-li.pdf (по общему признанию, немного устаревший, но лучшее, что я смог найти) дает его в диапазоне тактов процессора 100K-1M. Теоретически, в наихудшем случае для серверного блока с несколькими сокетами с 32-миллионными кэшами L3 на каждый сокет, состоящими из 64-байтовых строк кэша, полностью произвольного доступа и типичного времени доступа 40 циклов для циклов L3/100 для основной оперативной памяти штраф может доходить до 30M+ циклов ЦП (!).

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

Ответ 3

Разбейте стоимость переключения задач на "прямые затраты" (стоимость самого кода переключения задач) и "косвенные затраты" (затраты на TLB и т.д.).

Прямые затраты

Для прямых затрат это в основном стоимость сохранения состояния (архитектурно видимого для пространства пользователя) для предыдущей задачи, а затем загрузки состояния для следующей задачи. Это варьируется в зависимости от ситуации, главным образом потому, что оно может включать или не включать состояние FPU/MMX/SSE/AVX, которое может добавлять до нескольких килобайт данных (особенно, если задействован AVX - например, AVX2 сам по себе составляет 512 байт, а AVX- 512 больше 2 КиБ сам по себе).

Обратите внимание, что существует механизм "отложенной загрузки состояния", позволяющий избежать затрат на загрузку (некоторые или все) состояния FPU/MMX/SSE/AVX и избежать затрат на сохранение этого состояния, если оно не было загружено; и эта функция может быть отключена по соображениям производительности (если почти все задачи используют состояние, тогда стоимость "используемое состояние должно быть загружено" ловушка/исключение превышает то, что вы экономите, пытаясь избежать этого во время переключения задач) или по соображениям безопасности (например, потому что код в Linux "сохраняет, если используется", а не "сохраняет, затем очищает, если используется" и оставляет данные, принадлежащие одной задаче, в регистрах, которые могут быть получены другой задачей с помощью спекулятивных атак выполнения).

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

Косвенные расходы

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

Косвенные затраты можно разделить на 3 причины. Один из них - это косвенные затраты, которые возникают из-за того, что объект был полностью очищен переключателем задач. В прошлом это в основном ограничивалось пропусками TLB, вызванными тем, что TLB сбрасывались во время переключения задач. Обратите внимание, что это может произойти, даже когда используется PCID - существует ограничение в 4096 идентификаторов (и когда используется "смягчение последствий расплавления", идентификаторы используются парами - для каждого виртуального адресного пространства один идентификатор используется для пространства пользователя, а другой - для другого). для ядра), что означает, что при использовании более 4096 (или 2048) виртуальных адресных пространств ядро должно повторно использовать ранее использованные идентификаторы и очищать все TLB для идентифицируемого идентификатора. Однако теперь (со всеми спекулятивными проблемами безопасности выполнения) ядро может сбрасывать другие вещи (например, средства прогнозирования ветвлений), чтобы информация не могла просачиваться из одной задачи в другую, но я действительно не знаю, выполняет ли Linux 'или нет' t не поддерживает это для вещей, подобных кешу (и я подозреваю, что они в первую очередь пытаются предотвратить утечку данных из ядра в пространство пользователя и в конечном итоге предотвращают утечку данных из одной задачи в другую случайно).

Еще одна причина косвенных затрат - ограничения по мощности. Например, если кэш L2 может кэшировать максимум 256 КБ данных, а в предыдущем задании использовалось более 256 КБ данных; тогда кэш L2 будет заполнен данными, которые бесполезны для следующей задачи, и все данные, которые следующая задача хочет кэшировать (и ранее кэшировала), будут удалены из-за "использования в последнее время". Это относится ко всем вещам, подобным кешу (включая TLB и кеширование структуры подкачки более высокого уровня, даже когда используется функция PCID).

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

Обратите внимание, что верхний предел величины косвенных затрат зависит от того, что делает задача. Например, если задача использует большой объем данных, то косвенные затраты могут быть относительно дорогими (много кеша и TLB отсутствует), а если задача использует небольшое количество данных, то косвенные затраты могут быть незначительными (очень мало кеша и TLB пропускает).

несвязанный

Обратите внимание, что функция PCID имеет свои собственные расходы (не связанные с самими переключателями задач). В частности, когда трансляции страниц изменяются на одном процессоре, их, возможно, придется аннулировать на других процессорах, используя то, что называется "сбой многопроцессорного TLB", что относительно дорого (включает прерывание IPI/межпроцессорное соединение, которое нарушает работу других процессоров и стоит "низкие сотни" циклов "на процессор). Без PCID вы можете избежать некоторых из них. Например, без PCID, для однопоточного процесса, который выполняется на одном процессоре, вы знаете, что ни один другой процессор не может использовать то же виртуальное адресное пространство, и, следовательно, знаете, что вам не нужно выполнять "многопроцессорную перегрузку TLB" ", и если многопоточный процесс ограничен одним доменом NUMA, то только" CPU "в этом домене NUMA должны быть вовлечены в" сбой многопроцессорного TLB ". Когда используется PCID, вы не можете полагаться на эти уловки и иметь более высокие издержки, потому что "сбой TLB для нескольких процессоров" не так часто избегается.

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

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

Ответ 4

Примечание: как отметил Брендан в своем комментарии. Цель этого ответа - ответить на детали. Каково общее влияние переключателей контекста на производительность Windows-сервера/рабочего стола, включая накладные расходы операционной системы, которые различаются в Windows и Linux, в Solaris и т.д.

Лучший способ выяснить это, конечно, сравнить его. Проблема здесь заключается в том, что соотношение между количеством переключений контекста в секунду и временем ЦП является экспоненциальным. Другими словами, это O (n 2) стоимость. Это означает, что у нас есть максимальный предел, который просто не может быть превышен.

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

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

  • Добавьте следующий код в консольное приложение .NET и посмотрите результаты в perfmon.
  • Добавьте два счетчика в Perfmon: Processor->% времени процессора и System-> Переключение контекста в секунду. На 8-ядерном компьютере 128 потоков генерируют около 0,1% загрузки ЦП из работы, выполняемой потоками.

Само собой разумеется, что 2560 потоков должны генерировать примерно 2% ЦП, но вместо этого ЦП переходит на 100% при 2300 потоках (на моем настольном компьютере с ядром Core i7-4790K 4 Core + 4 Core).

  • 2048 потоков - 2 миллиона переключений контекста в секунду: процессор на 40%
  • 2300 потоков - 2,3 миллиона переключений контекста в секунду: процессор на 100%

perfmon graph

static void Main(string[] args)
{
    ThreadTestClass ThreadClass;
    bool Wait;
    int Counter;
    Wait = true;
    Counter = 0;
    while (Wait)
    {
        if (Console.KeyAvailable)
        {
            ConsoleKey Key = Console.ReadKey().Key;
            switch (Key)
            {
                case ConsoleKey.UpArrow:
                    ThreadClass = new ThreadTestClass();
                    break;
                case ConsoleKey.DownArrow:
                    SignalExitThread();
                    break;
                case ConsoleKey.PageUp:
                    SleepTime += 1;
                    break;
                case ConsoleKey.PageDown:
                    SleepTime -= 1;
                    break;
                case ConsoleKey.Insert:
                    for (int I = 0; I < 64; I++)
                    {
                        ThreadClass = new ThreadTestClass();
                    }
                    break;
                case ConsoleKey.Delete:
                    for (int I = 0; I < 64; I++)
                    {
                        SignalExitThread();
                    }
                    break;
                case ConsoleKey.Q:
                    Wait = false;
                    break;
                case ConsoleKey.Spacebar:
                    Wait = false;
                    break;
                case ConsoleKey.Enter:
                    Wait = false;
                    break;
            }
        }
        Counter += 1;
        if (Counter >= 10)
        {
            Counter = 0;
            Console.WriteLine(string.Concat(@"Thread Count: ", NumThreadsActive.ToString(), @" - SleepTime: ", SleepTime.ToString(), @" - Counter: ", UnSafeCounter.ToString()));
        }
        System.Threading.Thread.Sleep(100);
    }
    IsActive = false;
}

public static object SyncRoot = new object();
public static bool IsActive = true;
public static int SleepTime = 1;
public static long UnSafeCounter = 0;
private static int m_NumThreadsActive;
public static int NumThreadsActive
{
    get
    {
        lock(SyncRoot)
        {
            return m_NumThreadsActive;
        }
    }
}
private static void NumThreadsActive_Inc()
{
    lock (SyncRoot)
    {
        m_NumThreadsActive += 1;
    }
}
private static void NumThreadsActive_Dec()
{
    lock (SyncRoot)
    {
        m_NumThreadsActive -= 1;
    }
}
private static int ThreadsToExit = 0;
private static bool ThreadExitFlag = false;
public static void SignalExitThread()
{
    lock(SyncRoot)
    {
        ThreadsToExit += 1;
        ThreadExitFlag = (ThreadsToExit > 0);
    }
}

private static bool ExitThread()
{
    if (ThreadExitFlag)
    {
        lock (SyncRoot)
        {
            ThreadsToExit -= 1;
            ThreadExitFlag = (ThreadsToExit > 0);
            return (ThreadsToExit >= 0);
        }
    }
    return false;
}

public class ThreadTestClass
{
    public ThreadTestClass()
    {
        System.Threading.Thread RunThread;
        RunThread = new System.Threading.Thread(new System.Threading.ThreadStart(ThreadRunMethod));
        RunThread.Start();
    }

    public void ThreadRunMethod()
    {
        long Counter1;
        long Counter2;
        long Counter3;
        Counter1 = 0;
        NumThreadsActive_Inc();
        try
        {
            while (IsActive && (!ExitThread()))
            {
                UnSafeCounter += 1;
                System.Threading.Thread.Sleep(SleepTime);
                Counter1 += 1;
                Counter2 = UnSafeCounter;
                Counter3 = Counter1 + Counter2;
            }
        }
        finally
        {
            NumThreadsActive_Dec();
        }
    }
}