Вводная часть
В своё время, первый 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 сделала акцент в своей документации – вот цитата из неё:
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 счётчик тактов работает на постоянной частоте ядра и на него не влияют внешние факторы, хотя на самом деле температурный режим здесь не учитывается, т.к. он попадает под определение "форс-мажор". Однако и это уже большой плюс в их копилку! В графическом виде с грубыми штрихами, нововведение заключается в следующем..
Раньше, за основу бралась системная шина, частоту которой можно было править в биосе. Дальше к ней применялся множитель 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 счётчик доступен и на запись, но только в младшую его часть, при этом старшая часть обнуляется.
Начиная с микро/архитектуры процессоров 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 с аргументом нуль:
Если мы хотим получить строку с именем процессора, то CPUID придётся в цикле вызывать сразу 3-раза, вскармливая ему следующий аргумент на каждой итерации. Дело в том, что под имя резервируется 48-байт, а инструкция CPUID не может работать с указателями на память – она возвращает инфу исключительно в регистры, которые нужно будет сбрасывать в приёмный буфер. В результате получим готовую строкуг типа: "Pentium(R) Dual-Core CPU E5200 @ 2.5GHz".
Не знаю, что там курят инженеры, но строку мы получаем в абсолютно неотформатированном виде, ..например в предложении выше между словами CPU и E5200 могут быть 5-пробелов, от которых нужно будет избавляться. Более того, зачем-то они расположили строку по-правому краю зарезервированных байт.. т.е. строка начинается с дополненными до 48-ми байт кучи пробелов - спрашивается зачем?:
В демонстрационном примере, я собрал всё выше/сказанное под один капот.
Изначально мы не знаем, сколько уровней запроса-информации поддерживает инструкция 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-ядра на консоль, иначе – пропускаем этот шаг. Вот пример реализации:
Запустив этот код на своём стационаре Win7 я обнаружил, что реальная частота его процессора даже чуть выше заявленной 2.5GHz, зато отсутствует поддержка расширенной версии RDTSCP. А это и не удивительно, поскольку она появилась только в м/архитектуре процессоров "Nehalem", а у меня устаревший "Wolfdale".
Зато на буке с десяткой стоит более современный процессор и ему не чужды нововведения Intel, хотя реальная его частота просела на 5 kHz, и отличается от указанной вендором. Судя по подписи RDTSCP в регистре ECX, код исполнялся на нулевом ядре:
Под занавес..
На этом покончим с таймерами и в следующей части ознакомимся с тонкостями профилирования кода, для выявления т.н. "горячих точек" программы. Без понимания элементарных основ не возможно разобраться во-всём этом, т.к. операционная система в каждый момент времени ведёт себя далеко не стабильно. В этой области есть много моментов, на которые следует обратить внимание, если мы хотим получить приближённый к действительности результат. А пока, в таблице ниже я собрал основные свойства рассмотренных ранее таймеров, чтобы можно было сделать выводы: