Как посчитать такты процессора
В своё время, первый 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 не предел, то такой запас вполне оправдан. К примеру анонсированный на январь этого года
зарекается работать на частоте 5.3 GHz, соответственно и скорость переполнения его регистра TSC будет уже выше (с деталями этого процессора можно
На заднем дворе..
Техническая модель счётчика TSC далека от идеала и её нельзя воспринимать как эталон времени в системе. И речь здесь даже не о том, что если мы хотим использовать TSC в качестве программного таймера, то сначала должны откалибровать его.. т.е. узнать, сколько раз простучит CPU на текущей своей частоте в течении одной милли/секунды. Соответственно в любом случае на время калибровки нужно будет воспользоваться услугами дополнительного таймера, на роль которого как-нельзя лучше подходит анархист RTC со-своим независимым кварцем 32.768 кГц, или-же высокоточный таймер событий HPET .
Если реализацию TSC разложить на примитивы, то оказывается что в этом "академгородке" мир вращается не вокруг тактовой частоты процессора, а вокруг частоты системной шины чипсета. Ведь что такое частота процессора? Это произведение его множителя на частоту шины. Например если в биосе Bus-frequency =200 MHz, а CPU-ratio =12, то получим CPU-frequency =2400. Таким образом, на ход TSC влияют внешние факторы – во-первых частота шины, во-вторых температура ядра.
Здесь видно, что при достижении критической температуры процессора, логика по термодатчику сначала сбрасывает частоту, после чего с заданным шагом уменьшает и питание VID – Voltage Identificator . На всех материнских платах для этого предусмотрен т.н. VRM или
Для инженеров (которым нужен был 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 счётчик тактов работает на постоянной частоте ядра и на него не влияют внешние факторы, хотя на самом деле температурный режим здесь не учитывается, т.к. он попадает под определение "форс-мажор". Однако и это уже большой плюс в их копилку! В графическом виде с грубыми штрихами, нововведение заключается в следующем..
А вот на новых чипсетах (справа), аппаратную модель логирования тактов вынесли в отдельный домен. Теперь частота шины 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 .
Кстати среди 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. Поэтому подсовываем ей аргумент 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, код исполнялся на нулевом ядре:
На этом покончим с таймерами и в следующей части ознакомимся с тонкостями профилирования кода , для выявления т.н. "горячих точек" программы. Без понимания элементарных основ не возможно разобраться во-всём этом, т.к. операционная система в каждый момент времени ведёт себя далеко не стабильно. В этой области есть много моментов, на которые следует обратить внимание, если мы хотим получить приближённый к действительности результат. А пока, в таблице ниже я собрал основные свойства рассмотренных ранее таймеров, чтобы можно было сделать выводы:
В моей статье полугодичной давности о длинной арифметике есть замеры скорости (throughput в тактах) очень коротких фрагментов кода — всего по несколько инструкций. Методика измерения была кривовата, но давала правдоподобные результаты. Потом выяснилось, что результаты таки неверные — поверхностный подход всегда сказывается.
В этом посте я опишу надежный метод «нанобенчмаркинга» с минимальной погрешностью и без подключения специальных библиотек и драйверов, к которому в итоге пришел. Применимость: сравнение однопоточного потенциала процессоров, просто интерес.
Я использую только GCC — соответственно способ заточен под него. Но буду делать обобщения, чтобы владельцы других компиляторов могли разобраться.
Непосредственно мерилом служит команда RDTSC. На Википедии справедливо отмечается, что она ненадежна и рекомендуется использовать специальные сервисы ОС. Однако для микроизмерений они работают слишком долго (сотни тактов) и неодинаково от запуска к запуску, что вносит неустранимые погрешности. Сама по себе RDTSC работает не более нескольких десятков тактов — постоянное количество или одно из небольшого набора.
Единственным недостатком RDTSC при микроизмерениях остается плавающая тактовая частота процессоров, потому что time stamp counter всегда считает такты в соответствии со стандартным множителем. Зафиксировать множитель — задача не всегда тривиальная, ищите "disable cpu scaling" в сочетании с названием своей ОС и типом процессора. Удачное решение в Gnome — апплет indicator-cpufreq .
Измерительная обвязка состоит из трех вложенных циклов.
Внутренний цикл управляет потоком данных как и в рабочей программе.
В таком духе:
Важно, что в фразе «померить код» под «кодом» в этой статье подразумевается определенная последовательность инструкций процессора. Поэтому часть цикла между скобками либо должна быть написана на ассемблере, либо на Си, но вы должны четко понимать, чего добиваетесь от компилятора. Чтобы побороть кипучую активность GCC при -O3 , сразу добавьте опции -fno-prefetch-loop-arrays , -fno-unroll-loops , -ftree-vectorizer-verbose=1 . -fno-tree-vectorize либо -ftree-vectorize — уже в зависимости от того, что требуется на выходе — «как написано» или векторизованный вариант цикла.
Если вы хотите измерить обработку конкретного входа или код вообще без входа/выхода, все равно заверните его в цикл. Чтобы GCC не вынес код обратно, включите -fno-gcse (global common subexpression elimination), -fno-tree-pta (points-to analysis) и -fno-tree-pre (partial redundancy elimination). См. все опции оптимизации.
Начало цикла выровняйте на 32 байта. При -falign-loops ( -O2 ) GCC делает это сам.
Средний цикл содержит 2 замера тактов и неизменный внутренний цикл посередине. Его роль — определить минимальное время, за которое может выполниться внутренний цикл. 20-30 итераций вполне хватает, чтобы все данные попали в кеш, начальная и конечная RDTSC заняли одинаковое время, а также сошлись все прочие звезды, если они существуют :-)
Внешний цикл управляет длиной внутреннего. Расположите в нем инициализацию входных данных перед средним циклом.
Внешний цикл необходим, потому что время, достигнутое в среднем цикле, всегда включает константу — время инициализации внутреннего цикла + затраты на 1 ошибку в предсказании перехода (самые умные интеловские ядра ошибаются реже). Поэтому время из среднего цикла нельзя просто разделить на кол-во итераций.
- обработка данных,
- простой, т. к. следующий этап еще занят,
- простой, т. к. не пришли данные с предыдущего этапа.
В итоге на конвейере устанавливается «паттерн» работы длиной от 1 до 10-15(?) итераций:
Точный througput в тактах имеет смысл считать как минимум для 1 такого паттерна, а не 1 итерации.
Как легко убедиться из цифр в примерах ниже, даже при измерении паттернов разброс результатов остается. Предположительно, на самом деле RDTSC не так хороша, как расписано выше :-)
Итак, получив разницы времени выполнения внутренних циклов с длинами кратными шагу паттерна, остается посчитать статистики.
4 ответа 4
Такт работы - это такт работы, т.е. промежуток времени между двумя соседними "пиками" некоего задающего тактового сигнала. Увы, без него не обойтись, т.к. схемы у нас цифровые и нам нужно фиксировать состояние сигналов. Физически к изменению битов и выполнению команд такт относится опосредованно. Во-первых, разные блоки процессора работают на разной частоте. Но если рассматривать простейший случай, то одна команда выполняется минимум за один такт работы исполнительного у-ва. Всякие команды типа умножения или деления могут выполняться за сотни тактов. Во-вторых, общение с внешним миром у процессора происходит через шины, например, шину данных. У этой шины есть разрядность, например, 64 бит. Опять же в простейшем случае мы можем по этой шине передать за единицу времени кол-во информации равное произведению частоты на битность. Т.е. за такт можно сделать одну передачу, при этом все 64 бита передаются одновременно
Еще раз поясню, что "кол-во тактов" - это по сути единица измерения времени. Связь со временем через частоту:
@gecube, а дополните свой ответ рассказом о конвейере исполнения команд, суперскалярности и разных задержках при обращении к разным уровням памяти (кэшам).
clock( )
На всех UNIX-подобных ОС, очень старая функция clock( ) возвращает процессорное время процесса в тиках, а макрос CLOCKS_PER_SEC количество тиков в секунду.
Заметка: Возвращенное процессорное время включает в себя время проведенное в user mode И в system mode от имени процесса.
Внимание: Хотя изначально CLOCKS_PER_SEC должен был возвращать значение, зависящее от процессора, стандарты C ISO C89 и C99, Single UNIX Specification и стандарт POSIX требуют, чтобы CLOCKS_PER_SEC имел фиксированное значение 1,000,000, что ограничивает точность функции микросекундами. Большинство ОС соответствует этим стандартам, но FreeBSD, Cygwin и старые версии OSX используют нестандартные значения.
Внимание: На AIX и Solaris, функция clock( ) включает процессорное время текущего процесса И и любого завершенного дочернего процесса для которого родитель выполнил одну из функций wait( ) , system( ) или pclose( ) .
Внимание: В Windows, функция clock( ) поддерживается, но возвращает не процессорное, а реальное время.
Доступность clock( ): AIX, BSD, Cygwin, HP-UX, Linux, OSX и Solaris.
Получение процессорного времени:
GetProcessTimes( )
На Windows и Cygwin (UNIX-подобная среда и интерфейс командной строки для Windows), функция GetProcessTimes( ) заполняет структуру FILETIME процессорным временем, использованным процессом, а функция FileTimeToSystemTime( ) конвертирует структуру FILETIME в структуру SYSTEMTIME, содержащую пригодное для использования значение времени.
Доступность GetProcessTimes( ): Cygwin, Windows XP и более поздние версии.
Получение процессорного времени:
Использование
Чтобы замерить процессорное время алгоритма, вызовите getCPUTime( ) до и после запуска алгоритма, и выведите разницу. Не стоит предполагать, что значение, возвращенное при единичном вызове функции, несет какой-то смысл.
Каждая ОС предоставляет один или несколько способов получить процессорное время. Однако некоторые способы точнее остальных.
OS | clock | clock_gettime | GetProcessTimes | getrusage | times |
---|---|---|---|---|---|
AIX | yes | yes | yes | yes | |
BSD | yes | yes | yes | yes | |
HP-UX | yes | yes | yes | yes | |
Linux | yes | yes | yes | yes | |
OSX | yes | yes | yes | ||
Solaris | yes | yes | yes | yes | |
Windows | yes |
Каждый из этих способов подробно освещен ниже.
getrusage( )
На всех UNIX-подобных ОС, функция getrusage( ) это самый надежный способ получить процессорное время, использованное текущим процессом. Функция заполняет структуру rusage временем в секундах и микросекундах. Поле ru_utime содержит время проведенное в user mode, а поле ru_stime — в system mode от имени процесса.
Внимание: Некоторые ОС, до широкого распространения поддержки 64-бит, определяли функцию getrusage( ) , возвращающую 32-битное значение, и функцию getrusage64( ) , возвращающую 64-битное значение. Сегодня, getrusage( ) возвращает 64-битное значение, а getrusage64( ) устарело.
Доступность getrusage( ): AIX, BSD, Cygwin, HP-UX, Linux, OSX, and Solaris.
Получение процессорного времени:
Другие подходы
Существуют и другие ОС-специфичные способы получить процессорное время. На Linux, Solarisи некоторых BSD, можно парсить /proc/[pid]/stat, чтобы получить статистику процесса. На OSX, приватная функция API proc_pidtaskinfo( ) в libproc возвращает информацию о процессе. Также существуют открытые библиотеки, такие как libproc, procps и Sigar.
На UNIX существует несколько утилит позволяющих отобразить процессорное время процесса, включая ps, top, mpstat и другие. Можно также использовать утилиту time, чтобы отобразить время, потраченное на команду.
На Windows, можно использовать диспетчер задач, чтобы мониторить использование CPU.
На OSX, можно использовать Activity Monitor, чтобы мониторить использование CPU. Утилита для профайлинга Instruments поставляемая в комплекте с Xcode может мониторить использование CPU, а также много других вещей.
С коллегами обсуждаем до смешного простой вопрос: чем является такт работы процессора? Википедия уточняет:
В самом первом приближении тактовая частота характеризует производительность подсистемы (процессора, памяти и пр.), то есть количество выполняемых операций в секунду.
Обсуждаются два варианта:
- изменение значения бита (одного)
- ассемблерная операция (над рядом битов)
IMHO более правильно обратить внимание на другую цитату Такт процессора или такт ядра процессора — промежуток между двумя импульсами тактового генератора, который синхронизирует выполнение всех операций процессора. из той же Википедии. -- Тут можно добавить, что в разных устройствах процессора могут использоваться разные тактовые частоты (обычно кратные). Напрямую связывать один такт процессора ни с изменением одного бита, ни с одной ассемблерной операцией в большинстве современных процессоров не следует.
Тем более, современные «суперскалярные» архитектуры означают, что за «один такт» происходит кусочек отработки целой цепочки команд, некоторые из которых могут и никогда не быть выполнены на самом деле. В общем, такт — эфемерное понятие.
@VladD, почему эфемерное? Вполне ощутимое, а вот время обработки отдельной инструкции - да, вполне может варьироваться от 0 до каких-то космических значений (если, например, случился cache miss или чего похлеще).
@gecube: эфемерность его в том, что оно мало отражается в ощутимых программистом характеристиках компьютера. Например, скорости вычисления данной операции (как мы выяснили, она «размазана» по нескольким тактам).
Примеры
Сравните результаты замеров (здесь и далее все значения в тактах) из статьи про длинную арифметику:
Поверхностный метод | 7.5 | 5.5 | 5.5 | 7 | 5 | 2 | 2.5 | 3.25(?)–3.5 |
Умный метод | 7 | 6 | 6 | 7 | 5 | 2 | 2 | 3 |
Все дальнейшие тесты проводились на 2 ядрах: AMD K10 и Intel Core 2 Wolfdale.
Важно оценить сами инструменты.
Пустой цикл
Внутренний цикл выглядит так:
Далее (10, 1.0) — (длина паттерна, итого в среднем на 1 итерацию)
RDTSC
Без среднего и внутреннего цикла:
Приближенное вычисление синуса
Интересно посмотреть, сколько можно сэкономить, вычисляя синус рядом Тейлора 3-го порядка. При углах от −π/2 до π/2 получается точность в 2 знака после запятой. Можно представить приложения, где ее будет достаточно.
Инструкция FSIN — точный синус
Именно ее вызывает sin из math.h . Генерируемые микрооперации, наверное, напоминают эту реализацию синуса, так как скорость выполнения тоже зависит от угла. Поэтому точный througput имеет смысл, если в цикле вычисляется синус одного и того же угла. Среднее по случайному углу нужно для сравнения с независимым от угла грубым расчетом.
угол | 0.0 | 0.0001 | π/2 | случайный |
K10 | 30.2 ± 10.3 | 89.8 ± 2.9 | 143.1 ± 8.5 (2, 71.6) | 75.6 |
Core 2 | 40.0 ± 11.0 | 68.0 ± 5.6 | 88.0 ± 13.0 | 89.4 |
Ряд Тейлора 3-го порядка
K10 | 61.2 ± 15.6 (8, 7.7) |
Core 2 | 35.2 ± 16.8 (4, 8.8) |
Векторизованный ряд Тейлора
Если просто добавить опцию GCC -ftree-vectorize , по сути будет тот же результат (cм. выше). А тут используются Vector Extensions.
K10 | 41.8 ± 14.2 (5, 8.4, 4.2 на 1 синус) |
Core 2 | 44.3 ± 16.6 (5, 8.9, 4.5) |
Получается, что вычисление синуса с точностью до 2 знаков можно организовать как минимум в 10 раз быстрее, чем обычно.
Источники
Еще ссылки
Описанный метод не профилирует код. Вероятность того, что он поможет при оптимизации, также крайне мала, потому что даже если производительность упирается в вычислительный конвейер, сравнивать решения всегда можно обычным clock()-ом по циклу на миллион итераций.
Сегодня мы разберемся с двумя важными вопросами: как писать более эффективный код с CMSIS и как правильно рассчитывать скорость работы процессора. Начнем мы со второй части и изучим процессы, которые происходят в LPC1114 для генерации тактовой частоты.
Тактовая частота – основной источник «рабочей силы» в процессоре, ее генератор можно сравнить с сердцем у человека. В разных компонентах процессора может использоваться разная частота, которая, тем не менее, зарождается обычно в одном и том же кристалле (или резонаторе).
Большинство процессоров имеют встроенный резонатор и возможность подключить внешний резонатор или кристалл. Зачем это сделано? В основном, для удешевления процессора. Встроенный резонатор типично имеет погрешность около 1%, чего может хватить для многих задач, но есть еще больше задач, для которых такая точность неприемлема. В самом деле, если мы будем, например, считать время на встроенном резонаторе, то погрешность за сутки может достигнуть 14 минут. Если вы передаете пакет по сети где-то раз в полчаса – это совершенно не критичная погрешность. Другое дело, если вы делаете будильник.
(изображение из LPC111x User Manual)
Выше представлена обзорная схема генератора тактовой частоты, разбитая на компоненты. Сейчас мы займемся каждым из них по отдельности.
⓵ Основная частота
MAINCLKSEL задает основную частоту, от которой зависят почти все остальные. Она может быть основана на одном из нескольких источников.
Во-первых, это IRC – внутренний резонатор. Рабочая частота – 12 МГц (на самом деле, можно тюнинговать в небольших пределах), погрешность — около 1%. Именно отсюда генерируется тактовая частота процессора в момент старта, так что весь загрузочный код выполняется при тактовой частоте в 12 МГц. Вариант максимально простой (вам вообще ничего не надо делать, чтобы он работал), не требует дополнительных внешних компонентов. К сожалению, имеет свои проблемы: резонатор, как я уже упоминал, несколько неточен, кроме того, нам не особенно интересно гонять ядро на 12 МГц, когда оно отлично работает на 50 МГц.
Во-вторых, основную частоту можно задать еще с одного внутреннего генератора – watchdog oscillator, который обычно используется для работы watchdog. Этот осциллятор работает на скоростях (программно настраиваемо) от 9,4 кГц до 2,3 МГц с погрешностью ±40%, — казалось бы, не лучшее решение для основной частоты. С другой стороны – это именно то замечательное и энергоэффективное решение, если вам нужно перевести ядро в спящий режим, при этом оставив какую-то часть периферии рабочей.
В-третьих, мы можем получить основную частоту из системного осциллятора до или после ФАПЧ. Не будем сейчас вникать в специфику работы ФАПЧ, так как это достаточно объемная тема. Интересующимся советую изучить раздел «3.11 System PLL functional description».
⓶ Системный осциллятор
Системный осциллятор – это та часть процессора, которая не будет работать без аппаратных модификаций, в нем отсутствует основная рабочая сила осциллятора – кристалл (или кварцевый резонатор), который необходимо подключить снаружи, для чего на любом современном процессоре есть пины XTALIN/XTALOUT.
Конкретно LPC1114 (впрочем, как и остальные процессоры линейки LPC111x) поддерживает кристаллы с частотой осцилляции от 1 МГц до 25 МГц. Помимо самого кристалла, вам также понадобятся два конденсатора, значения которых зависят от параметров выбранного кристалла. Тут я отсылаю вас к datasheet, где в разделе 12.3 (XTAL input) есть и схема подключения, и таблица с рекомендованными емкостями конденсаторов. В тестовой схеме я пробовал использовать кристалл с частотой 12 МГц, емкостью нагрузки 20 пФ и двумя конденсаторами на 39 пФ, но этот режим работы далее рассматриваться не будет.
Если у вас есть надежный внешний источник тактовой частоты, то можно пропустить системный осциллятор, тогда тактовая частота берется с пина XTALIN.
Системный осциллятор можно использовать непосредственно как генератор основной частоты, или же предварительно пропустить его через ФАПЧ.
Не вдаваясь в электромеханику, ФАПЧ – это устройство, которое сначала умножает, а потом делит входную тактовую частоту. На входе ФАПЧ может принять частоту от IRC или системного осциллятора, а выход будет использован для основной частоты.
Настройка параметров ФАПЧ потенциально опасна для внутренностей процессора, потому я рекомендую NXP-шную утилиту (успешно конвертируется и работает в Google Drive) для подбора необходимых параметров, просто задайте частоту осциллятора на входе и итоговую частоту, которую хотите получить, и она рассчитает возможные варианты.
В сети есть интересная заметка о том, как поднять частоту IRC для генерации 50 МГц на выходе ФАПЧ, но для отладки этого результата вам понадобится осциллоскоп.
⓸ Системная частота
Обычно ядро (то, что Cortex-M0) работает на основной частоте, но, при необходимости, основную частоту можно разделить (на значение вплоть до 255), получив в итоге системную частоту. Помимо непосредственно ядра, на этой частоте будут работать флеш-память, RAM и вся периферия, за исключением SPI и UART. Имейте в виду, что максимальная частота тут – это 50 МГц.
⓹ А что же со SPI и UART?
Из-за специфики этих интерфейсов у них есть свои выделенные делители частоты, например, у UART он позволяет выбрать необходимый битрейт.
Несмотря на некоторую неочевидность схемы, на вход делителя попадает не основная, а системная частота.
Расчет делителя для битрейта – достаточно сложная задача, потому в очередной раз отправляю вас в инструкцию – «13.5.15 UART Fractional Divider Register (U0FDR — 0x4000 8028)». Там есть и формула расчета, и объяснение дополнительного дробного аргумента, а также блок-схема для поиска нужных параметров для заданного битрейта и пара примеров.
У SPI все как-то существенно проще, скорее всего потому, что мастер на шине задает частоту, и остальные устройства работают на ней – заочной синхронизации не требуется. Так что единственное, что мы можем сделать – это задать делитель. Важный момент – когда процессор работает в мастер-режиме, то минимальный делитель – 2, т.е., при частоте системной частоты в 48 МГц скорость передачи данных на SPI будет 24 МГц.
UPD: как верно заметил valeriyk, этот делитель не единственное, что влияет на выходную частоту. У SPI, например, несущая частота расчитывается по формуле: PCLK / (CPSDVSR * (SCR + 1)) , где PCLK — это частота периферии; CPSDVSR — «предделитель»; SCR — количество тактов предделителя на один бит вывода.
⓺ Watchdog на страже жизнедеятельности
Watchdog по своей специфике — компонент изолированный. Поэтому, в качестве ведущей частоты можно использовать системную, IRC или отдельный осциллятор. Точно так же, у watchdog есть свой выделенный делитель.
Зачем для watchdog нужен отдельный тактовый генератор? Если программа случайно поломает основной генератор, конечно же! Тогда у нее все еще будет шанс быть перезагруженной по таймеру watchdog.
⓻ На выход
Наконец, процессор может генерировать выходной сигнал тактовой частоты на пине CLKOUT (одна из альтернативных функций у GPIO 0.1). В качестве ведущей частоты мы можем использовать любую из доступных нам: из осцилляторов (IRC, системного или watchdog) или системную частоту (после ФАПЧ, если он включен). Ну и, конечно, свой делитель.
Немного о mbed
Мы детально рассмотрели процесс генерации тактовой частоты в LPC1114, но что же с LPC1768? На самом деле, у каждой линейки процессоров может быть (и скорее всего будет свой особый подход, потому инструкцию по этой теме надо изучить предельно внимательно. В LPC1768 также есть внутренний осциллятор – IRC, но работает он на частоте 12 МГц. Помимо него есть основной (main) осциллятор, идентичный системному осциллятору. На mbed к нему подключен кристалл на 12 МГц. Наконец, есть осциллятор часов реального времени (RTC), но кристалл к нему не подключен.
Также, помимо основного ФАПЧ, есть дополнительный, который используется для генерации рабочей частоты USB. Все компоненты периферии имеют независимые настраиваемые делители относительно рабочей частоты.
Практические нюансы изменения частоты
Изменение рабочей тактовой частоты влечет за собой несколько последствий. Самое очевидное — необходимость перенастраивать таймеры. Также, потребуется переинициализация периферии, работающей с протоколами где важно зафиксировать несущую частоту (UART, USB). Наконец, количество тактов для доступа к флеш-памяти тоже играет важную роль. У LPC1114 значение по умолчанию — 3 такта (рабочая частота до 50 МГц, см. документацию по регистру FLASHCFG), чего вполне хватает для наших задач. Но у LPC1768 значение по умолчанию — 4 такта, с рабочей частотой до 80 МГц, чего нам будет недостаточно.
Тем не менее, работать на более высокой частоте, скорее всего, будет выгодно. Встраиваемые процессоры большую часть времени проводят в режиме сна, так что чем быстрее они отработают цикл бодрствования — тем меньше энергии они затратят в итоге.
За работу!
Теперь у нас есть необходимый теоретический багаж, и мы готовы применить полученные знания на практике – заставить светодиод мигать детерминировано, 1 раз в секунду.
Как вы видели раньше, очень много задач выполняются однотипно – записью и чтением регистров (вообще, все задачи выполняются именно так). ARM позаботилась о том, чтобы задачи, не привязанные к конкретному процессору, можно было выполнять одним и тем же кодом на C, для этого и существует CMSIS – набор драйверов для ядра процессора. Вендоры обычно расширяют его драйверами для всей остальной периферии.
Сложный момент с CMSIS состоит в том, что иногда не совсем понятно, где найти актуальную версию. Базовый набор файлов можно скачать непосредственно у ARM, на момент написания там доступна версия 3.01. Помимо заголовочных файлов, ARM предоставляет библиотеку для разноплановых сложных расчетов на DSP (которого в нашем железе все равно нет). Хуже обстоит дело с драйверами от конкретных производителей. У NXP, например, CMSIS для LPC1114 основан на CMSIS 1.30, а для LPC1768 – на 2.10. Более того, в наборе драйверов периферии есть явные ошибки в коде. А уж драйверы для чипов TI приходится основательно искать в гугле.
Из этого можно сделать два важных вывода: во-первых, код драйверов почти весь открыт, так что «доверяй, но проверяй»: инструкция и даташит — это ваша основная литература по работе с периферией. Во-вторых, в драйверах нет почти ничего, что нельзя было бы написать самому, т.е., это отличный и, зачастую, рабочий справочный материал. Главное – не забывать относиться к нему критически, если что-то выглядит странно – раскуривайте инструкцию по процессору.
Исходный код теперь несколько более структурирован. Хотя в результате он существенно вырос в количестве фалов, теперь намного проще поддерживать несколько разных платформ. Исходники для сегодняшнего примера доступны на GitHub: farcaller/arm-demos (pull-реквесты для новых архитектур приветствуются!).
Дерево исходников еще не до конца причесано, в частности, я не избавился от примитивных boot.s и memmap.ld . Следующая часть будет целиком посвящена вопросам компоновщика (включая сборку мусора и правильную инициализацию .data и .bss), где мы займемся добиванием до конца всех спорных моментов. Весь код разбит на три категории: в app/ находятся файлы «приложения» – непосредственно рабочий код примера. Он оформлен в стиле arduino, через функции setup() и loop() . В platform/ хранятся описания разных платформ и платформозависимые функции (кроме platform/common , файлы которого линкуются во все платформы). Наконец, в cpu/ находятся CMSIS для конкретных процессоров.
Весь этот комбайн собирается маленьким забавным Rakefile. Наверное, можно было бы обойтись и make, но хотелось все аккуратно собрать в одном файле, так что для сборки примеров вам пригодится руби не старше версии 1.9.
Работа по часам
Для реализации нашей задачи (напомню, нам надо мигать светодиодом ровно раз в секунду) нам пригодился бы какой-то таймер. К счастью, таймеров в LPC-шных процессорах сразу несколько, мы будем работать с самым унифицированным – SysTick. Этот таймер описан непосредственно в CMSIS, т.е., есть большая вероятность того, что он будет и в любом другом процессоре. Его предполагается использовать для измерения квантов времени при переключении задач в ОС, но ничего не мешает использовать его для простых задач.
SysTick – это простой таймер, который считает от заданного значения вниз до нуля, где он устанавливает бит переполнения, дергает прерывание и начинает считать сначала.
Для начала о синтаксисе. Эти замечательные структуры доступны нам из CMSIS, больше не надо запоминать, где находятся регистры, да и доступ к полям реализуется существенно нагляднее.
Для инициализации таймера мы записываем 4 в регистр контроля. Это выключает таймер, если он был включен, выключает прерывание и переводит SysTick на использование частоты процессора (напоминаю, что по умолчанию — это 12 МГц). Далее мы загружаем стартовую точку отсчета в регистр SYST_RVR, ограничивая максимум — 16777215, сбрасываем текущее значение регистра в ноль и запускаем таймер.
Теперь о том, как нам подождать одну секунду:
Мы считываем значение COUNTFLAG из регистра SYST_CSR. COUNTFLAG выставляется в единицу, когда счетчик идет на новый круг, и сбрасывается в ноль при чтении. Таким образом, мы будем в цикле, пока счетчик не переполнится.
Заглянем в другие файлы нашего проекта. app/systick-blink.c :
Тут все достаточно наглядно. Инициализируем «драйвер» светодиода и таймера, и в цикле включаем-выключаем светодиод с задержкой. В зависимости от платформы, используем разное стартовое значение таймера (IRC на mbed и protoboard у нас работают на разных частотах). А как же работает код самого светодиода?
Как видите, с CMSIS все стало действительно читабельнее. Единственный интересный момент — это то, что вместо общего регистра GPIO мы сейчас используем регистр с маской. Он позволяет устанавливать биты GPIO для конкретных пинов с маской, т.е., можно просто писать нужное значение, не думая о том, что надо сохранять состояние соседних пинов. Детальнее (и в картинках) об этом можно прочитать в инструкции: «12.4.1 Write/read data operation».
Для сравнения вот код для mbed. platform/mbed/led.c :
Как видите, он весьма схож. У LPC1768 нет возможности задавать маску прямо в адресе указателя, но зато есть побайтовый доступ к регистрам, что генерирует немного более эффективный ассемблерный листинг.
Собрать проект можно командой rake build_protoboard или rake build_mbed . Можно даже сразу прошить устройство: rake upload_protoboard TTY=/dev/ftdi/tty/device или rake upload_mbed MOUNT=/Volumes/MBED соответственно. Сейчас светодиоды мигают идентично на обоих устройствах.
Поиграем частотой?
Вроде бы мы и решили поставленную задачу — светодиод мигает с корректным интервалом, но что-то еще осталось за кадром. Максимальная рабочая частота LPC1114 — 50 МГц, а у LPC1768 и того больше — 100 МГц, получается, мы гоняем их едва ли в треть силы!
Настало время заняться правильной инициализацией платформы. platform/protoboard/init.c :
В исходном коде доступны три шаблона для LPC1114: стандартные 12 МГц от IRC, 48 МГц от IRC, пропущенного через ФАПЧ, и 48 МГц от системного осциллятора, пропущенного через ФАПЧ. Последний вариант требует дополнительной аппаратной поддержки, но мы рассматриваем его, так как это очень актуальный режим использования.
Если мы работаем от системного осциллятора, его необходимо корректно инициализировать, а в первую очередь — включить. Как мы обсуждали ранее, осциллятор можно пропустить, если на входе XTALIN присутствует уже сформированный сигнал тактовой частоты.
После первичной инициализации следует сделать небольшую задержку. Далее мы переводим ФАПЧ на работу от системного осциллятора (вместо IRC), для этого существует интересный механизм: пишем 0, пишем 1, ждем — регистр начнет возвращать 1.
Вторая часть инициализирует ФАПЧ, который на данном этапе получает на входе сигнал или от IRC, или от системного осциллятора. Настраиваем делители по формуле из инструкции, включаем ФАПЧ и ждем, пока он заблокируется. Основная частота после загрузки работает от IRC, переводим ее на работу от выхода ФАПЧ и ждем, пока это изменение «устаканится».
На 48 МГц для SysTick нам понадобится 48000000 циклов, но это больше его максимального значения. Один из вариантов решения — ждать несколько циклов таймера, что реализовано в функции platform_systick_wait_loop (другим вариантом было бы использовать 32-разрядный таймер CT32B0).
У LPC1768 код, опять же, в целом похожий. Тут важный момент в том, что на выходе из PLL должно быть не менее 275 МГц, когда на входе в процессор — не более 100 МГц. В общем, внимательно проверяем делители. Также важно отметить, что мы повышаем количество тактов, необходимых для доступа к флеш-памяти, потому что мы будем работать на частоте выче чем значение по умолчанию.
Код, приведенный в примере, актуален только для LPC1768 на mbed, так как привязан к конкретной частоте кристалла. Более того, если вы работаете с LPC1768 «напрямую», то его загрузчик стартует с IRC с включенным ФАПЧ, так что в своем инициализаторе его перед настройкой необходимо выключить.
Подводя итоги
Еще хотел сегодня рассказать про CLKOUT и о том, как можно контролировать частоту анализатором логики или осциллографом, но так статья получилась бы слишком большой. CLKOUT, 32-разрядные таймеры, прерывания и спящий режим — все это будет в следующих выпусках.
До меня доехала коробка со Stellaris LaunchPad, я подумаю над тем, как лучше всего будет добавить еще одну архитектуру, не раздувая повествование. В любом случае, LPC1114 становится основным целевым процессором, все примеры мы сначала будем обкатывать на нем.
Приношу свои извинения за «многобукаф», далее постараюсь писать более содержательно.
P.S. Как всегда, большое спасибо pfactum за вычитку текста и бесценные комментарии по электромеханике. И за то, что объяснил про ФАПЧ :-).
КДПВ
От переводчика:
Большинство моих знакомых для измерения времени в разного вида бенчмарках в С++ используют chrono или, в особо запущенных случаях, ctime . Но для бенчмаркинга гораздо полезнее замерять процессорное время. Недавно я наткнулся на статью о кроссплатформенном замере процессорного времени и решил поделиться ею тут, возможно несколько увеличив качество местных бенчмарков.
P.S. Когда в статье написано "сегодня" или "сейчас", имеется ввиду "на момент выхода статьи", то есть, если я не ошибаюсь, март 2012. Ни я, ни автор не гарантируем, что это до сих пор так.
P.P.S. На момент публикации оригинал недоступен, но хранится в кэше Яндекса
Функции API, позволяющие получить процессорное время, использованное процессом, отличаются в разных операционных системах: Windows, Linux, OSX, BSD, Solaris, а также прочих UNIX-подобных ОС. Эта статья предоставляет кросс-платформенную функцию, получающую процессорное время процесса и объясняет, какие функции поддерживает каждая ОС.
Процессорное время увеличивается, когда процесс работает и потребляет циклы CPU. Во время операций ввода-вывода, блокировок потоков и других операций, которые приостанавливают работу процессора, процессорное время не увеличивается пока процесс снова не начнет использовать CPU.
Разные инструменты, такие как ps в POSIX, Activity Monitor в OSX и Task Manager в Windows показывают процессорное время, используемое процессами, но часто бывает полезным отслеживать его прямо из самого процесса. Это особенно полезно во время бенчмаркинга алгоритмов или маленькой части сложной программы. Несмотря на то, что все ОС предоставляют API для получения процессорного времени, в каждой из них есть свои тонкости.
Функция getCPUTime( ) , представленная ниже, работает на большинстве ОС (просто скопируйте код или скачайте файл getCPUTime.c). Там, где это нужно, слинкуйтесь с librt, чтобы получить POSIX-таймеры (например, AIX, BSD, Cygwin, HP-UX, Linux и Solaris, но не OSX). В противном случае, достаточно стандартных библиотек.
clock_gettme( )
На большинстве POSIX-совместимых ОС, clock_gettime( ) (смотри мануалы к AIX, BSD, HP-UX, Linux и Solaris) предоставляет самое точное значение процессорного времени. Первый аргумент функции выбирает "clock id", а второй это структура timespec , заполняемая использованным процессорным временем в секундах и наносекундах. Для большинства ОС, программа должна быть слинкована с librt.
Однако, есть несколько тонкостей, затрудняющих использование этой функции в кросс-платформенном коде:
- Функция является опциональной частью стандарта POSIX и доступна только если _POSIX_TIMERS определен в значением больше 0. На сегодняшний день, AIX, BSD, HP-UX, Linux и Solaris поддерживают эту функцию, но OSX не поддерживает.
- Структура timespec , заполняемая функцией clock_gettime( ) может хранить время в наносекундах, но точность часов отличается в разных ОС и на разных системах. Функция clock_getres( ) возвращает точность часов, если она вам нужна. Эта функция, опять-таки, является опциональной частью стандарта POSIX, доступной только если _POSIX_TIMERS больше нуля. На данный момент, AIX, BSD, HP-UX, Linux и Solaris предоставляют эту функцию, но в Solaris она не работает. определяет имена нескольких стандартных значений "clock id", включая CLOCK_PROCESS_CPUTIME_ID , чтобы получить процессорное время процесса. Тем не менее, сегодня BSD и HP-UX не имеют этого id, и взамен определяют собственный id CLOCK_VIRTUAL для процессорного времени. Чтобы запутать все ещё больше, Solaris определяет оба этих, но использует CLOCK_VIRTUAL для процессорного времени потока, а не процесса.
- Вместо того, чтобы использовать одну из констант, объявленных выше, функция clock_getcpuclockid( ) возвращает таймер для выбранного процесса. Использование процесса 0 позволяет получить процессорное время текущего процесса. Однако, это ещё одна опциональная часть стандарта POSIX и доступна только если _POSIX_CPUTIME больше 0. На сегодняшний день, только AIX и Linux предоставляют эту функцию, но линуксовские include-файлы не определяют _POSIX_CPUTIME и функция возвращает ненадёжные и несовместимые с POSIX результаты.
- Функция clock_gettime( ) может быть реализована с помощью регистра времени процессора. На многопроцессорных системах, у отдельных процессоров может быть несколько разное восприятие времени, из-за чего функция может возвращать неверные значения, если процесс передавался от процессора процессору. На Linux, и только на Linux, это может быть обнаружено, если clock_getcpuclockid( ) возвращает не-POSIX ошибку и устанавливает errno в ENOENT . Однако, как замечено выше, на Linux clock_getcpuclockid( ) ненадежен.
Доступность clock_gettime( ): AIX, BSD, Cygwin, HP-UX, Linux и Solaris. Но clock id на BSD и HP-UX нестандартные.
Доступность clock_getres( ): AIX, BSD, Cygwin, HP-UX и Linux, но не работает Solaris.
Доступность clock_getcpuclockid( ): AIX и Cygwin, не недостоверна на Linux.
Получение процессорного времени:
times( )
На всех UNIX-подобных ОС, устаревшая функция times( ) заполняет структуру tms с процессорным временем в тиках, а функция sysconf( ) возвращает количество тиков в секунду. Поле tms_utime содержит время, проведенное в user mode, а поле tms_stime — в system mode от имени процесса.
Внимание: Более старый аргумент функции sysconf( ) CLK_TCK устарел и может не поддерживаться в некоторых ОС. Если он доступен, функция sysconf( ) обычно не работает при его использовании. Используйте _SC_CLK_TCK вместо него.
Доступность times( ): AIX, BSD, Cygwin, HP-UX, Linux, OSX и Solaris.
Получение процессорного времени:
Примеры
Сравните результаты замеров (здесь и далее все значения в тактах) из статьи про длинную арифметику:
Поверхностный метод | 7.5 | 5.5 | 5.5 | 7 | 5 | 2 | 2.5 | 3.25(?)–3.5 |
Умный метод | 7 | 6 | 6 | 7 | 5 | 2 | 2 | 3 |
Все дальнейшие тесты проводились на 2 ядрах: AMD K10 и Intel Core 2 Wolfdale.
Важно оценить сами инструменты.
Пустой цикл
Внутренний цикл выглядит так:
Далее (10, 1.0) — (длина паттерна, итого в среднем на 1 итерацию)
RDTSC
Без среднего и внутреннего цикла:
Приближенное вычисление синуса
Интересно посмотреть, сколько можно сэкономить, вычисляя синус рядом Тейлора 3-го порядка. При углах от −π/2 до π/2 получается точность в 2 знака после запятой. Можно представить приложения, где ее будет достаточно.
Инструкция FSIN — точный синус
Именно ее вызывает sin из math.h . Генерируемые микрооперации, наверное, напоминают эту реализацию синуса, так как скорость выполнения тоже зависит от угла. Поэтому точный througput имеет смысл, если в цикле вычисляется синус одного и того же угла. Среднее по случайному углу нужно для сравнения с независимым от угла грубым расчетом.
угол | 0.0 | 0.0001 | π/2 | случайный |
K10 | 30.2 ± 10.3 | 89.8 ± 2.9 | 143.1 ± 8.5 (2, 71.6) | 75.6 |
Core 2 | 40.0 ± 11.0 | 68.0 ± 5.6 | 88.0 ± 13.0 | 89.4 |
Ряд Тейлора 3-го порядка
K10 | 61.2 ± 15.6 (8, 7.7) |
Core 2 | 35.2 ± 16.8 (4, 8.8) |
Векторизованный ряд Тейлора
Если просто добавить опцию GCC -ftree-vectorize , по сути будет тот же результат (cм. выше). А тут используются Vector Extensions.
K10 | 41.8 ± 14.2 (5, 8.4, 4.2 на 1 синус) |
Core 2 | 44.3 ± 16.6 (5, 8.9, 4.5) |
Получается, что вычисление синуса с точностью до 2 знаков можно организовать как минимум в 10 раз быстрее, чем обычно.
Источники
Еще ссылки
Описанный метод не профилирует код. Вероятность того, что он поможет при оптимизации, также крайне мала, потому что даже если производительность упирается в вычислительный конвейер, сравнивать решения всегда можно обычным clock()-ом по циклу на миллион итераций.
Сегодня мы разберемся с двумя важными вопросами: как писать более эффективный код с CMSIS и как правильно рассчитывать скорость работы процессора. Начнем мы со второй части и изучим процессы, которые происходят в LPC1114 для генерации тактовой частоты.
Тактовая частота – основной источник «рабочей силы» в процессоре, ее генератор можно сравнить с сердцем у человека. В разных компонентах процессора может использоваться разная частота, которая, тем не менее, зарождается обычно в одном и том же кристалле (или резонаторе).
Большинство процессоров имеют встроенный резонатор и возможность подключить внешний резонатор или кристалл. Зачем это сделано? В основном, для удешевления процессора. Встроенный резонатор типично имеет погрешность около 1%, чего может хватить для многих задач, но есть еще больше задач, для которых такая точность неприемлема. В самом деле, если мы будем, например, считать время на встроенном резонаторе, то погрешность за сутки может достигнуть 14 минут. Если вы передаете пакет по сети где-то раз в полчаса – это совершенно не критичная погрешность. Другое дело, если вы делаете будильник.
(изображение из LPC111x User Manual)
Выше представлена обзорная схема генератора тактовой частоты, разбитая на компоненты. Сейчас мы займемся каждым из них по отдельности.
⓵ Основная частота
MAINCLKSEL задает основную частоту, от которой зависят почти все остальные. Она может быть основана на одном из нескольких источников.
Во-первых, это IRC – внутренний резонатор. Рабочая частота – 12 МГц (на самом деле, можно тюнинговать в небольших пределах), погрешность — около 1%. Именно отсюда генерируется тактовая частота процессора в момент старта, так что весь загрузочный код выполняется при тактовой частоте в 12 МГц. Вариант максимально простой (вам вообще ничего не надо делать, чтобы он работал), не требует дополнительных внешних компонентов. К сожалению, имеет свои проблемы: резонатор, как я уже упоминал, несколько неточен, кроме того, нам не особенно интересно гонять ядро на 12 МГц, когда оно отлично работает на 50 МГц.
Во-вторых, основную частоту можно задать еще с одного внутреннего генератора – watchdog oscillator, который обычно используется для работы watchdog. Этот осциллятор работает на скоростях (программно настраиваемо) от 9,4 кГц до 2,3 МГц с погрешностью ±40%, — казалось бы, не лучшее решение для основной частоты. С другой стороны – это именно то замечательное и энергоэффективное решение, если вам нужно перевести ядро в спящий режим, при этом оставив какую-то часть периферии рабочей.
В-третьих, мы можем получить основную частоту из системного осциллятора до или после ФАПЧ. Не будем сейчас вникать в специфику работы ФАПЧ, так как это достаточно объемная тема. Интересующимся советую изучить раздел «3.11 System PLL functional description».
⓶ Системный осциллятор
Системный осциллятор – это та часть процессора, которая не будет работать без аппаратных модификаций, в нем отсутствует основная рабочая сила осциллятора – кристалл (или кварцевый резонатор), который необходимо подключить снаружи, для чего на любом современном процессоре есть пины XTALIN/XTALOUT.
Конкретно LPC1114 (впрочем, как и остальные процессоры линейки LPC111x) поддерживает кристаллы с частотой осцилляции от 1 МГц до 25 МГц. Помимо самого кристалла, вам также понадобятся два конденсатора, значения которых зависят от параметров выбранного кристалла. Тут я отсылаю вас к datasheet, где в разделе 12.3 (XTAL input) есть и схема подключения, и таблица с рекомендованными емкостями конденсаторов. В тестовой схеме я пробовал использовать кристалл с частотой 12 МГц, емкостью нагрузки 20 пФ и двумя конденсаторами на 39 пФ, но этот режим работы далее рассматриваться не будет.
Если у вас есть надежный внешний источник тактовой частоты, то можно пропустить системный осциллятор, тогда тактовая частота берется с пина XTALIN.
Системный осциллятор можно использовать непосредственно как генератор основной частоты, или же предварительно пропустить его через ФАПЧ.
Не вдаваясь в электромеханику, ФАПЧ – это устройство, которое сначала умножает, а потом делит входную тактовую частоту. На входе ФАПЧ может принять частоту от IRC или системного осциллятора, а выход будет использован для основной частоты.
Настройка параметров ФАПЧ потенциально опасна для внутренностей процессора, потому я рекомендую NXP-шную утилиту (успешно конвертируется и работает в Google Drive) для подбора необходимых параметров, просто задайте частоту осциллятора на входе и итоговую частоту, которую хотите получить, и она рассчитает возможные варианты.
В сети есть интересная заметка о том, как поднять частоту IRC для генерации 50 МГц на выходе ФАПЧ, но для отладки этого результата вам понадобится осциллоскоп.
⓸ Системная частота
Обычно ядро (то, что Cortex-M0) работает на основной частоте, но, при необходимости, основную частоту можно разделить (на значение вплоть до 255), получив в итоге системную частоту. Помимо непосредственно ядра, на этой частоте будут работать флеш-память, RAM и вся периферия, за исключением SPI и UART. Имейте в виду, что максимальная частота тут – это 50 МГц.
⓹ А что же со SPI и UART?
Из-за специфики этих интерфейсов у них есть свои выделенные делители частоты, например, у UART он позволяет выбрать необходимый битрейт.
Несмотря на некоторую неочевидность схемы, на вход делителя попадает не основная, а системная частота.
Расчет делителя для битрейта – достаточно сложная задача, потому в очередной раз отправляю вас в инструкцию – «13.5.15 UART Fractional Divider Register (U0FDR — 0x4000 8028)». Там есть и формула расчета, и объяснение дополнительного дробного аргумента, а также блок-схема для поиска нужных параметров для заданного битрейта и пара примеров.
У SPI все как-то существенно проще, скорее всего потому, что мастер на шине задает частоту, и остальные устройства работают на ней – заочной синхронизации не требуется. Так что единственное, что мы можем сделать – это задать делитель. Важный момент – когда процессор работает в мастер-режиме, то минимальный делитель – 2, т.е., при частоте системной частоты в 48 МГц скорость передачи данных на SPI будет 24 МГц.
UPD: как верно заметил valeriyk, этот делитель не единственное, что влияет на выходную частоту. У SPI, например, несущая частота расчитывается по формуле: PCLK / (CPSDVSR * (SCR + 1)) , где PCLK — это частота периферии; CPSDVSR — «предделитель»; SCR — количество тактов предделителя на один бит вывода.
⓺ Watchdog на страже жизнедеятельности
Watchdog по своей специфике — компонент изолированный. Поэтому, в качестве ведущей частоты можно использовать системную, IRC или отдельный осциллятор. Точно так же, у watchdog есть свой выделенный делитель.
Зачем для watchdog нужен отдельный тактовый генератор? Если программа случайно поломает основной генератор, конечно же! Тогда у нее все еще будет шанс быть перезагруженной по таймеру watchdog.
⓻ На выход
Наконец, процессор может генерировать выходной сигнал тактовой частоты на пине CLKOUT (одна из альтернативных функций у GPIO 0.1). В качестве ведущей частоты мы можем использовать любую из доступных нам: из осцилляторов (IRC, системного или watchdog) или системную частоту (после ФАПЧ, если он включен). Ну и, конечно, свой делитель.
Немного о mbed
Мы детально рассмотрели процесс генерации тактовой частоты в LPC1114, но что же с LPC1768? На самом деле, у каждой линейки процессоров может быть (и скорее всего будет свой особый подход, потому инструкцию по этой теме надо изучить предельно внимательно. В LPC1768 также есть внутренний осциллятор – IRC, но работает он на частоте 12 МГц. Помимо него есть основной (main) осциллятор, идентичный системному осциллятору. На mbed к нему подключен кристалл на 12 МГц. Наконец, есть осциллятор часов реального времени (RTC), но кристалл к нему не подключен.
Также, помимо основного ФАПЧ, есть дополнительный, который используется для генерации рабочей частоты USB. Все компоненты периферии имеют независимые настраиваемые делители относительно рабочей частоты.
Практические нюансы изменения частоты
Изменение рабочей тактовой частоты влечет за собой несколько последствий. Самое очевидное — необходимость перенастраивать таймеры. Также, потребуется переинициализация периферии, работающей с протоколами где важно зафиксировать несущую частоту (UART, USB). Наконец, количество тактов для доступа к флеш-памяти тоже играет важную роль. У LPC1114 значение по умолчанию — 3 такта (рабочая частота до 50 МГц, см. документацию по регистру FLASHCFG), чего вполне хватает для наших задач. Но у LPC1768 значение по умолчанию — 4 такта, с рабочей частотой до 80 МГц, чего нам будет недостаточно.
Тем не менее, работать на более высокой частоте, скорее всего, будет выгодно. Встраиваемые процессоры большую часть времени проводят в режиме сна, так что чем быстрее они отработают цикл бодрствования — тем меньше энергии они затратят в итоге.
За работу!
Теперь у нас есть необходимый теоретический багаж, и мы готовы применить полученные знания на практике – заставить светодиод мигать детерминировано, 1 раз в секунду.
Как вы видели раньше, очень много задач выполняются однотипно – записью и чтением регистров (вообще, все задачи выполняются именно так). ARM позаботилась о том, чтобы задачи, не привязанные к конкретному процессору, можно было выполнять одним и тем же кодом на C, для этого и существует CMSIS – набор драйверов для ядра процессора. Вендоры обычно расширяют его драйверами для всей остальной периферии.
Сложный момент с CMSIS состоит в том, что иногда не совсем понятно, где найти актуальную версию. Базовый набор файлов можно скачать непосредственно у ARM, на момент написания там доступна версия 3.01. Помимо заголовочных файлов, ARM предоставляет библиотеку для разноплановых сложных расчетов на DSP (которого в нашем железе все равно нет). Хуже обстоит дело с драйверами от конкретных производителей. У NXP, например, CMSIS для LPC1114 основан на CMSIS 1.30, а для LPC1768 – на 2.10. Более того, в наборе драйверов периферии есть явные ошибки в коде. А уж драйверы для чипов TI приходится основательно искать в гугле.
Из этого можно сделать два важных вывода: во-первых, код драйверов почти весь открыт, так что «доверяй, но проверяй»: инструкция и даташит — это ваша основная литература по работе с периферией. Во-вторых, в драйверах нет почти ничего, что нельзя было бы написать самому, т.е., это отличный и, зачастую, рабочий справочный материал. Главное – не забывать относиться к нему критически, если что-то выглядит странно – раскуривайте инструкцию по процессору.
Исходный код теперь несколько более структурирован. Хотя в результате он существенно вырос в количестве фалов, теперь намного проще поддерживать несколько разных платформ. Исходники для сегодняшнего примера доступны на GitHub: farcaller/arm-demos (pull-реквесты для новых архитектур приветствуются!).
Дерево исходников еще не до конца причесано, в частности, я не избавился от примитивных boot.s и memmap.ld . Следующая часть будет целиком посвящена вопросам компоновщика (включая сборку мусора и правильную инициализацию .data и .bss), где мы займемся добиванием до конца всех спорных моментов. Весь код разбит на три категории: в app/ находятся файлы «приложения» – непосредственно рабочий код примера. Он оформлен в стиле arduino, через функции setup() и loop() . В platform/ хранятся описания разных платформ и платформозависимые функции (кроме platform/common , файлы которого линкуются во все платформы). Наконец, в cpu/ находятся CMSIS для конкретных процессоров.
Весь этот комбайн собирается маленьким забавным Rakefile. Наверное, можно было бы обойтись и make, но хотелось все аккуратно собрать в одном файле, так что для сборки примеров вам пригодится руби не старше версии 1.9.
Работа по часам
Для реализации нашей задачи (напомню, нам надо мигать светодиодом ровно раз в секунду) нам пригодился бы какой-то таймер. К счастью, таймеров в LPC-шных процессорах сразу несколько, мы будем работать с самым унифицированным – SysTick. Этот таймер описан непосредственно в CMSIS, т.е., есть большая вероятность того, что он будет и в любом другом процессоре. Его предполагается использовать для измерения квантов времени при переключении задач в ОС, но ничего не мешает использовать его для простых задач.
SysTick – это простой таймер, который считает от заданного значения вниз до нуля, где он устанавливает бит переполнения, дергает прерывание и начинает считать сначала.
Для начала о синтаксисе. Эти замечательные структуры доступны нам из CMSIS, больше не надо запоминать, где находятся регистры, да и доступ к полям реализуется существенно нагляднее.
Для инициализации таймера мы записываем 4 в регистр контроля. Это выключает таймер, если он был включен, выключает прерывание и переводит SysTick на использование частоты процессора (напоминаю, что по умолчанию — это 12 МГц). Далее мы загружаем стартовую точку отсчета в регистр SYST_RVR, ограничивая максимум — 16777215, сбрасываем текущее значение регистра в ноль и запускаем таймер.
Теперь о том, как нам подождать одну секунду:
Мы считываем значение COUNTFLAG из регистра SYST_CSR. COUNTFLAG выставляется в единицу, когда счетчик идет на новый круг, и сбрасывается в ноль при чтении. Таким образом, мы будем в цикле, пока счетчик не переполнится.
Заглянем в другие файлы нашего проекта. app/systick-blink.c :
Тут все достаточно наглядно. Инициализируем «драйвер» светодиода и таймера, и в цикле включаем-выключаем светодиод с задержкой. В зависимости от платформы, используем разное стартовое значение таймера (IRC на mbed и protoboard у нас работают на разных частотах). А как же работает код самого светодиода?
Как видите, с CMSIS все стало действительно читабельнее. Единственный интересный момент — это то, что вместо общего регистра GPIO мы сейчас используем регистр с маской. Он позволяет устанавливать биты GPIO для конкретных пинов с маской, т.е., можно просто писать нужное значение, не думая о том, что надо сохранять состояние соседних пинов. Детальнее (и в картинках) об этом можно прочитать в инструкции: «12.4.1 Write/read data operation».
Для сравнения вот код для mbed. platform/mbed/led.c :
Как видите, он весьма схож. У LPC1768 нет возможности задавать маску прямо в адресе указателя, но зато есть побайтовый доступ к регистрам, что генерирует немного более эффективный ассемблерный листинг.
Собрать проект можно командой rake build_protoboard или rake build_mbed . Можно даже сразу прошить устройство: rake upload_protoboard TTY=/dev/ftdi/tty/device или rake upload_mbed MOUNT=/Volumes/MBED соответственно. Сейчас светодиоды мигают идентично на обоих устройствах.
Поиграем частотой?
Вроде бы мы и решили поставленную задачу — светодиод мигает с корректным интервалом, но что-то еще осталось за кадром. Максимальная рабочая частота LPC1114 — 50 МГц, а у LPC1768 и того больше — 100 МГц, получается, мы гоняем их едва ли в треть силы!
Настало время заняться правильной инициализацией платформы. platform/protoboard/init.c :
В исходном коде доступны три шаблона для LPC1114: стандартные 12 МГц от IRC, 48 МГц от IRC, пропущенного через ФАПЧ, и 48 МГц от системного осциллятора, пропущенного через ФАПЧ. Последний вариант требует дополнительной аппаратной поддержки, но мы рассматриваем его, так как это очень актуальный режим использования.
Если мы работаем от системного осциллятора, его необходимо корректно инициализировать, а в первую очередь — включить. Как мы обсуждали ранее, осциллятор можно пропустить, если на входе XTALIN присутствует уже сформированный сигнал тактовой частоты.
После первичной инициализации следует сделать небольшую задержку. Далее мы переводим ФАПЧ на работу от системного осциллятора (вместо IRC), для этого существует интересный механизм: пишем 0, пишем 1, ждем — регистр начнет возвращать 1.
Вторая часть инициализирует ФАПЧ, который на данном этапе получает на входе сигнал или от IRC, или от системного осциллятора. Настраиваем делители по формуле из инструкции, включаем ФАПЧ и ждем, пока он заблокируется. Основная частота после загрузки работает от IRC, переводим ее на работу от выхода ФАПЧ и ждем, пока это изменение «устаканится».
На 48 МГц для SysTick нам понадобится 48000000 циклов, но это больше его максимального значения. Один из вариантов решения — ждать несколько циклов таймера, что реализовано в функции platform_systick_wait_loop (другим вариантом было бы использовать 32-разрядный таймер CT32B0).
У LPC1768 код, опять же, в целом похожий. Тут важный момент в том, что на выходе из PLL должно быть не менее 275 МГц, когда на входе в процессор — не более 100 МГц. В общем, внимательно проверяем делители. Также важно отметить, что мы повышаем количество тактов, необходимых для доступа к флеш-памяти, потому что мы будем работать на частоте выче чем значение по умолчанию.
Код, приведенный в примере, актуален только для LPC1768 на mbed, так как привязан к конкретной частоте кристалла. Более того, если вы работаете с LPC1768 «напрямую», то его загрузчик стартует с IRC с включенным ФАПЧ, так что в своем инициализаторе его перед настройкой необходимо выключить.
Подводя итоги
Еще хотел сегодня рассказать про CLKOUT и о том, как можно контролировать частоту анализатором логики или осциллографом, но так статья получилась бы слишком большой. CLKOUT, 32-разрядные таймеры, прерывания и спящий режим — все это будет в следующих выпусках.
До меня доехала коробка со Stellaris LaunchPad, я подумаю над тем, как лучше всего будет добавить еще одну архитектуру, не раздувая повествование. В любом случае, LPC1114 становится основным целевым процессором, все примеры мы сначала будем обкатывать на нем.
Приношу свои извинения за «многобукаф», далее постараюсь писать более содержательно.
P.S. Как всегда, большое спасибо pfactum за вычитку текста и бесценные комментарии по электромеханике. И за то, что объяснил про ФАПЧ :-).
КДПВ
От переводчика:
Большинство моих знакомых для измерения времени в разного вида бенчмарках в С++ используют chrono или, в особо запущенных случаях, ctime . Но для бенчмаркинга гораздо полезнее замерять процессорное время. Недавно я наткнулся на статью о кроссплатформенном замере процессорного времени и решил поделиться ею тут, возможно несколько увеличив качество местных бенчмарков.
P.S. Когда в статье написано "сегодня" или "сейчас", имеется ввиду "на момент выхода статьи", то есть, если я не ошибаюсь, март 2012. Ни я, ни автор не гарантируем, что это до сих пор так.
P.P.S. На момент публикации оригинал недоступен, но хранится в кэше Яндекса
Функции API, позволяющие получить процессорное время, использованное процессом, отличаются в разных операционных системах: Windows, Linux, OSX, BSD, Solaris, а также прочих UNIX-подобных ОС. Эта статья предоставляет кросс-платформенную функцию, получающую процессорное время процесса и объясняет, какие функции поддерживает каждая ОС.
Процессорное время увеличивается, когда процесс работает и потребляет циклы CPU. Во время операций ввода-вывода, блокировок потоков и других операций, которые приостанавливают работу процессора, процессорное время не увеличивается пока процесс снова не начнет использовать CPU.
Разные инструменты, такие как ps в POSIX, Activity Monitor в OSX и Task Manager в Windows показывают процессорное время, используемое процессами, но часто бывает полезным отслеживать его прямо из самого процесса. Это особенно полезно во время бенчмаркинга алгоритмов или маленькой части сложной программы. Несмотря на то, что все ОС предоставляют API для получения процессорного времени, в каждой из них есть свои тонкости.
Функция getCPUTime( ) , представленная ниже, работает на большинстве ОС (просто скопируйте код или скачайте файл getCPUTime.c). Там, где это нужно, слинкуйтесь с librt, чтобы получить POSIX-таймеры (например, AIX, BSD, Cygwin, HP-UX, Linux и Solaris, но не OSX). В противном случае, достаточно стандартных библиотек.
Читайте также: