SwitchToThread vs Sleep(0)

Тема в разделе "WASM.WIN32", создана пользователем Jin X, 21 июн 2020.

  1. Jin X

    Jin X Active Member

    Публикаций:
    0
    Регистрация:
    15 янв 2009
    Сообщения:
    369
    Адрес:
    Кольца Сатурна
    В текущем потоке установлен максимальный приоритет (SetThreadPriority(THREAD_PRIORITY_TIME_CRITICAL)). Я же правильно понимаю, что приоритет процесса в данном случае неважен, если он не REALTIME_PRIORITY_CLASS (а такой можно установить только из-под админа)?

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

    На ум приходит 2 варианта: SwitchToThread и Sleep(0).
    Что лучше и почему?

    В разных источниках вижу противоречивую инфу: где-то пишут, что SwitchToThread не переключает контекст на потоки с более низким приоритетом, в отличие от Sleep(0), где-то пишут, что наоборот.
    В MSDN инфа об этом есть только в описании Sleep в контексте конкретно XP.

    Есть ли ещё какая-либо разница между этими 2-мя функциями (Sleep интересует именно с 0)?

    И ещё вопрос: есть ли какие-либо ситуации (и какие), когда винда может переключить исполнение с текущего потока на поток с более низким приоритетом (на том же ядре), если текущий поток вообще не обращается к ядру или если его не остановили или не понизили его приоритет?
     
  2. HoShiMin

    HoShiMin Well-Known Member

    Публикаций:
    5
    Регистрация:
    17 дек 2016
    Сообщения:
    1.422
    Адрес:
    Россия, Нижний Новгород
    Sleep(0) и SwithToThread делают абсолютно одно и то же.
    Sleep сводится к NtDelayExecution, SwitchToThread - к NtYieldExecution, который, в свою очередь, сводится к KeYieldExecution(0).
    NtDelayExecution сводится к KeDelayExecutionThread, внутри которого есть проверка, задан ли нулевой интервал.
    Если интервал нулевой - вызывается KeYieldExecution(0).

    Итого, Sleep(0) и SwitchToThread сводятся к KeYieldExecution(0), только Sleep(0) приходит к ней чуть более длинным путём с доп. проверкой, поэтому оптимальнее вызывать SwitchToThread.

    NtYieldExecution:
    upload_2020-6-21_20-44-16.png

    KeDelayExecutionThread:
    upload_2020-6-21_20-43-47.png
    --- Сообщение объединено, 21 июн 2020 ---
    Планировщик переключает потоки, независимо от того, переходят они в ядро или нет, поэтому потоки с низким приоритетом всё равно будут выполняться (кроме случаев, когда у твоего процесса и потока реалтаймовый приоритет).
    Однако, твой поток по-прежнему могут прервать из ядра, т.к. с реалтаймовым приоритетом он продолжает работать на PASSIVE_LEVEL.
    --- Сообщение объединено, 21 июн 2020 ---
    Хотя начал подробнее смотреть и засомневался, попадём ли мы вообще в блок с KeYieldExecution(0)...
    Ведь, похоже, самое первое условие не выполнится - поток не Alertable, в Interval ноль, в WaitMode будет UserMode (1).

    Поторопился с ответом...
     
    M0rg0t, hiddy и Rel нравится это.
  3. Indy_

    Indy_ Well-Known Member

    Публикаций:
    4
    Регистрация:
    29 апр 2011
    Сообщения:
    4.775
    Jin X,

    Если установить реалтайм приоритет, получить число переключений контекста(SYSTEM_THREAD_INFORMATION.ContextSwitches), затем покрутить цикл в 10 секунд и вновь запросить число переключений - будет всего несколько, которые были при вызове сервиса до смены приоритета.

    Если в цикл вставить ZwYieldExecution возвращается ~73k, те 7k/sec переключений. При нормальном приоритете переключений ~100/sec. Тоесть сервис делает то что должен - принудительно переключает контекст, отдаёт время потока.
     
  4. Jin X

    Jin X Active Member

    Публикаций:
    0
    Регистрация:
    15 янв 2009
    Сообщения:
    369
    Адрес:
    Кольца Сатурна
    Нууу... если посмотреть на Sleep, то там как-то всё замороченнее. И судя по описанию, там вроде бы ещё должна быть проверка приоритета (правда, не на всех виндах).

    Как я понял из этого документа, потоки с более низким приоритетом выполняются, только если более приоритетные потоки ждут (к примеру, GetMessage, Sleep, WaitFor...), в противном случае, скажем, если у меня просто цикл каких-то вычислений, менее приоритетные будут тупо ожидать. Разве не так?

    Хорошо, а после возврата у меня же будет обновлённый квант, так ведь? Т.е. пару-тройку десятков миллисек я могу делать всё, что мне надо (если, конечно, не появится более приоритетный поток).
    Ну или... если более приоритетный поток не выйдет из спячки (те же WaitFor...).
     
  5. Indy_

    Indy_ Well-Known Member

    Публикаций:
    4
    Регистрация:
    29 апр 2011
    Сообщения:
    4.775
    Jin X,

    > Т.е. пару-тройку десятков миллисек я могу делать всё, что мне надо

    Принудительно отдать квант что бы получить начало следующего ?
    Зачем это нужно, какая задача ?
     
  6. Jin X

    Jin X Active Member

    Публикаций:
    0
    Регистрация:
    15 янв 2009
    Сообщения:
    369
    Адрес:
    Кольца Сатурна
    Замер времени чтения из памяти (кэш – не кэш).
     
  7. Indy_

    Indy_ Well-Known Member

    Публикаций:
    4
    Регистрация:
    29 апр 2011
    Сообщения:
    4.775
    Jin X,

    Тоесть профайл в виде значения снять ?

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

    Обнаружить начало кванта просто в x64, планировщик фиксит gs.
     
  8. Jin X

    Jin X Active Member

    Публикаций:
    0
    Регистрация:
    15 янв 2009
    Сообщения:
    369
    Адрес:
    Кольца Сатурна
    А если будут переключения контекста?

    Расскажу подробнее.
    Решил я сделать тест для определения размера линии кэша. Но не через cpuid (это всё есть), а через rdtsc. Цель – будем считать, что по приколу. Понятное дело, что [почти] всегда должно выходить 64, но суть не в этом, а в том, что хочется сделать так, чтобы прога работала на разных процессорах. Будем проверять размеры 128, 64, 32 байт (меньше, думаю, смысла нет, больше тоже).

    Технология такая (делал в VS, конфиг Release x86):
    1. Выделяю кусок памяти (1 Мб или 100 Мб, чтобы не попал в L3), создаю доп. массив с индексами/смещениями элементов с шагом 256 байт. Этот массив перемешиваю (чтоб затруднить процу предсказание обращений к памяти с предварительной загрузкой данных в кэш). Без этого (проверено) вообще не работает.
    2. Выставляю потоку приоритет TIME_CRITICAL.
    3. Далее два вложенный цикла: первый step = 128, 64, 32; второй – перебор элементов массива смещений (i).
    4. В каждом цикле делаю: SwitchToThread, далее замеряю холостой цикл, замеряю чтение из куска памяти dword'а по смещению i, замеряю чтение из куска памяти dword'а по смещению (i+step-4). Вычитаю из 2-х последних холостой цикл. Замер через mfence/rdtsc/mfence. Выравнивание адреса i по step делается.
    5. Суммирую все значения по всем внутренним итерациям.
    6. Во внешнем цикле: если разница между первыми и вторыми чтениями хотя бы раза в 4, значит размер линии кэша = step, выходим, иначе goto 4.

    Результаты:
    • На Intel Sandy Bridge (десктоп) работает (например, показывает 239/206 тактов на цикл для step=128 (иногда, правда, раза в 1,5 различаются значени), 221/18 тактов на цикл для step=64).
    • На AMD Ryzen 5 2600U (ноут) показывает 128 байт. Причём, если расширить тест до step=1024, например, тест показывает размер 256 байт (причём, там значения 297/9, например). Почему так? Хотя, если выделять не 100 Мб, а 1 Мб, вторые значения на step=256 и 128 больше (267/27, к примеру), чем на step=64 (277/2).

    Бывает ещё и такое:
    • Если выделять 100 Мб, а не 1 Мб, то первые значения (первое чтение из 2-х) в половине тестов (step'ов) будут в 2-3 (а иногда и раз в 5) раза ниже, чем при других step'ах. На 1 Мб такого эффекта не наблюдается.
    • Вариант на асме (асм-вставки п.3-6, описанных выше) работает так же в конфиге Debug, но в Release скорость первого чтения для самого старшего step (1024 или 128 – неважно, первая внешняя итерация) больше раз в 10. Причём, как на Intel, так и на AMD. Но по этому поводу я ещё посмотрю, может, где-то баг. Но почему тогда в Debug бага нет?

    Но самый главный вопрос – почему на AMD чтение второго значения с шагом 252 байта такое быстрое, как будто линия кэша = 256 байтам?
     
  9. Indy_

    Indy_ Well-Known Member

    Публикаций:
    4
    Регистрация:
    29 апр 2011
    Сообщения:
    4.775
    Jin X,

    > А если будут переключения контекста?

    В твоём случае это важно. Тут были тесты melt&spectre, твой замер и есть эта атака. Так вот проблема не в самой ошибке вводимой планированием была, а в сбросе всех частей механизма трансляции, когда планировщик свопит поток, то загружает cr3, а это сбрасывает все кэши. Поэтому при включённом планировании ничего толком подобного(статистику по кэшам) на нт замерить не получится. Тогда наконец понятно для чего тебе отключать планировщик :)
    --- Сообщение объединено, 22 июн 2020 ---
    Jin X,

    Можно обнаружить планирование на 64, это связано с ошибками compat mode https://wasm.in/threads/access-violation-pri-vozvrate-iz-64-bitnogo-rezhima.33604/#post-415896

    Механизм фикса сегментов неизменен XP%10:

    Код (Text):
    1. ; If the user segment selectors have been changed, then reload them with
    2. ; their cannonical values.
    3. ;
    4. ; N.B. The following code depends on the values defined in ntamd64.w that
    5. ;      can be loaded in ds, es, fs, and gs. In particular an "and" operation
    6. ;      is used for the below comparison.
    7. ;
    8.  
    9.         mov     eax, ds                 ; compute sum of segment selectors
    10.         mov     ecx, es                 ;
    11.         and     eax, ecx                ;
    12.         mov     ecx, gs                 ;
    13.         and     eax, ecx                ;
    14.         cmp     ax, (KGDT64_R3_DATA or RPL_MASK) ; check if sum matches
    15.         je      short KiSC25            ; if e, sum matches expected value
    16.         mov     ecx, KGDT64_R3_DATA or RPL_MASK ; reload user segment selectors
    17.         mov     ds, ecx                 ;
    18.         mov     es, ecx                 ;
    19.  
    20. ;
    21. ; N.B. The following reload of the GS selector destroys the system MSR_GS_BASE
    22. ;      register. Thus this sequence must be done with interrupt off.
    23. ;
    24.  
    25.         mov     eax, (PcSelf - PcPrcb)[rbx] ; get current PCR address
    26.         mov     edx, (PcSelf - PcPrcb + 4)[rbx] ;
    27.         cli                             ; disable interrupts
    28.         mov     gs, ecx                 ; reload GS segment selector
    29.         mov     ecx, MSR_GS_BASE        ; get GS base MSR number
    30.         wrmsr                           ; write system PCR base address
    31.         sti                             ; enable interrupts
    - wrk

    Хороший способ.
     
    Последнее редактирование: 22 июн 2020