В ядре Linux® используется ряд особых возможностей набора компиляторовGNU (GCC) — от возможностей упрощения и более короткой записи допредоставления компилятору подсказок для оптимизации кода. Откройте длясебя некоторые из этих особых возможностей GCC и узнайте, как ихиспользовать в ядре Linux.
GCC и Linux — это великолепная пара. Хотя это независимые друг отдруга программные продукты, Linux полностью зависит от GCC приразвертывании на новых архитектурах. Кроме того, возможности GCC,известные как расширения, используются в Linux дляполучения большей функциональности и оптимизации. В этой статьеисследуются многие из этих важных расширений и рассказывается об ихиспользовании в ядре Linux.
Текущая стабильная версия GCC (версия 4.3.2) поддерживает 3 версии стандарта C:
- оригинальный стандарт Международной организации по стандартизации (ISO) языка C (ISO C89 или C90)
- ISO C90 с поправкой 1
- Текущий стандарт ISO C99 (стандарт, используемый GCC по умолчанию, в статье предполагается использование именно его)
Замечание: В статье предполагается, что вы используете стандартISO C99. Если вы укажете использовать более раннюю версию стандарта,чем ISO C99, некоторые из расширений, описанных в этой статье, могутбыть выключены. Указать GCC используемую версию стандарта, можно спомощью опции командной строки -std
.
Имеющиеся расширения C можно классифицировать несколькими способами. В этой статье мы их разделяем на две большие группы:
- Функциональные расширения, дающие вам благодаря GCC новые возможности.
- Оптимизационные расширения, — помогающие генерировать более эффективный код.
Начнем с изучения некоторых приемов GCC, расширяющих стандартный язык C.
GCCпозволяет идентифицировать тип переменной по ссылке на нее. Такойподход создает возможности для реализации того, что часто называют обобщенным программированием (generic programming). Подобная функциональность присутствует во многих современных языках, таких как: C++, Ada, иJava™. В Linux для построения зависимых от типа операций, таких как min
и max
используется команда typeof
. В листинге 1 показано, как можно использовать typeof
для создания обобщенных макросов (из./linux/include/linux/kernel.h).
Листинг 1. Использование typeof
для создания обобщенных макросов
|
GCCвключает в себя поддержку интервалов, которые можно использовать вомногих областях языка C. Одним из таких мест являются инструкции case
в блоках switch
/case
. В сложных структурах условий обычно приходится использовать каскады инструкций if
для получения того же самого результата, что представлен в более элегантной форме в листинге 2(из./linux/drivers/scsi/sd.c). Кроме того, при использовании switch
/case
в компиляторе включается оптимизация, использующая реализацию таблиц перехода.
Листинг 2. Использование интервалов внутри инструкций case
|
Интервалы также можно использовать для инициализации данных, как показано ниже (из./linux/arch/cris/arch-v32/kernel/smp.c). В этом примере создается массив spinlock_t
размера LOCK_COUNT
. Каждый элемент массива инициализируется значением SPIN_LOCK_UNLOCKED
.
/* Вектор блокировок, используемых для различных атомарных операций */ |
Интервалы можноприменять и для более сложных способов инициализации. Например,следующий код задает различные значения для разных подынтерваловмассива.
int widths[] = { [0 ... 9] = 1, [10 ... 99] = 2, [100] = 3 }; |
Согласностандарту С, для массива необходимо определить как минимум одинэлемент. Как правило, это требование усложняет проектирование кода.Однако GCC поддерживает концепцию массивов нулевой длины, которые могутбыть особенно полезны при определении структур данных. Эта концепцияпохожа на гибкие элементы массива в ISO C99, но использует другойсинтаксис.
В следующем примере в конце структурыобъявляется массив нулевой длины (из./linux/drivers/ieee1394/raw1394-private.h). Это позволяет экземпляруэтой структуры ссылатся на память, следующую непосредственно за ней.Это может быть полезно, когда вам необходимо иметь переменноеколичество элементов в массиве.
struct iso_block_store { |
Воммногих случаях, может быть полезно или даже необходимо определитьместо, откуда была вызвана функция. GCC для этого предоставляетвстроенную функцию __builtin_return_address
. Эта функция часто используется при отладке, но также имеет множество других применений в ядре Linux.
Как показано в коде ниже,__builtin_return_address
имеет аргумент, называемый level
. Этот аргумент определяет уровень в стеке вызовов, для которого вы хотите получить адрес. Например, если вы зададите level
равным 0
, вы получите адрес текущей функции. Если вы зададите level
равным 1
, вы получите адрес вызывающей функции и так далее.
void * __builtin_return_address( unsigned int level ); |
Функция local_bh_disable
в следующем примере (из ./linux/kernel/softirq.c) выключает механизмыотложенных прерываний (softirq), тасклетов и механизм нижних половин налокальном процессоре. Адрес возврата узнается с помощью __builtin_return_address
для дальнейшего использования при трассировке.
void local_bh_disable(void) |
GCCпредоставляет встроенную функцию, которую можно использовать чтобыопределить, является ли некоторое значение константой временикомпиляции или нет. Это полезная информация, зная которую вы можетесоставлять выражения, которые могут быть оптимизированы с помощьюсвертки констант. Для такой проверки используется функция __builtin_constant_p
.
Прототип для функции __builtin_constant_p
показан ниже. Заметьте, что __builtin_constant_p
определяет не все константы, так как некоторые из них не так просто выявить средствами GCC.
int __builtin_constant_p( exp ) |
Выявление константдовольно часто используется в Linux. В примере, показанном в листинге 3(из ./linux/include/linux/log2.h), выявление констант используется дляоптимизации макроса roundup_pow_of_two
. Если выражениераспознается как константа, то для оптимизации используется специальноеконстантное выражение. Если же выражение не является константой,вызывается другая макрофункция для округления значения до степенидвойки.
Листинг 3. Использование выявления констант для оптимизации макрофункций
|
ВGCC имеется несколько атрибутов уровня функции, используя которые выможете предоставлять компилятору больше информации для оптимизации. Вэтом разделе описываются некоторые из этих атрибутов, связанные сфункциональностью. В следующем разделе рассказывается об атрибутах, влияющих на производительность.
Как показано в листинге 4 (из ./linux/include/linux/compiler-gcc3.h),атрибутам функций даются символьные обозначения (алиасы). Вы можетеиспользовать этот листинг как руководство при чтении следующих примеровкода, демонстрирующих использование атрибутов функций.
Листинг 4. Определение атрибутов функций
|
Определения в листинге 4отражают некоторые атрибуты функций, доступные в GCC. Также это одни изнаиболее полезных атрибутов функций в ядре Linux. В дальнейшем мыпокажем, как наилучшим образом использовать эти атрибуты:
always_inline
— указывает GCC всегда подставлять функции, независимо от того включена оптимизация или нет.deprecated
— сигнализирует вам, что функция устарела и ее больше не следуетиспользовать. Если вы попытаетесь использовать устаревшую функцию,компилятор выдаст предупреждение. Этот атрибут также можно применять ктипам и переменным.__used__
— сообщает компилятору, что эта функция используется, независимо оттого найдет ли GCC экземпляры вызова этой функции. Это может бытьполезно в тех случаях, когда функции С вызываются из ассемблера.__const__
— сообщает компилятору, что эта функция не имеет состояния (т.е.использует для генерации возвращаемого результата только переданные ейаргументы).warn_unused_result
— принуждает компилятор всегда проверять, что возвращаемое значениефункции проверяется в месте вызова. Этим гарантируется, что везде,откуда вызывается функция результат будет проверяться, что позволяетобработать потенциальные ошибки.
Далее показаны примеры таких функций, используемые в ядре Linux.Пример deprecated
взят из независимого от архитектуры ядра (./linux/kernel/resource.c), а пример const
из кода ядра для архитектуры IA64(./linux/arch/ia64/kernel/unwind.c).
int __deprecated __check_region(struct resource |
Теперь давайте изучим некоторые имеющиеся в GCC приемы для генерации наилучшего возможного машинного кода.
Подсказывание наиболее вероятной ветви
Одна из самых широко используемых в ядре Linux техник оптимизации — это __builtin_expect
.Работая с условиями в коде, вы часто знаете какая ветвь наиболеевероятна, а какая — нет. Если компилятор знает эту прогнознуюинформацию, он может сгенерировать наиболее оптимальный код обходаветвей.
Как показано ниже, использование __builtin_expect
основано на двух макросах, называемых likely
и unlikely
(из./linux/include/linux/compiler.h).
#define likely(x) __builtin_expect(!!(x), 1) |
Когда вы используете __builtin_expect
,компилятор может принимать решения по выбору инструкций учитываяпредоставляемую вами прогнозную информацию. Это позволяет расположитькод, выполнение которого наиболее вероятно, ближе к условию. Также этоулучшает кэширование и передачу инструкций.
Например,если условие помечено likely, то компилятор может поместить порциюкода True непосредственно после ветвления. Код для варианта False вэтом случае будет доступен через инструкцию ветвления, что не такоптимально, но и менее вероятно. При таком способе код оптимизируетсядля наиболее вероятного варианта.
В листинге 5 показана функция, в которой используются как макрос likely
, так и unlikely
(из ./linux/net/core/datagram.c). Функция ожидает, что переменная sum
будет равна нулю(контрольная сумма для пакета верна) и что переменная ip_summed
не равна CHECKSUM_HW
.
Листинг 5. Пример использования макросов likely и unlikely
|
Другойважный способ улучшения производительности — кэширование необходимыхданных рядом с процессором. Кэширование минимизирует количествовремени, необходимое для обращения к данным. Самые современныепроцессоры имеют три класса памяти:
- Кэш 1-го уровня — как правило поддерживает доступ к данным в течение одного такта
- Кэш 2-го уровня поддерживает доступ к данным в течение двух тактов
- Системная память — поддерживает более продолжительное время доступа
Чтобыминимизировать задержки доступа к данным и таким образом улучшитьпроизводительность, лучше всего держать данные в ближайшей к процессорупамяти. Выполнение этой задачи вручную называется предварительной выборкой. GCC поддерживает предварительную выборку данных вручную с помощью встроенной функции, называемой __builtin_prefetch
. Эта функция используется для помещения данных в кэш незадолго до того как они понадобятся. Как показано ниже, функция__builtin_prefetch
принимает три аргумента:
- адрес данных
- параметр
rw
— используется для индикации того, подготавливаются ваши данные для чтения (операция Read) или для записи (операция Write) - параметр
locality
, определяющий что следует сделать с данными после использования, — оставить в кэше или удалить их оттуда
void __builtin_prefetch( const void *addr, int rw, int locality ); |
Предварительная выборкаданных интенсивно используется ядром Linux. Наиболее часто онареализуется с помощью макросов и оберточных функций. Листинг 6 содержитпример вспомогательной функции, в которой используется такаяфункция-обертка (из ./linux/include/linux/prefetch.h). В функцииреализуется механизм упреждающего просмотра вперед для потоковыхопераций. Использование этой функции, как правило, дает улучшениепроизводительности за счет минимизации неудачных обращений к кэшу ипростаивания данных в кэше.
Листинг 6. Оберточная функция для предварительной выборки блока данных
|
Вдополнение к атрибутам функций, обсуждавшимся ранее в этой статье, вGCC также имеются атрибуты переменных и определения типов. Один изнаиболее важных атрибутов — это aligned
, которыйиспользуется для выравнивания объектов в памяти. Помимо того, чтоиспользование выравнивания объектов в памяти важно дляпроизводительности, оно может быть необходимо для определенныхустройств и конфигураций железа. Атрибут aligned
имеет один аргумент, описывающий желаемый тип выравнивания.
Следующий пример используется для программной приостановки выполнения (из./linux/arch/i386/mm/init.c). Объект PAGE_SIZE
представляет собой требуемое выравнивание страницы.
char __nosavedata swsusp_pg_dir[PAGE_SIZE] |
Пример в листинге 7 иллюстрирует пару моментов, связанных с оптимизацией:
- Атрибут
packed
упаковывает элементы структуры таким образом, чтобы она занимала как можно меньше места.Это значит, что если определена переменная типаchar
, она будет занимать не больше чем байт (8 бит). Битовые поля сжимаются до одного бита, вместо того чтобы занимать больше места. - В этом коде оптимизация осуществляется с помощью одной спецификации
__attribute__
, которая определяет несколько разделенных запятой атрибутов.
Листинг 7. Упаковка структур и задание множественных атрибутов
|
Свежие комментарии