Статья Системные таймеры, Часть[5] – Счётчик процессора TSC

Предыдущие части:

1.
Общие сведения. Legacy-таймеры PIT и RTC.
2. Шина PCI – таймер менеджера питания ACPI.
3. ACPI таблицы – таймер HPET.
4. Таймер контролёра Local-APIC.
5. Счётчик процессора TSC.
6.
Win - профилирование кода.
---------------------------------------


Вводная часть

В своё время, первый 32-битный процессор i80386 наделал много шуму. Регистры в 2-раза большей разрядности и виртуальная память 4Gb казалась из мира фантастики. На его обкатку и реализацию более совершенных идей Интелу потребовалось без малого 10-лет, и в 1993-году мир увидел духовного преемника 486 – Pentium.

Ещё тогда инженеров заинтересовал вопрос, сколько инструкций за такт сможет выполнить процессор на скорости 200 MHz? Для решения этой задачи, они выделили один модельно-специфичный регистр под номером MSR.10h и с каждым тактом ядра стали увеличивать его на 1. В результате получили работающий непосредственно на частоте процессора, инкрементный счётчик. Вычисление скорости инструкций подразумевало двойное чтение этого счётчика, в промежутке которых вставляли тестируемый код.

Позже выяснилось, что при длительной работе CPU 32-битного значения 0xFFFFFFFF = 4.294.967.295 для этих целей не хватает, т.к. переполнение регистра обратно в нуль влекло за собой ошибку в расчётах. Так MSR.10h разбух с 32 до 64-бит (поженили 2-регистра) и чтобы он не был безликим, окрестили его в IA32_Time_Stamp_Counter. Отметим, что TSC это не таймер – он не генерит никаких прерываний, а просто считает в себя такты процессора.

В своих доках Intel гарантирует, что 64-битный счётчик не переполнится в течении 10-ти лет непрерывной работы CPU (по команде Reset он сбрасывается в нуль), хотя на практике этот период намного больше и пропорционален текущей частоте. Допустим f процессора равна 3.0 GHz, а это в герцах 3.000.000.000. Значит за одну секунду он "простучит" именно столько раз, и на такую-же единицу увеличится TSC. Если перевести 3 млрд в hex, то получим всего 0xB2D05E00 в сек, а дальше.. обратите внимание на столбец HEX ниже:

MsrTime.png


Если-бы TSC был 32-битный, то на этом процессоре он переполнился-бы буквально на 2-ой секунде. Однако вдвое большая разрядность позволяет сбрасывать в него уже астрономические значения, и даже по истечении 80-лет процессор 3 GHz будет продолжать зомбировать нас своим кадилом без переполнения счётчика. Но если учесть, что 3 GHz для Intel'a не предел, то такой запас вполне оправдан. К примеру анонсированный на январь этого года
зарекается работать на частоте 5.3 GHz, соответственно и скорость переполнения его регистра TSC будет уже выше (с деталями этого процессора можно ).


На заднем дворе..

Техническая модель счётчика TSC далека от идеала и её нельзя воспринимать как эталон времени в системе. И речь здесь даже не о том, что если мы хотим использовать TSC в качестве программного таймера, то сначала должны откалибровать его.. т.е. узнать, сколько раз простучит CPU на текущей своей частоте в течении одной милли/секунды. Соответственно в любом случае на время калибровки нужно будет воспользоваться услугами дополнительного таймера, на роль которого как-нельзя лучше подходит анархист RTC со-своим независимым кварцем 32.768 кГц, или-же высокоточный таймер событий HPET.

Если реализацию TSC разложить на примитивы, то оказывается что в этом "академгородке" мир вращается не вокруг тактовой частоты процессора, а вокруг частоты системной шины чипсета. Ведь что такое частота процессора? Это произведение его множителя на частоту шины. Например если в биосе Bus-frequency =200 MHz, а CPU-ratio =12, то получим CPU-frequency =2400. Таким образом, на ход TSC влияют внешние факторы – во-первых частота шины, во-вторых температура ядра.

