Экстремальный разгон процессора

         

Как мы будем действовать


Счетчики производительности по разному реализованы в процессорах семейства P6 (к которым принадлежат Pentium Pro/Pentium-II/Pentium-3) и Pentium-4. Никаких принципиальных различий нет, но коды счетчиков производительности и номера MSR-регистров слегка другие и код, предназначенный, для P6, попав на Pentium-4, вызывает исключение, как правило, заканчивающиеся голубым экраном смерти под WindowsNT.

Главным образом мы будем говорить про семейство процессоров P6 и в этом есть свой резон, во-первых, они в наибольшей степени нуждаются в разгоне (Pentium-4 и без того производительны), и, во-вторых, в отличии от Pentium-4 они не поддерживают автоматическое снижение тактовой частоты при перегреве, уменьшая свой разгонный потенциал. Но, как бы там ни было, перенести код с P6 на Pentium-4 сможет любой программист, даже начинающий, так что не будет отвлекаться на несущественные различия между этими платформами, а сразу перейдем к делу.

Процессоры семейства P6 несут на своем борту два счетчика производительности, физически представляющие собой внутренние 40-битные MSR-регистры — PerfCtr0 и PerfCtr1, каждый из которых может подсчитывать события определенного вида, коды которых задаются другими MSR-регистрами — PerfEvtSel0 и PerfEvtSel1 соответственно. Они же отвечают за запуск/останов счетчиков производительности.

Коды событий, которые процессор может подсчитывать, перечислены в приложении "A" руководства по системному программированию "Intel Architecture Software Developer's Manual Volume 3: System Programming Guide". В частности, событие "промах кэш памяти данных" проходит под номером 48h, а "промах кэш памяти кода" — 81h.

Рисунок 1 номера различных событий, за которыми можно вести мониторинг с помощью счетчиков производительности

Чтение/запись MSR регистров осуществляется командами RDMSR/WRMSR, доступными _только_ из нулевого кольца и действующими следующим образом: в регистр ECX помещается номер выбранного MSR-регистра, а в регистровой паре EDX:EAX – возвращаемое/записываемое значение.
Номера MSR- регистров так же можно узнать из руководства по системному программированию. Так например, номер регистр PerfEvtSel0 имеет номер 186h, а структура его управляющих полей приведена на рис. 1.



Рисунок 2 структура MSR-регистров PrefEvtSel0/ PrefEvtSel1

Собственно говоря, все, что нам нужно это занести код события в регистр PerfEvtSel0/PerfEvtSel1 (биты 0-7), маску события, в данном случае равную нулю (биты 8-15) и взвести флажок Enable Counter (бит 22), чтобы начать подсчет событий. Описание остальных битов можно найти в документации, нам они совершенно не интересны за исключением, пожалуй, поля USR (бит 16), открывающего к счетчику доступ с пользовательского уровня, позволяя реализовать основной код в программе прикладного режима, которую намного проще отлаживать чем драйвер.

Но все-таки совсем без драйвера обойтись не получится, поскольку инструкция RDMSR на прикладном уровне возбуждает неизменное исключение. Как же быть?! Intel предоставила крошечную лазейку в виде команды RDPMC читающей текущий счетчик производительности в регистровую EDX:EAX. Текущий — это тот, который до этого был установлен командой WRMSR, запустивший MSR-регистр PerfEvtSel0 или PerfEvtSel1. Однако, по умолчанию, RDMSR с прикладного уровня недоступна и прежде, чем ей удастся воспользоваться необходимо взвести PCE флажок в регистре CR4 (бит 8), модифицировать который можно только из нулевого кольца, зато потом наступает благодать!!!

Подробнее о счетчиках производительности и всем, что с ними связано можно прочитать в разделе "Performance-Monitoring Events and Counters" руководства "Intel  Architecture Optimization Reference Manual" или уже упомянутой "библии" системного программиста "Intel Architecture Software Developer's Manual Volume 3: System Programming Guide"



Рисунок 3 бит PCE регистра CR4 управляет доступом к команде RDPMC с прикладного уровня



Таким образом, мыщъх'иная программа состоит из двух частей: крохотного псевдодрайвера и прикладной части.


