ТЕХНИКА ОПТИМИЗАЦИИ ПРОГРАММ

       

Неточность измерений


Одно из фундаментальных отличий цифровой от аналоговой техники заключается в том, что верхняя граница точности цифровых измерений определяется точностью измерительного инструмента (точность аналоговых измерительных инструментов, напротив, растет с увеличением количества замеров).

А чем можно измерять время работы отдельных участков программы? В персональных компьютерах семейства IBM PC AT имеются как минимум два таких механизма: системный таймер (штатная скорость: 18,2 тика в секунду, т.е. 55 мсек., максимальная скорость – 1.193.180 тиков в секунду или 0,84 мсек.), часы реального времени (скорость 1024 тика в секунду т.е. 0,98 мсек.). В дополнении к этому в процессорах семейства Pentium появился так называемый регистр счетчик - времени (Time Stamp Counter), увеличивающийся на единицу каждый такт процессорного ядра.

Теперь – разберемся со всем этим хозяйством подробнее. Системный таймер (с учетом времени, расходующего на считывание показаний) обеспечивает точность порядка 5 мсек., что составляет более двух десятков тысяч тактов даже в 500 MHz системе! Это – целая вечность для процессора. За это время он успевает перемолотить море данных, скажем, отсортировать сотни полторы чисел. Поэтому, системный таймер для профилирования отдельных функций непригоден. В частности, нечего и надеяться с его помощью найти узкие места функции quick sort! Да что там узкие места – при небольшом количестве обрабатываемых чисел он и общее время сортировки определяет весьма неуверенно.

Причем, прямого доступа к системному таймеру под нормальной операционной системой никто не даст, а минимальный временной интервал, еще засекаемый штатной Си-функций clock(), составляет всего лишь 1/100 сек, – а это годиться разве что для измерения времени выполнения всей программы целиком.

Точность часов реального времени так же вообще не идет ни в какое сравнение с точность системного таймера (перепрограммированного, разумеется).

Остается надеяться лишь на Time Stamp Counter.
Первое знакомство с ним вызывает просто бурю восторга и восхищения "ну наконец-то Intel подарила нам то, о чем мы так долго мечтали!". Судите сами: во-первых, операционные системы семейства Windows (в том числе и "драконическая" в этом плане NT) предоставляют беспрепятственный доступ к машинной команде RDTSC, читающий содержимое данного регистра. Во-вторых, поскольку он инкрементируется каждый такт, создается обманчивое впечатление, что с его помощью возможно точно определять время выполнения каждой команды процессора. Увы! На самом же деле это не далеко так!

Начнем с того, что в конвейерных системах (как мы уже помним) вообще нет такого понятия как время выполнения команды и следует говорить либо о пропускной способности, либо латентности. Сразу же возникает вопрос: а что же все-таки RDTSC меряет? Документация Intel не дает прямого ответа, но судя по всему, RDTSC считывает содержимое регистра счетчика-времени в момент прохождения данной инструкции через соответствующее исполнительное устройство. Причем, RDTSC – это неупорядоченная команда, т.е. она может завешаться даже раньше предшествующих ей команд. Именно так и произойдет если предшествующая команда простаивает в ожидании операндов.

Рассмотрим крайний случай, когда измеряется время выполнения минимальной порции кода (одна машинная команда уходит на то, чтобы сохранить считанное значение в первой итерации):

RDTSC                   ; читаем значение регистра времени

                        ; и помещаем его в регистры EDX и EAX

MOV   [clock], EAX      ; сохраняем младшее двойное слово



                        ; регистра времени в переменную clock

RDTSC                   ; читаем регистр времени еще раз

SUB   EAX, [clock]      ; вычисляем разницу замеров между

                        ; первым и вторым замером

Листинг 5 Попытка замера времени выполнения одной машинной команды

При прогоне этого примера на P-III он выдаст 32 тактов, вызывая тем самым риторический вопрос: "а почему, собственно, так много?" Хорошо, оставим выяснение обстоятельств "похищения процессорных тактов до лучших времени", а сейчас попробуем измерять время выполнения какой-нибудь машинной команды, ну скажем, INC EAX, увеличивающий значение регистра EAX на единицу.


Поместим ее между инструкциями RDTSC и перекомпилируем программу.

Вот это да! Прогон показывает все те же 32 такта. Странно! Добавим-ка мы еще одну INC EAX. Как это так – снова 32 такта?! Зато при добавлении сразу трех

инструкций INC EAX контрольное время исполнения скачкообразно увеличивается на единицу, что составляет 33 такта. Четыре и пять инструкций INC EAX дадут аналогичный результат, а вот при добавлении шестой, результат изменений вновь подскакивает на один такт.

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

      MOV   ECX,1000    ; поместить в регистр ECX значение 1000