Рассмотрим случай, когда мы откалибровали один из счётчиков многоядерного процессора, после чего какой-то левый программный поток загрузил соседнее ядро на все 100 – это типичная ситуация на МР-системах. Тогда при достижении определённого в биос температурного значения, процессор выставит на шину сигнал PROCHOT#, что означает Processor Hot (горячий). В свою очередь чипсет примет этот сигнал и на какое-то время сбросит как питание, так и частоту ядра, чтобы предотвратить выход его из строя. Эти действа происходят в фоне и система никак даже не оповещает нас об этом. Соответственно частота процессора падает, и теперь нам нужно заново калибровать свой счётчик TSC. То-есть частота процессора жёстко не фиксирована и постоянно плавает в некотором диапазоне. Вот как демонстрирует это Intel в своих доках:

Prochot.png


Здесь видно, что при достижении критической температуры процессора, логика по термодатчику сначала сбрасывает частоту, после чего с заданным шагом уменьшает и питание VID – Voltage Identificator. На всех материнских платах для этого предусмотрен т.н. VRM или
Питание будет стремиться к нулю до тех пор, пока температура ядра не восстановится, после чего процессор снимет PROCHOT#, и всё вернётся на круги свои.

Для инженеров (которым нужен был TSC с фиксированной частотой под каждый процессор) это представляло проблему, и решить её кстати так и не удалось до сих-пор – счётчик как плавал, так и плавает в обе стороны. Однако какие-то попытки с их стороны всё-же были, на что Intel сделала акцент в своей документации – вот цитата из неё:

17.15 TIME-STAMP COUNTER
Processor families increment the time-stamp counter differently:

• For Pentium M, Pentium-4, Intel Xeon and for P6 family processors: the time-stamp counter increments with every internal processor clock cycle. The internal processor clock cycle is determined by the current core-clock to bus-clock ratio. Intel SpeedStep® technology transitions may also impact the processor clock.

• For Intel Core Duo, Intel Xeon-5100, Core-2-Duo and Atom processors: the time-stamp counter increments at a constant rate. On certain processors, the TSC frequency may not be the same as the frequency in the brand string.
Constant TSC behavior ensures that the duration of each clock tick is uniform and supports the use of the TSC as a wall clock timer even if the processor core changes frequency. This is the architectural behavior moving forward.

То-есть разраб утверждает, что на процессорах выше Pentium-4 счётчик тактов работает на постоянной частоте ядра и на него не влияют внешние факторы, хотя на самом деле температурный режим здесь не учитывается, т.к. он попадает под определение "форс-мажор". Однако и это уже большой плюс в их копилку! В графическом виде с грубыми штрихами, нововведение заключается в следующем..

TSC_01.png


Раньше, за основу бралась системная шина, частоту которой можно было править в биосе. Дальше к ней применялся множитель CPU (Ratio, Multiplier) и получали его рабочую частоту CPU-Clock. Соответственно чем выше частота на выходе из чипсета, тем активнее считал счётчик TSC. Здесь всё ясно..

А вот на новых чипсетах (справа), аппаратную модель логирования тактов вынесли в отдельный домен. Теперь частота шины BCLK и её овер в биосе на счётчик уже не влияют. Тут к клокеру сразу применяется множитель CPU, а все остальные механизмы для счётчика прозрачны. К примеру в иерархии выше может стоять технология "Speed-Step", которая использует несколько предопределённых шаблонов напряжения и частоты, и переключается между ними в зависимости от нагрузки на процессор. Поэтому в доках и говорится, что значения TSC могут несколько отличаться от заявленной производителем частоты процессора. Кстати помимо TSC, в этом-же домене живут встроенная графика PEG (PCI-Ex Graphics) и шина обмена-данными DMI. В спеках на PCI-Express можно найти такую схему:

Peg_Dmi.png



Программный доступ

Одновременно с появлением в Pentium счётчика TSC, в набор инструкций процессора была включена инструкция ассемблера RDTSC (Read counter). Она возвращает значение 64-битного счётчика в регистровую пару EDX:EAX, причём в EDX сбрасываются старшие 32-бита, а в EAX – актуальные младшие. Запрет на использование юзером этой инструкции включается битом[2] TSCD в регистре конфигурации процессора CR4. Единичное состояние этого бита позволяет оперировать счётчиком только ядру, а нулевое – открывает доступ и юзеру. Прямым обращением к регистру MSR.10h счётчик доступен и на запись, но только в младшую его часть, при этом старшая часть обнуляется.

C-подобный:
;// прямое чтение/запись TSC из драйвера, или RMode
     mov   ecx,10h      ;// номер регистра MSR
     rdmsr              ;// EDX:EAX = значение счётчика
     mov   ecx,10h
     wrmsr              ;// write MSR.10h

;// чтение TSC с любого уровня
     rdtsc              ;// EDX:EAX = значение счётчика

Начиная с микро/архитектуры процессоров Intel под кодовым названием "Nehalem", к механизму подсчёта тактов было введено интересное дополнение. Суть его заключается в том, чтобы была возможность не только прочитать значение TSC, но и узнать, какому именно ядру оно принадлежит. Если учесть, что у каждого ядра свои регистры и свой счётчик TSC, то в этом есть какой-то смысл.

Для реализации задуманного, прицепом к MSR.10h инженеры выделили ещё один регистр, ..на этот раз MSR.C0000103h и назвали его IA32_TSC_AUX. В этом AUX-регистре хранится уникальный идентификатор ядра. Так появилась родственная к предыдущей.. инструкция RDTSCP, которая так-же возвращает 64-битный счётчик в пару EDX:EAX, но бонусом в регистре ECX ещё и подпись ядра. Итого её выхлоп получает уже в трёх регистрах ECX:EDX:EAX.

Поддержка процессором расширенной версии определяется инструкцией CPUID с аргументом EAX=0x80000001. Если она вернёт в EDX взведённый бит[27], значит процессор поддерживает RDTSCP. Польза от этой инструкции минимальна.. Поскольку она возвращает ID-ядра, то с её помощью можно отследить миграцию программных потоков с одного ядра процессора, на другое. Обычно если основной поток программы запущен на ядре#0, он на нём и исполняется. Однако это правило не действительно для дочерних потоков приложения, и системный планировщик Sheduler может тасовать их по свободным ядрам. Здесь и пригодится инструкция RDTSCP, которая при каждом вызове возвращает ID-ядра, на котором выполняется.

Кстати среди Win32-API есть группа функций xxAffinityMask(). К примеру SetProcessAffinityMask() позволяет битовой маской жёстко прописать ядра, на которых должен исполняться наш процесс.. например 1,2,3 или только 2,4. В купе с этой функцией, от инструкции RDTSCP можно выжать ещё больше профита.



CPUID – идентификация процессора

Когда процессор только сходит с производственного конвейера, он не может даже элементарно сложить два числа. Чтобы наделить его интеллектом, производитель встраивает в тушку процессора постоянную память micro/code-ROM и заливает в неё набор поддерживаемых им инструкций. Теперь декодер процессора сможет распознать входной поток данных и понять, что именно хочет от него программа. Память эта доступна на запись, что позволяет обновлять только микрокоды, не прибегая к полной замене CPU на новый. Если ваш проц не поддерживает какие-то инструкции, имеет смысл заглянуть на сайт производителя и скачать обновления его микрокодов.

Помимо азбуки в виде инструкций, в этот-же ROM производитель зашивает и характеристики данной модели CPU. Это огромная база-данных, в которой в мельчайших деталях расписаны все свойства и возможности процессора. Информация закодирована битовой маской, для расшифровки которой имеются специальные таблицы. Одну из таких таблиц я прикрепил в скрепке, чтобы была возможность хотя-бы поверхностно ознакомится с ней.

Прочитать идентификатор CPU можно специальной инструкцией CPUID. В качестве аргумента она ждёт в регистре EAX код запрашиваемой информации. Имеется стандартный набор кодов с EAX=0x0000_xxxx, и расширенный набор с EAX=0x8000_xxxx. Приняв аргумент, на выходе CPUID заполняет информацией 4-регистра процессора EAX,EBX,ECX,EDX, после чего нам остаётся только проверить нужные биты в них. Например чтобы получить строку вендора, достаточно вызвать CPUID с аргументом нуль:


Код:
Standard level: 0000_0000h
--------------------------
Input.: EAX=0000_0000h. Get maximum supported standard level and vendor ID string
Output: EAX=xxxx_xxxxh maximum supported standard level
        EBX-EDX-ECX vendor ID string
        ------------------------------
        GenuineIntel = Intel processor
        UMC UMC UMC  = UMC processor
        AuthenticAMD = AMD processor
        CyrixInstead = Cyrix processor
        NexGenDriven = NexGen processor
        CentaurHauls = Centaur processor
        RiseRiseRise = Rise Technology processor
        SiS SiS SiS  = SiS processor
        GenuineTMx86 = Transmeta processor
        Geode by NSC = National Semiconductor processor

Если мы хотим получить строку с именем процессора, то CPUID придётся в цикле вызывать сразу 3-раза, вскармливая ему следующий аргумент на каждой итерации. Дело в том, что под имя резервируется 48-байт, а инструкция CPUID не может работать с указателями на память – она возвращает инфу исключительно в регистры, которые нужно будет сбрасывать в приёмный буфер. В результате получим готовую строкуг типа: "Pentium(R) Dual-Core CPU E5200 @ 2.5GHz".

Не знаю, что там курят инженеры, но строку мы получаем в абсолютно неотформатированном виде, ..например в предложении выше между словами CPU и E5200 могут быть 5-пробелов, от которых нужно будет избавляться. Более того, зачем-то они расположили строку по-правому краю зарезервированных байт.. т.е. строка начинается с дополненными до 48-ми байт кучи пробелов - спрашивается зачем?:


Код:
Extended levels: 8000_0002h, 8000_0003h, and 8000_0004h
-------------------------------------------------------
Input.: EAX=8000_0002h get processor name string (part 1)
        EAX=8000_0003h get processor name string (part 2)
        EAX=8000_0004h get processor name string (part 3)
Output: EAX-EBX-ECX-EDX =  processor name string

В демонстрационном примере, я собрал всё выше/сказанное под один капот.
Изначально мы не знаем, сколько уровней запроса-информации поддерживает инструкция CPUID. Поэтому подсовываем ей аргумент EAX=0, и в этом-же регистре на выходе получаем макс.возможный код запроса. Эту-же операцию проводим и с расширенным набором EAX=80000000h. Дальше получаем вендора, строку с именем процессора, и код различных его характеристик (см.доку в скрепке).

На сл.этапе, воспользовавшись функцией GetProcessAffinityMask() узнаём кол-во ядер процессора – эта fn. возвращает их в виде битовой маски, например 4-ядра будут представлены как 0000.1111b. Чтобы вычислить реальную (а не заявленную) частоту процессора, мы используя счётчик TSC посчитаем, сколько процессор сделает тактов за 1-сек – это и будет его частотой. Бонусом через CPUID.80000006h можно показать размер кэша L2.

На финишной прямой, при помощи того-же CPUID сбросим на консоль поддержку стандартной (CPUID.1h) и расширенной (CPUID.80000001h) версий RDTSC и RDTSCP соответственно. Напомню, что поддержка первой определяется битом[4] в регистре EDX, а расширенной – битом[27]. Если код определит наличие усовершенствованной RDTSCP, то выводим его счётчик и ID-ядра на консоль, иначе – пропускаем этот шаг. Вот пример реализации:


C-подобный:
format   pe console
include 'win32ax.inc'
entry    start
;//---------
.data
caption   db  13,10,' CPU info v0.1'
          db  13,10,' ************************'
          db  13,10,' CPUID max.Std,level.: 0x%08X'
          db  13,10,' CPUID max.Ext.level.: 0x%08X',0
vend      db  13,10
          db  13,10,' Vendor..............: %s',0
name      db  13,10,' Name................: %s',0
features  db  13,10,' Features............: %X',0
cores     db  13,10
          db  13,10,' CPU count...........: %d',0