Драйвер обеспечивает загрузку необходимого кода события в соответствующий MSR-регистр (PerfEvtSel0 или PerfEvtSel1) и запускает счетчик, предварительно "разблокировав" команду RDPMC.

Поскольку, RDPMC способна читать только один счетчик (а нам необходимо отслеживать по меньшей мере два события — промахи кэш памяти кода и данных), драйвер должен обеспечивать IOCTL-интерфейс с прикладным приложением, позволяя ему переключаться с одного счетчика на другой.

Чтобы не переводить понапрасну бумагу, ниже будут приведены только ключевые фрагменты кода, а все остальное читатель без труда допишет и сам. В частности, процедура инициализации драйвера среди прочего должна содержать:

DriverInitialize:          ; // процедура инициализации драйвера

       …

       MOV    EAX, CR4

       OR     EAX, 100h ; // разрешаем доступ к RDPMC с прикладного уровня

       MOV    CR4, EAX

       …

Листинг 3 фрагмент процедуру инициализации драйвера

Следующий код обеспечивает взаимодействие драйвера с прикладной программой через API-функцию DeviceIOControl, передающий в IOCTL-коде номер события, за которым необходимо вести мониторинг. По соображениям наглядности, здесь используется всего лишь один счетчик производительности, управляемый MSR-регистром PerfEvtSel0.

IRP_MJ_DEVICE_CONTROL:                   ; // процедура обработки IOCTL-запросов

       ; // настраиваем регистр perfevtsel0 для мониторинга нужных событий

       XOR    EDX, EDX

       MOV    EAX, pisl->Parameters.DeviceIoControl.IoControlCode ; //номер события

       TEST   EAX, EAX             ; // если код события равен нулю

       JZ     wrt                  ; // то вырубаем счетчик

      

       OR     EAX, 10000h          ; // делаем счетчик доступным

                                  ; // с прикладного уровня

       OR     EAX, 400000h         ; // пускаем счетчик

wrt:

       MOV ECX,0x186              ; // выбираем MSR-регистр PERFEVTSEL0

       WRMSR

Листинг 4 фрагмент драйвера, отвечающий за выбор нужного события



При деиницилизации драйвера крайне желательно "отобрать" доступ к команде RDPMC с прикладного уровня и остановить все ранее запущенные счетчики производительности, сбросив флажок Enable Counter в MSR-регистрах PerfEvtSel0/PerfEvtSel1 (код, приведенный ниже останавливает только PerfEvtSel0):

DriverUnload:              ; // процедура деиницилизации драйвера

       …

       ; // сбрасываем бит pce регистра cr4 для запрета чтения

       ; // счетчика производительности с пользовательского уровня

       MOV    EAX, CR4

       MOV    ECX, 100h

       NOT    ECX           ; // запрещаем доступ к RDPMC с прикладного уровня

       AND    EAX, ECX

       MOV    CR4, EAX

      

       ; // останавливаем счетчик производительности

       XOR    EDX, EDX

       XOR    EAX, EAX

       MOV    ECX, 186h

       WRMSR

       …

Листинг 5 фрагмент процедуры деиницилизации драйвера

Прикладная программа первым делом должна загрузить драйвер (пусть для определенности он будет называться 996.SYS), открыв его с помощью функции CreateFile. При этом управление получит процедура инициализации, открывающая доступ к машинной команде RDPMC, но сами счетчики производительности еще не заданы, так что читать, собственно говоря, нечего и незачем.

Нет никакой необходимости писать загрузку драйвера на ассемблере и лучше всего воспользоваться для этой цели языком Си:

// определения необходимых констант

#define PrefCtrl0          0x0000

#define DCU_MISS_OUTSTANDING      0x0048

// дескриптор драйвера 996

static HANDLE _996_handle = INVALID_HANDLE_VALUE;

int _996_init()