@for:                   ; метка начала цикла

      INC   EAX         ;\

      INC   EAX         ; +- одна группа профилируемых инструкций

      INC   EAX         ;/

     

      INC   EAX         ;\

      INC   EAX         ; +- вторая группа профилируемых инструкций

      INC   EAX         ;/

DEC ECX                 ; уменьшить значение регистра ECX на 1

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

                        ;  счетчика цикла)

JNZ @xxx                ; до тех пор, пока ECX не обратится в ноль

                        ; перепрыгивать на метку @for

Листинг 6 Измерение времени выполнения 6х1000 машинных команд INC

На P-III выполнение данного цикла займет отнюдь не ~2.000, а целых 6.781 тактов, что соответствует по меньшей мере одному такту, приходящемуся на каждую математическую инструкцию! Следовательно, в предыдущем случае, при измерении времени выполнения нескольких машинных команд, инструкция RDTSC "вперед батьки пролезла в пекло", сообщив нам совсем не тот результат, которого мы от нее ожидали!



Вот если бы существовал способ "притормозить" выполнение RDTSC до тех пор, пока полностью не будут завершены все предшествующие ей машинные инструкции… И такой способ есть! Достаточно поместить перед RDTSC одну из команд упорядоченного выполнения. Команды упорядоченного выполнения начинают обрабатываться только после схода с конвейера последней предшествующей ей неупорядоченной команды и, покуда команда упорядоченного выполнения не завершится, следующие за ней команды мирно дожидаются своей очереди, а не лезут как толпа дикарей вперед на конвейер.

Подавляющее большинство команд упорядоченного выполнения – это привилегированные

команды (например, инструкции чтения/записи портов ввода-вывода) и лишь очень немногие из них доступны с прикладного уровня. В частности, к таковым принадлежит инструкция идентификации процессора CPUID.

Многие руководства (в частности и Ангер Фог в своем руководстве "How to optimize for the Pentium family of microprocessors" и технический документ "Using the RDTSC Instruction for Performance Monitoring" от корпорации Intel) предлагают использовать приблизительно такой код:

XOR   EAX,EAX           ; вызываем машинную команду CPUID,

CPUID                   ; после выполнения которой все

                        ; предыдущие машинные инструкции

                        ; гарантированно сошли к конвейера

                        ; и потому никак не могут повлиять

                        ; на результаты наших замеров

RDTSC                   ; вызываем инструкцию RDTSC, которая

                        ; возвращает в регистре EAX младшее

                        ; двойное слово текущего значения

                        ; Time Stamp Counter 'a

MOV [clock],EAX         ; сохраняем полученное только что

                        ; значение в переменной clock

// …                    ;\

// профилируемый код    ; +-здесь исполняется профилируемый код

// …                    ;/



XOR   EAX,EAX           ; еще раз выполняем команду CPUID,

CPUID                   ; чтобы все предыдущие инструкции

                        ; гарантированно успели покинуть

                        ; конвейер

RDTSC                   ; вызываем инструкцию RDTSC для чтения

                        ; нового значение Time Stamp Count 'a

SUB EAX,[clock]         ; вычисляем разность второго и первого

                        ; замеров, тем самым определяя реальное

                        ; время выполнения профилируемого

                        ; фрагмента кода

Листинг 7 Официально рекомендованный способ вызова RDTSC для измерения времени выполнения

К сожалению, даже этот, официально рекомендованный, код все равно не годится для измерения времени выполнения отдельных инструкций, поскольку полученный с его помощью результат представляет собой полное время выполнения инструкции, т.е. ее латентность, а отнюдь не пропускную способность, которая нас интересует больше всего. (Кстати, и у Intel, и Фрога есть одна грубая ошибка – в их варианте программы перед инструкцией CPUID отсутствует явное задание аргумента, который CPUID ожидает увидеть в регистре EAX. А, поскольку, время ее выполнения зависит от аргумента, то и время выполнения профилируемого фрагмента не постоянно, а зависит от состояния регистров на входе и выходе. В предлагаемом много варианте, инициализация EAX осуществляется явно, что страхует профилировщик от всяких там наведенных эффектов).

Имеется и другая проблема, еще более серьезная, чем первая. Вы помните постулат квантовой физики, сводящийся к тому, что всякое измерение свойств объекта неизбежно вносит в этот объект изменения, искажающие результат измерений? Причем, эти искажения невозможно устранить простой калибровкой, поскольку изменения могут носить не только количественный, но и качественный характер.

Если профилируемый код задействует те же самые узлы процессора, что и команды RDTSC/CPUID, время его выполнения окажется совсем иным нежели в "живой" действительности! Никаким ухищрениями нам не удастся достигнуть точности измерений до одного–двух тактов!

Поэтому, минимальный промежуток времени, которому еще можно верить, составляет, как показывает практика и личный опыт автора,  по меньше мере пятьдесят – сто тактов.

Отсюда следствие: штатными средствами процессора измерять время выполнения отдельных команд невозможно.


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