freq      db  13,10,' CPU frequence.......: %d MHz',0
cache     db  13,10,' L2 cache size.......: %d Kb',0
tscInfo   db  13,10
          db  13,10,' RDTSC  support......: %d',0
tscP      db  13,10,' RDTSCP support......: %d',0
tscStamp  db  13,10,' RDTSC  counter......: 0x%08X%08X',0
tscpStamp db  13,10,' RDTSCP counter......: 0x%08X%08X'
          db  13,10,' RDTSCP core ID......: 0x%02X',0

pAffin    dd  0
sAffin    dd  0
cycle     dd  0
tscpFlag  db  0
cpu       db  64 dup(0)
buff      db  0
;//---------
.code
start:
;//(0)=== Макс.поддерживаемые уровни запросов CPUID
        xor     eax,eax            ;// аргумент(0)
        cpuid                      ;//
        push    eax                ;//
        mov     eax,0x80000000     ;// аргумент
        cpuid                      ;//
        pop     ebx                ;//
       cinvoke  printf,caption,ebx,eax

;//(1)=== Получаем строку вендора
        xor     eax,eax            ;// аргумент
        cpuid                      ;//
        mov     dword[buff+0],ebx  ;// кидаем его в буфер
        mov     dword[buff+4],edx  ;//
        mov     dword[buff+8],ecx  ;//
       cinvoke  printf,vend,buff   ;//

;//(2)=== Строка с именем процессора
;// нужно вызывать в цикле аргументы 0x80000002,3,4
        mov    [cycle],3      ;// кол-во повторов
        mov     edi,buff      ;// указатель на приёмник
        push    edi           ;//
        mov     eax,0x80000002 ;// аргумент на старте
@@:     push    eax            ;// запомнить его
        cpuid                 ;// очередной вызов..
        stosd                 ;// сбросить EAX по указателю EDI
        mov     eax,ebx       ;//
        stosd                 ;// сбросить EBX
        mov     eax,ecx       ;//
        stosd                 ;// сбросить ECX
        mov     eax,edx       ;//
        stosd                 ;// сбросить EDX
        pop     eax           ;// восстановить аргумент
        inc     eax           ;// увеличить его на 1
        dec     [cycle]       ;// уменьшить счётчик повторов
        jnz     @b            ;// повторить, пока он не нуль!

;//(3)=== Форматируем строку с именем процессора
;// (убираем дублируешиеся пробелы в буфере)
        pop     edi           ;// указатель на него
        mov     esi,edi       ;// он-же источник для стр.инструкций
        mov     ah,' '        ;// что искать..
@findDoubleSpace:             ;//
        lodsb                 ;// AL = очередной символ из ESI
        or      al,al         ;// конец строки? (проверка на маркер 0)
        je      @prn          ;// да..
        cmp     al,' '        ;// иначе: проверить на пробел
        jne     @fuck         ;// нет..
        cmp     ax,'  '       ;// иначе: проверить на дубль
        je      @f            ;// пропустить, если пара пробелов
@fuck:  stosb                 ;// иначе: перезаписать буф
@@:     xchg    ah,al         ;// сделать текущий символ, предыдущим
        jmp     @findDoubleSpace  ;// повторить, пока не встретим нуль..

@prn:   mov     byte[edi],0   ;// вставить маркер окончания строки
       cinvoke  printf,name,buff  ;// вывести строку на консоль!

;//(4)=== Код с характеристиками процессора ============
        mov     eax,1         ;// аргумент
        cpuid                 ;//
       cinvoke  printf,features,eax  ;// лежит в EAX

;//(5)=== Кол-во ядер CPU ==============================
;// здесь -1 дескриптор текущего процесса
        invoke   GetProcessAffinityMask,-1,pAffin,sAffin
        mov      eax,[pAffin] ;// получили маску
        xor      ebx,ebx      ;// здесь будет кол-во из маски
@@:     shr      eax,1        ;// вытолнуть мл.бит
        inc      ebx          ;// кол-во ядер +1
        or       eax,eax      ;// проверить оставшуюся маску на нуль
        jne      @b           ;// повторить, если нет..
       cinvoke   printf,cores,ebx  ;// в EBX лежат ядра!