{

       if (_996_handle == INVALID_HANDLE_VALUE)

       {

              _996_handle = CreateFile("\\\\.\\996",GENERIC_READ,

                                  FILE_SHARE_READ | FILE_SHARE_WRITE, NULL,

                                  OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

             

              if (_996_handle == INVALID_HANDLE_VALUE) return 0;



       } return 1;

}

Листинг 6 прикладная функция, загружающая драйвер в память

Тоже самое относится и к функции, вызывающей DeviceIoControl и передающей ей код интересующего нас события. На языке Си она выглядит гораздо нагляднее:

int _996_select(int xCode, int REG)

{

       DWORD x;

       if (REG != PrefCtrl0) return 0;

      

       // если программист забыл загрузить драйвер,

       // данная функция делает это самостоятельно

       if (_996_handle == INVALID_HANDLE_VALUE) _996_init();

      

       // если загрузка драйвера провалилась сваливаем отсюда

       if (_996_handle == INVALID_HANDLE_VALUE) return 0;

       return DeviceIoControl(_996_handle, xCode, &x,0, &x, 0, &x, 0);

}

Листинг 7 прикладная функция, позволяющая выбирать интересующее нас событие для его мониторинга

Процедура закрытия драйвера должна просто вызывать CloseHandle, а все остальное за нас сделает сам драйвер. Впрочем, драйвер можно и не закрывать. При выходе из приложения операционная система сделает это автоматически.

int _996_exit()

{

       if (_996_handle != INVALID_HANDLE_VALUE)

       {

              CloseHandle(_996_handle);

       }

       return 1;

}

Листинг 8 прикладная функция выгружающая драйвер из памяти

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

       // ИНИЦИЛИЗАЦИЯ ДРАЙВЕРА 996

       if (_996_init()==0) return printf("-ERR: 996 driver not loaded!\n");

      

       // ВЫБОР СОБЫТИЯ ДЛЯ МОНИТОРИНГА И ЗАПУСК СЧЕТЧИКА

       _996_select(DCU_MISS_OUTSTANDING, PrefCtrl0);



      

       for(;;)

       {

              __asm

              {

                     mov    ecx, PrefCtrl0       ; // читаем регистр PrefCtrl0...

                     RDPMC                ; // ...и помещаем результат в EDX:EAX

                    

                     mov _edx, edx        ; // сохраняем EDX:EAX в...

                     mov _eax, eax        ; //          ...одноименных переменных

              }

             

              // анализ кол-ва кэш-промахов

              // ===========================

             

              …

              Sleep(0);                  ; // отдаем остаток кванта и спим

       }

Листинг 9  ключевой фрагмент функции, осуществляющей контроль за кэш-активностью

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

На самом деле, нет ничего проще! Достаточно просто прекратить отдавать кванты, загрузив процессор "тупой" работой, не требующей обращения к памяти. Например, складывать два регистра в цикле. При условии, что в системе имеются два активных потока, один из которых принадлежит приложению, гоняющему кэш и в хвост и в гриву, а другой поток — гонят цикл в нашей программе, на однопроцессорных материях операционная система будет выделять приложению только 50% машинного времени, следовательно, нагрузка на кэш упадает. А если мы запустим три потока, мотающие такие циклы, кэш-приложение получит только 25% машинного времени! Количество протоков и продолжительность выполнения цикла подбираются экспериментально и для каждого приложения они индивидуальны (а это значит, что для достижения наивысшей производительности придется отслеживать какие приложения запущены и выбирать соответствующий им профиль.муторно конечно, но разгон того стоит):

       MOV ECX,-1

cool:

       ADD EAX,ECX

       DEC ECX

       LOOP cool

Листинг 10 цикл, отбирающий процессорные такты у приложения, напрягающего кэш и дающее ему время на остыв

Остается разобраться с "загниванием" байтов в "застоявшейся" кэш-памяти. Ну тут все просто! Хоть мы не можем непосредственно обновить ее содержимое, достаточно просто с некоторой периодичностью (определяемой опять-таки чисто экспериментально) загружать в кэш посторонние данные (ну там мусор какой-нибудь), заставляя приложение заново перечитывать оригинальное содержимое из оперативной памяти. Учитывая, что пропускная способность современных DRAM-контроллеров измеряется гигабайтами в секунду, особого падения производительности это не вызовет, зато позволит разогнать процессор до сумасшедших тактовых частот!


Содержание раздела