Соображения производительности
Производительность является ключевым аспектом любой программной системы. Нигде не это больше истины, чем в ядре, где небольшие проблемы производительности имеют тенденцию быть увеличенными повторным выполнением. Поэтому чрезвычайно важно, чтобы Ваш код был максимально эффективен.
В этой главе рассматриваются важность низкой задержки прерывания и тонкозернистой блокировки и говорит Вам, как определить то, чему части Вашего кода принесли бы преимущества больше всего из более эффективного проекта.
Задержка прерывания
В OS X Вы никогда не должны будете, вероятно, писать код, работающий в контексте прерывания. В целом только аппаратные средства системной платы требуют этого. Однако в маловероятном случае, что действительно необходимо записать код в контексте прерывания, задержка прерывания должна быть первоочередной задачей.
Задержка прерывания относится к задержке между сгенерированным прерыванием и обработчиком прерываний, фактически начинающим обслуживать то прерывание. На практике худшая задержка прерывания случая близко связывается на сумму времени, проведенного в привилегированном режиме (также названный привилегированным режимом) с прерываниями прочь при обработке некоторого другого прерывания. Низкая задержка прерывания необходима для разумной общей производительности, особенно при работе с аудио и видео. Для имения разумной производительности мягкого реального времени (например, производительность мультимедийных приложений), задержка прерывания, вызванная каждым драйвером устройства, должна быть и маленькой и ограничена.
OS X проявляет большую заботу к связанному, и минимизируйте задержку прерывания для встроенных драйверов. Это делает это прежде всего с помощью потоков службы прерывания (также известный как потоки службы I/O).
Когда OS X берет прерывание, низкоуровневый призыв обработчиков прерывания к универсальной подпрограмме обработки прерываний, очищающей незаконченное прерывание, укусил в контроллере прерываний и вызывает специфичного для устройства обработчика прерываний. Тот специфичный для устройства обработчик, в свою очередь, отправляет сообщение в поток службы прерывания, чтобы уведомить его, что прерывание произошло, и затем возвраты обработчика. Когда никакие дальнейшие прерывания не находятся на рассмотрении, управляют возвратами к в настоящее время выполняющемуся потоку.
В следующий раз, когда поток службы прерывания планируется, он проверяет, чтобы видеть, произошло ли прерывание, то службы прерывание. Как имя предполагает, это фактически происходит в контексте потока, не контексте прерывания. Этот проект вызывает два существенных различия от традиционного проекта операционной системы:
Задержка прерывания является близким нулем, так как код, выполняющийся в контексте прерывания, является очень маленьким.
В то время как драйвер устройства выполняется, для прерывания возможно произойти. Это означает, что традиционные (потоковые) драйверы устройств могут быть вытеснены и должны использовать блокировку или другие похожие методы защитить любые совместно используемые данные (несмотря на то, что они должны сделать так так или иначе для работы над компьютерами с многократными процессорами).
Эта модель крайне важна для производительности OS X. Вы не должны пытаться обойти этот проект путем выполнения больших объемов работы в контексте прерывания. Выполнение так будет вредно для общей производительности системы.
Блокировка узких мест
Трудно передать данные между многократными потоками или между потоком и контекстами прерывания, не используя блокировку или другую синхронизацию. Эта блокировка защищает Ваши данные от того, чтобы быть ударенным другим потоком. Однако это также имеет неудачный побочный эффект того, чтобы быть потенциальным узким местом.
В некоторых типах коммуникации (особенно n-путь), блокировка может существенно препятствовать производительности путем разрешения только одной вещи произойти за один раз. Блокировки чтения-записи, обсужденные в Примитивах Синхронизации, могут помочь улучшить эту проблему в наиболее распространенной ситуации, где многократные клиенты должны быть в состоянии считать информации, но только редко должны изменять те данные.
Однако существует много случаев, где блокировки чтения-записи не полезны. В этом разделе рассматриваются некоторые возможные проблемы и способы улучшить производительность в тех ограничениях.
Работа с высоко спорила блокировки
Когда много потоков должны получить блокировку (или небольшое количество потоков должно часто получать блокировку), эту блокировку рассматривают, высоко спорил. Высоко утвердил, что блокировки часто представляют дефектный проект кода, но они иногда неизбежны. В тех случаях блокировка имеет тенденцию становиться главным узким местом производительности.
Возьмите, например, проблему many-many коммуникации, которая должна синхронизироваться через общий буфер. В то время как некоторое улучшение может быть получено при помощи блокировок чтения-записи вместо обычного взаимного исключения, проблема многократных писателей означает, что блокировки чтения-записи все еще выполняют плохо.
Одно возможное решение для этой many-many проблемы связи состоит в том, чтобы разбить блокировку в многократные блокировки. Вместо того, чтобы совместно использовать единственный буфер для самой коммуникации, сделайте совместно используемый буфер, содержащий учетную информацию для коммуникации (например, список буферов, доступных для чтения). Тогда присвойтесь, каждое частное лицо буферизуют его собственную блокировку. Читатели, возможно, тогда должны были бы проверить несколько расположений для нахождения правильных данных, но это все еще часто приводит к лучшей производительности, так как писатели должны только бороться за блокировку записи при изменении учетной информации.
Другое решение для many-many связи состоит в том, чтобы устранить буфер полностью и передать передачу сообщений использования, сокеты, IPC, RPC или другие методы.
Еще одно решение состоит в том, чтобы реструктурировать Ваш код в способе, которым блокировка является ненужной. Это часто намного более трудно. Один метод, который часто полезен, должен использовать в своих интересах флаги и атомарные инкременты, как обрисовано в общих чертах в следующем абзаце. Для простоты, единственного писателя, представлен пример единственного читателя, но возможно расширить эту идею более сложным проектам.
Возьмите буфер с некоторым числом слотов. Сохраните индекс чтения и индекс записи в тот буфер. Когда индекс записи и читал, индекс то же, в буфере нет никаких данных. При записи, ясный следующее расположение. Тогда сделайте атомарный инкремент на указателе. Запишите данные. Конец путем установки флага в том новом расположении, говорящем, что данные допустимы.
Когда контакт с многократными читателями и многократными писателями, и как таковой, выходит за рамки этого раздела, Обратите внимание на то, что это решение становится намного более трудным.
Сокращение конкуренции путем уменьшения гранулярности
Одно из фундаментальных свойств блокировок является гранулярностью. Гранулярность блокировки относится на сумму кода или данных, которые это защищает. Блокировка, защищающая большой блок кода или большой объем данных, упоминается как крупнозернистая блокировка, в то время как блокировка, защищающая только мелкую сумму кода или данных, упоминается как тонкозернистая блокировка. С крупнозернистой блокировкой, намного более вероятно, будут спорить (необходимый одному потоку, будучи сохраненным другим), чем более точно гранулярная блокировка.
Существует два основных способа уменьшить гранулярность. В то время как блокировка сохранена, первое должно минимизировать объем кода, выполняемый. Например, если у Вас есть код, вычисляющий значение и хранящий его в таблицу, не берите блокировку прежде, чем вызвать функцию и выпускайте его после функциональных возвратов. Вместо этого возьмите блокировку в той части кода прямо, прежде чем Вы запишете данные и выпустите их, как только Вам больше не нужны они.
Конечно, сокращение суммы защищенного кода не всегда возможно или практично, если код должен гарантировать непротиворечивость, где значение, которое это пишет, зависит от других значений в таблице, так как те значения могли измениться перед получением блокировки, требуя, чтобы Вы возвратились и восстановили работу.
Также возможно сократить гранулярность путем блокировки данных в меньших модулях. В вышеупомянутом примере у Вас могла быть блокировка на каждой ячейке таблицы. При обновлении ячеек в таблице Вы запустили бы путем определения ячеек, от которых зависит конечная ячейка, затем заблокируйте те ячейки и конечную ячейку в некотором фиксированном порядке. (Для предотвращения мертвой блокировки необходимо всегда или блокировать ячейки в том же порядке или использовать надлежащее try
функционируйте и выпустите, все соединяет отказ.)
Как только Вы заблокировали все включенные ячейки, можно тогда выполнить вычисление и выпустить блокировки, уверенные, что никакой другой поток не повредил вычисления. Однако путем соединения меньшей единицы информации, Вы также сократили вероятность двух потоков, бывших должных получить доступ к той же ячейке.
Немного более радикальная версия этого должна использовать чтение-запись, соединяет основание на ячейку, и всегда обновляйте в определенном порядке. Это - однако, скорее экстремальное значение, и трудный сделать правильно.
Профилирование кода
Профилирование кода означает определять, как часто выполняются определенные части кода. Путем знания, как часто часть кода используется, можно более точно измерить важность оптимизации той части кода. Существует много хороших инструментов для профилирования приложений пространства пользователя. Однако профилирование кода в ядре является совсем другим животным, так как не разумно присоединить к нему как Вы, был бы рабочий процесс. (Это возможно при помощи второго компьютера, но даже тогда, это не тривиальная задача.)
В этом разделе описываются два полезных способа профилировать Ваш код ядра: счетчики и профилирование блокировки. Любые изменения, которые Вы вносите для разрешения профилирования кода, должны быть сделаны только во время разработки. Это не вид изменений, которые Вы хотите выпустить к конечным пользователям.
Используя счетчики для профилирования кода
Первый метод профилирования кода со счетчиками. Для профилирования раздела кода со счетчиком необходимо сначала создать глобальную переменную, имя которой описывает ту часть кода, и инициализируйте его для обнуления. Вы тогда добавляете что-то как
#ifdef PROFILING |
foo_counter++; #endif |
в надлежащей части кода. Если Вы тогда определяете PROFILING
, тот счетчик создается и инициализируется для обнуления, затем постепенно увеличился каждый раз, когда рассматриваемый код выполняется.
Одно маленькое препятствие с этим видом профилирования является проблемой получения данных. Это может быть сделано несколькими способами. Самое простое должно, вероятно, установить a sysctl
, использование адреса foo_counter
как параметр. Затем Вы могли просто выйти sysctl
команда из командной строки и считала или очищает переменную. Добавление a sysctl
описан более подробно в BSD sysctl API.
В дополнение к использованию sysctl
, Вы могли также получить данные путем печати его значения при разгрузке модуля (в случае KEXT) или при помощи удаленного отладчика для присоединения к ядру и непосредственно проверке переменной. Однако a sysctl
обеспечивает наибольшую гибкость. С a sysctl
, можно выбрать значение в любое время, не как раз в то самое время, когда разгружен модуль. Возможность произвольно выбрать значение упрощает определять важность части кода к одному определенному действию.
При разработке кода для использования в Наборе I/O необходимо, вероятно, использовать водительское setProperties
вызовите вместо a sysctl
.
Профилирование блокировки
Профилирование блокировки является другим полезным способом найти причину неэффективности кода. Профилирование блокировки может дать Вам следующую информацию:
сколько раз была взята блокировка
сколько времени блокировка была сохранена в среднем
как часто блокировка была недоступна
Другими словами это позволяет Вам определять конкуренцию блокировки, и таким образом, может помочь Вам минимизировать конкуренцию реструктурированием кода.
Существует много различных способов сделать профилирование блокировки. Наиболее распространенный способ состоит в том, чтобы создать Ваши собственные вызовы блокировки, постепенно увеличивающие счетчик и затем вызывающие реальные функции блокировки. То, когда Вы перемещаетесь от отладки в цикл тестирования перед выпуском, можно тогда заменить функции, определяет, чтобы заставить фактические функции быть вызванными непосредственно. Например, Вы могли бы записать что-то вроде этого:
extern struct timeval time; |
boolean_t mymutex_try(mymutex_t *lock) { |
int ret; |
ret=mutex_try(lock->mutex); |
if (ret) { |
lock->tryfailcount++; |
} |
return ret; |
} |
void mymutex_lock(mymutex_t *lock) { |
if (!(mymutex_try(lock))) { |
mutex_lock(lock->mutex); |
} |
lock->starttime = time.tv_sec; |
} |
void mymutex_unlock(mymutex_t *lock) { |
lock->lockheldtime += (time.tv_sec - lock->starttime); |
lock->heldcount++; |
mutex_unlock(lock->mutex); |
} |
Эта подпрограмма имеет точность только к ближайшей секунде, которая не особенно точна. Идеально, Вы хотите отслеживать обоих time.tv_sec
и time.tv_usec
и прокрутите микросекунды в секунды, поскольку число становится большим.
От этой информации можно получить среднее время, которым блокировка была сохранена путем деления общего времени, сохраненного числом раз, которым это было сохранено. Это также говорит Вам число раз, блокировка была сразу взята вместо ожидания, которое является ценной частью данных при анализе конкуренции.
Как с противооснованным профилированием после записи кода для записи использования блокировки и конкуренции, необходимо найти способ получить ту информацию. A sysctl
хороший способ сделать это, так как это относительно просто реализовать и может обеспечить представление «снимка» структуры данных в любом моменте времени. Для получения дополнительной информации о добавлении a sysctl
, посмотрите BSD sysctl API.
Другой способ сделать профилирование блокировки состоит в том, чтобы использовать встроенный ETAP (Аналитический Пакет Трассировки События). Этот пакет состоит из дополнительного кода, разработанного для профилирования блокировки. Однако, так как это требует, чтобы ядро перекомпилировало, оно обычно не рекомендуется.