РАЗБОР
Статья
Системные таймеры, Часть[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-битного значения
В своих доках Intel гарантирует, что 64-битный счётчик не переполнится в течении 10-ти лет непрерывной работы CPU (по команде Reset он сбрасывается в нуль), хотя на практике этот период намного больше и пропорционален текущей частоте. Допустим f процессора равна 3.0 GHz, а это в герцах 3.000.000.000. Значит за одну секунду он "простучит" именно столько раз, и на такую-же единицу увеличится TSC. Если перевести 3 млрд в hex, то получим всего
Если-бы TSC был 32-битный, то на этом процессоре он переполнился-бы буквально на 2-ой секунде. Однако вдвое большая разрядность позволяет сбрасывать в него уже астрономические значения, и даже по истечении 80-лет процессор 3 GHz будет продолжать зомбировать нас своим кадилом без переполнения счётчика. Но если учесть, что 3 GHz для Intel'a не предел, то такой запас вполне оправдан. К примеру анонсированный на январь этого года Core i9-10900K зарекается работать на частоте 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 в своих доках:
Здесь видно, что при достижении критической температуры процессора, логика по термодатчику сначала сбрасывает частоту, после чего с заданным шагом уменьшает и питание VID – Voltage Identificator. На всех материнских платах для этого предусмотрен т.н. VRM или Voltage Regulation Module. Питание будет стремиться к нулю до тех пор, пока температура ядра не восстановится, после чего процессор снимет PROCHOT#, и всё вернётся на круги свои.
Для инженеров (которым нужен был TSC с фиксированной частотой под каждый процессор) это представляло проблему, и решить её кстати так и не удалось до сих-пор – счётчик как плавал, так и плавает в обе стороны. Однако какие-то попытки с их стороны всё-же были, на что Intel сделала акцент в своей документации – вот цитата из неё:
То-есть разраб утверждает, что на процессорах выше Pentium-4 счётчик тактов работает на постоянной частоте ядра и на него не влияют внешние факторы, хотя на самом деле температурный режим здесь не учитывается, т.к. он попадает под определение "форс-мажор". Однако и это уже большой плюс в их копилку! В графическом виде с грубыми штрихами, нововведение заключается в следующем..
Раньше, за основу бралась системная шина, частоту которой можно было править в биосе. Дальше к ней применялся множитель CPU (Ratio, Multiplier) и получали его рабочую частоту CPU-Clock. Соответственно чем выше частота на выходе из чипсета, тем активнее считал счётчик TSC. Здесь всё ясно..
А вот на новых чипсетах (справа), аппаратную модель логирования тактов вынесли в отдельный домен. Теперь частота шины BCLK и её овер в биосе на счётчик уже не влияют. Тут к клокеру сразу применяется множитель CPU, а все остальные механизмы для счётчика прозрачны. К примеру в иерархии выше может стоять технология "Speed-Step", которая использует несколько предопределённых шаблонов напряжения и частоты, и переключается между ними в зависимости от нагрузки на процессор. Поэтому в доках и говорится, что значения TSC могут несколько отличаться от заявленной производителем частоты процессора. Кстати помимо TSC, в этом-же домене живут встроенная графика PEG (PCI-Ex Graphics) и шина обмена-данными DMI. В спеках на PCI-Express можно найти такую схему:
Программный доступ
Одновременно с появлением в Pentium счётчика TSC, в набор инструкций процессора была включена инструкция ассемблера RDTSC (Read counter). Она возвращает значение 64-битного счётчика в регистровую пару EDX:EAX, причём в EDX сбрасываются старшие 32-бита, а в EAX – актуальные младшие. Запрет на использование юзером этой инструкции включается битом[2]
Начиная с микро/архитектуры процессоров Intel под кодовым названием "Nehalem", к механизму подсчёта тактов было введено интересное дополнение. Суть его заключается в том, чтобы была возможность не только прочитать значение TSC, но и узнать, какому именно ядру оно принадлежит. Если учесть, что у каждого ядра свои регистры и свой счётчик TSC, то в этом есть какой-то смысл.
Для реализации задуманного, прицепом к MSR.10h инженеры выделили ещё один регистр, ..на этот раз MSR.C0000103h и назвали его IA32_TSC_AUX. В этом AUX-регистре хранится уникальный идентификатор ядра. Так появилась родственная к предыдущей.. инструкция RDTSCP, которая так-же возвращает 64-битный счётчик в пару
Поддержка процессором расширенной версии определяется инструкцией CPUID с аргументом
Кстати среди Win32-API есть группа функций xxAffinityMask(). К примеру SetProcessAffinityMask() позволяет битовой маской жёстко прописать ядра, на которых должен исполняться наш процесс.. например 1,2,3 или только 2,4. В купе с этой функцией, от инструкции RDTSCP можно выжать ещё больше профита.
CPUID – идентификация процессора
Когда процессор только сходит с производственного конвейера, он не может даже элементарно сложить два числа. Чтобы наделить его интеллектом, производитель встраивает в тушку процессора постоянную память micro/code-ROM и заливает в неё набор поддерживаемых им инструкций. Теперь декодер процессора сможет распознать входной поток данных и понять, что именно хочет от него программа. Память эта доступна на запись, что позволяет обновлять только микрокоды, не прибегая к полной замене CPU на новый. Если ваш проц не поддерживает какие-то инструкции, имеет смысл заглянуть на сайт производителя и скачать обновления его микрокодов.
Помимо азбуки в виде инструкций, в этот-же ROM производитель зашивает и характеристики данной модели CPU. Это огромная база-данных, в которой в мельчайших деталях расписаны все свойства и возможности процессора. Информация закодирована битовой маской, для расшифровки которой имеются специальные таблицы. Одну из таких таблиц я прикрепил в скрепке, чтобы была возможность хотя-бы поверхностно ознакомится с ней.
Прочитать идентификатор CPU можно специальной инструкцией CPUID. В качестве аргумента она ждёт в регистре
Если мы хотим получить строку с именем процессора, то CPUID придётся в цикле вызывать сразу 3-раза, вскармливая ему следующий аргумент на каждой итерации. Дело в том, что под имя резервируется 48-байт, а инструкция CPUID не может работать с указателями на память – она возвращает инфу исключительно в регистры, которые нужно будет сбрасывать в приёмный буфер. В результате получим готовую строкуг типа: "Pentium(R) Dual-Core CPU E5200 @ 2.5GHz".
Не знаю, что там курят инженеры, но строку мы получаем в абсолютно неотформатированном виде, ..например в предложении выше между словами CPU и E5200 могут быть 5-пробелов, от которых нужно будет избавляться. Более того, зачем-то они расположили строку по-правому краю зарезервированных байт.. т.е. строка начинается с дополненными до 48-ми байт кучи пробелов - спрашивается зачем?:
В демонстрационном примере, я собрал всё выше/сказанное под один капот.
Изначально мы не знаем, сколько уровней запроса-информации поддерживает инструкция CPUID. Поэтому подсовываем ей аргумент
На сл.этапе, воспользовавшись функцией GetProcessAffinityMask() узнаём кол-во ядер процессора – эта fn. возвращает их в виде битовой маски, например 4-ядра будут представлены как 0000.1111b. Чтобы вычислить реальную (а не заявленную) частоту процессора, мы используя счётчик TSC посчитаем, сколько процессор сделает тактов за 1-сек – это и будет его частотой. Бонусом через CPUID.80000006h можно показать размер кэша L2.
На финишной прямой, при помощи того-же CPUID сбросим на консоль поддержку стандартной (CPUID.1h) и расширенной (CPUID.80000001h) версий RDTSC и RDTSCP соответственно. Напомню, что поддержка первой определяется битом[4] в регистре
Запустив этот код на своём стационаре Win7 я обнаружил, что реальная частота его процессора даже чуть выше заявленной 2.5GHz, зато отсутствует поддержка расширенной версии RDTSCP. А это и не удивительно, поскольку она появилась только в м/архитектуре процессоров "Nehalem", а у меня устаревший "Wolfdale".
Зато на буке с десяткой стоит более современный процессор и ему не чужды нововведения Intel, хотя реальная его частота просела на 5 kHz, и отличается от указанной вендором. Судя по подписи RDTSCP в регистре ECX, код исполнялся на нулевом ядре:
Под занавес..
На этом покончим с таймерами и в следующей части ознакомимся с тонкостями профилирования кода, для выявления т.н. "горячих точек" программы. Без понимания элементарных основ не возможно разобраться во-всём этом, т.к. операционная система в каждый момент времени ведёт себя далеко не стабильно. В этой области есть много моментов, на которые следует обратить внимание, если мы хотим получить приближённый к действительности результат. А пока, в таблице ниже я собрал основные свойства рассмотренных ранее таймеров, чтобы можно было сделать выводы:
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 ниже:Если-бы TSC был 32-битный, то на этом процессоре он переполнился-бы буквально на 2-ой секунде. Однако вдвое большая разрядность позволяет сбрасывать в него уже астрономические значения, и даже по истечении 80-лет процессор 3 GHz будет продолжать зомбировать нас своим кадилом без переполнения счётчика. Но если учесть, что 3 GHz для Intel'a не предел, то такой запас вполне оправдан. К примеру анонсированный на январь этого года Core i9-10900K зарекается работать на частоте 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 в своих доках:
Здесь видно, что при достижении критической температуры процессора, логика по термодатчику сначала сбрасывает частоту, после чего с заданным шагом уменьшает и питание VID – Voltage Identificator. На всех материнских платах для этого предусмотрен т.н. VRM или Voltage Regulation Module. Питание будет стремиться к нулю до тех пор, пока температура ядра не восстановится, после чего процессор снимет PROCHOT#, и всё вернётся на круги свои.
Для инженеров (которым нужен был TSC с фиксированной частотой под каждый процессор) это представляло проблему, и решить её кстати так и не удалось до сих-пор – счётчик как плавал, так и плавает в обе стороны. Однако какие-то попытки с их стороны всё-же были, на что Intel сделала акцент в своей документации – вот цитата из неё:
То-есть разраб утверждает, что на процессорах выше Pentium-4 счётчик тактов работает на постоянной частоте ядра и на него не влияют внешние факторы, хотя на самом деле температурный режим здесь не учитывается, т.к. он попадает под определение "форс-мажор". Однако и это уже большой плюс в их копилку! В графическом виде с грубыми штрихами, нововведение заключается в следующем..
Раньше, за основу бралась системная шина, частоту которой можно было править в биосе. Дальше к ней применялся множитель CPU (Ratio, Multiplier) и получали его рабочую частоту CPU-Clock. Соответственно чем выше частота на выходе из чипсета, тем активнее считал счётчик TSC. Здесь всё ясно..
А вот на новых чипсетах (справа), аппаратную модель логирования тактов вынесли в отдельный домен. Теперь частота шины BCLK и её овер в биосе на счётчик уже не влияют. Тут к клокеру сразу применяется множитель CPU, а все остальные механизмы для счётчика прозрачны. К примеру в иерархии выше может стоять технология "Speed-Step", которая использует несколько предопределённых шаблонов напряжения и частоты, и переключается между ними в зависимости от нагрузки на процессор. Поэтому в доках и говорится, что значения TSC могут несколько отличаться от заявленной производителем частоты процессора. Кстати помимо TSC, в этом-же домене живут встроенная графика PEG (PCI-Ex Graphics) и шина обмена-данными DMI. В спеках на PCI-Express можно найти такую схему:
Программный доступ
Одновременно с появлением в 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 я обнаружил, что реальная частота его процессора даже чуть выше заявленной 2.5GHz, зато отсутствует поддержка расширенной версии RDTSCP. А это и не удивительно, поскольку она появилась только в м/архитектуре процессоров "Nehalem", а у меня устаревший "Wolfdale".
Зато на буке с десяткой стоит более современный процессор и ему не чужды нововведения Intel, хотя реальная его частота просела на 5 kHz, и отличается от указанной вендором. Судя по подписи RDTSCP в регистре ECX, код исполнялся на нулевом ядре:
Под занавес..
На этом покончим с таймерами и в следующей части ознакомимся с тонкостями профилирования кода, для выявления т.н. "горячих точек" программы. Без понимания элементарных основ не возможно разобраться во-всём этом, т.к. операционная система в каждый момент времени ведёт себя далеко не стабильно. В этой области есть много моментов, на которые следует обратить внимание, если мы хотим получить приближённый к действительности результат. А пока, в таблице ниже я собрал основные свойства рассмотренных ранее таймеров, чтобы можно было сделать выводы:
AI-выжимка
сгенерировано ИИ
Тезисы статьи скоро
Статья читается полностью. Тезисы со ссылками на разделы появятся позже.
Содержание
Codeby Academy
Практика и мастерство
От основ до продвинутого — программы для практиков Codeby.
Перейти к курсу →
Комментарии
0