;//(6)=== Реальная частота процессора ==================
;// читаем TSC за 0,5 сек с контрольным/вторым выстрелом
;// здесь CPUID выступает в качестве инструкции-сериализации,
;// чтобы очистить конвейер процессора от имеющихся в нём инструкций
        mov      [cycle],2    ;// повторов..
@@:     xor      eax,eax      ;//
        cpuid                 ;// теперь конвейер чистый!
        rdtsc                 ;// берём такты в пару EDX:EAX
        push     eax          ;// запомнить мл.часть
        invoke   Sleep,500    ;// накапливаем счётчик TSC..
        rdtsc                 ;// повторный вызов
        pop      ebx          ;// EBX = счётчик на входе
        sub      eax,ebx      ;// TSC за 0,5 сек
        mov      ebx,500000   ;// перевести в Мбайты
        xor      edx,edx      ;//
        div      ebx          ;//
        dec      [cycle]      ;//
        jnz      @b           ;// повторить для точности..
       cinvoke   printf,freq,eax  ;// EAX = реальная частота!

;//(7)=== Кэш L2 =======================================
        mov      eax,0x80000006
        cpuid                 ;//
        shr      ecx,16       ;// лежит в битах[31:16]
       cinvoke   printf,cache,ecx

;//(8)=== Тест поддержки TSC и TSCP ====================
        mov      eax,1        ;//
        cpuid                 ;//
        mov      eax,1        ;//
        bt       edx,4        ;// RDTSC
        jc       @f           ;//
        dec      al           ;//
@@:    cinvoke   printf,tscInfo,eax

        mov      eax,0x80000001
        cpuid                 ;//
        mov      eax,0        ;//
        bt       edx,27       ;// RDTSCP
        jnc      @f           ;//
        inc      al           ;//
        inc      [tscpFlag]   ;// взвести флаг, если имеется поддержка
@@:    cinvoke   printf,tscP,eax

;//(9)=== Читаем текущие счётчики TSC и TSCP ===========
        rdtsc                 ;//
       cinvoke   printf,tscStamp,edx,eax

        cmp      [tscpFlag],0 ;// проверить флаг в переменной
        je       @f           ;// пропустить, если он нуль
        rdtscp                ;// иначе: вызов!
       cinvoke   printf,tscpStamp,edx,eax,ecx  ;// ECX = id-ядра

@@:    cinvoke  gets,buff     ;// ждём клаву..
       cinvoke  exit,0        ;// на выход!

;//**********************************
section '.idata' import data readable
library  msvcrt,'msvcrt.dll',kernel32,'kernel32.dll'
import   msvcrt, printf,'printf',exit,'exit',gets,'gets';,gcvt,'_gcvt'
include 'api\kernel32.inc'

Win7.png


Запустив этот код на своём стационаре Win7 я обнаружил, что реальная частота его процессора даже чуть выше заявленной 2.5GHz, зато отсутствует поддержка расширенной версии RDTSCP. А это и не удивительно, поскольку она появилась только в м/архитектуре процессоров "Nehalem", а у меня устаревший "Wolfdale".

Зато на буке с десяткой стоит более современный процессор и ему не чужды нововведения Intel, хотя реальная его частота просела на 5 kHz, и отличается от указанной вендором. Судя по подписи RDTSCP в регистре ECX, код исполнялся на нулевом ядре:

Win10.png



Под занавес..

На этом покончим с таймерами и в следующей части ознакомимся с тонкостями профилирования кода, для выявления т.н. "горячих точек" программы. Без понимания элементарных основ не возможно разобраться во-всём этом, т.к. операционная система в каждый момент времени ведёт себя далеко не стабильно. В этой области есть много моментов, на которые следует обратить внимание, если мы хотим получить приближённый к действительности результат. А пока, в таблице ниже я собрал основные свойства рассмотренных ранее таймеров, чтобы можно было сделать выводы:

timers.png
 

Вложения

  • x86_CPUID.zip
    32 КБ · Просмотры: 1 346
Последнее редактирование:
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!