В ядре 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 для создания обобщенных макросов
#define min(x, y) ({ \ typeof(x) _min1 = (x); \ typeof(y) _min2 = (y); \ (void) (&_min1 == &_min2); \ _min1 < _min2 ? _min1 : _min2; })
|
Интервалы
GCC
включает в себя поддержку интервалов, которые можно использовать во
многих областях языка C. Одним из таких мест являются инструкции case в блоках switch /case . В сложных структурах условий обычно приходится использовать каскады инструкций if для получения того же самого результата, что представлен в более элегантной форме в листинге 2
(из
./linux/drivers/scsi/sd.c). Кроме того, при использовании
switch /case в компиляторе включается оптимизация, использующая реализацию таблиц перехода.
Листинг 2. Использование интервалов внутри инструкций case
static int sd_major(int major_idx) { switch (major_idx) { case 0: return SCSI_DISK0_MAJOR; case 1 ... 7: return SCSI_DISK1_MAJOR + major_idx - 1; case 8 ... 15: return SCSI_DISK8_MAJOR + major_idx - 8; default: BUG(); return 0; /* shut up gcc */ } }
|
Интервалы также можно использовать для инициализации данных, как показано ниже (из
./linux/arch/cris/arch-v32/kernel/smp.c). В этом примере создается массив spinlock_t размера LOCK_COUNT . Каждый элемент массива инициализируется значением SPIN_LOCK_UNLOCKED .
/* Вектор блокировок, используемых для различных атомарных операций */ spinlock_t cris_atomic_locks[] = { [0 ... LOCK_COUNT - 1] = 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 { atomic_t refcount; size_t data_size; quadlet_t data[0]; };
|
Определение адреса вызова
Вом
многих случаях, может быть полезно или даже необходимо определить
место, откуда была вызвана функция. 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) { __local_bh_disable((unsigned long)__builtin_return_address(0)); }
|
Выявление констант
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. Использование выявления констант для оптимизации макрофункций
#define roundup_pow_of_two(n) \ ( \ __builtin_constant_p(n) ? ( \ (n == 1) ? 1 : \ (1UL << (ilog2((n) - 1) + 1)) \ ) : \ __roundup_pow_of_two(n) \ )
|
Атрибуты функций
В
GCC имеется несколько атрибутов уровня функции, используя которые вы
можете предоставлять компилятору больше информации для оптимизации. В
этом разделе описываются некоторые из этих атрибутов, связанные с
функциональностью. В следующем разделе рассказывается об атрибутах, влияющих на производительность.
Как показано в листинге 4 (из ./linux/include/linux/compiler-gcc3.h),
атрибутам функций даются символьные обозначения (алиасы). Вы можете
использовать этот листинг как руководство при чтении следующих примеров
кода, демонстрирующих использование атрибутов функций.
Листинг 4. Определение атрибутов функций
# define __inline__ __inline__ __attribute__((always_inline)) # define __deprecated __attribute__((deprecated)) # define __attribute_used__ __attribute__((__used__)) # define __attribute_const__ __attribute__((__const__)) # define __must_check __attribute__((warn_unused_result))
|
Определения в листинге 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 *parent, unsigned long start, unsigned long n)
static enum unw_register_index __attribute_const__
decode_abreg(unsigned char abreg, int memory)
|
Расширения оптимизации
Теперь давайте изучим некоторые имеющиеся в GCC приемы для генерации наилучшего возможного машинного кода.
Подсказывание наиболее вероятной ветви
Одна из самых широко используемых в ядре Linux техник оптимизации - это __builtin_expect .
Работая с условиями в коде, вы часто знаете какая ветвь наиболее
вероятна, а какая - нет. Если компилятор знает эту прогнозную
информацию, он может сгенерировать наиболее оптимальный код обхода
ветвей.
Как показано ниже, использование __builtin_expect основано на двух макросах, называемых likely и unlikely (из
./linux/include/linux/compiler.h).
#define likely(x) __builtin_expect(!!(x), 1) #define unlikely(x) __builtin_expect(!!(x), 0) |
Когда вы используете __builtin_expect ,
компилятор может принимать решения по выбору инструкций учитывая
предоставляемую вами прогнозную информацию. Это позволяет расположить
код, выполнение которого наиболее вероятно, ближе к условию. Также это
улучшает кэширование и передачу инструкций.
Например,
если условие помечено "likely", то компилятор может поместить порцию
кода True непосредственно после ветвления. Код для варианта False в
этом случае будет доступен через инструкцию ветвления, что не так
оптимально, но и менее вероятно. При таком способе код оптимизируется
для наиболее вероятного варианта.
В листинге 5 показана функция, в которой используются как макрос
likely , так и unlikely (из ./linux/net/core/datagram.c). Функция ожидает, что переменная
sum будет равна нулю
(контрольная сумма для пакета верна) и что переменная ip_summed не равна CHECKSUM_HW .
Листинг 5. Пример использования макросов likely и unlikely
unsigned int __skb_checksum_complete(struct sk_buff *skb) { unsigned int sum;
sum = (u16)csum_fold(skb_checksum(skb, 0, skb->len, skb->csum)); if (likely(!sum)) { if (unlikely(skb->ip_summed == CHECKSUM_HW)) netdev_rx_csum_fault(skb->dev); skb->ip_summed = CHECKSUM_UNNECESSARY; } return sum; }
|
Предварительная выборка
Другой
важный способ улучшения производительности - кэширование необходимых
данных рядом с процессором. Кэширование минимизирует количество
времени, необходимое для обращения к данным. Самые современные
процессоры имеют три класса памяти:
- Кэш 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. Оберточная функция для предварительной выборки блока данных
#ifndef ARCH_HAS_PREFETCH #define prefetch(x) __builtin_prefetch(x) #endif
static inline void prefetch_range(void *addr, size_t len) { #ifdef ARCH_HAS_PREFETCH char *cp; char *end = addr + len;
for (cp = addr; cp < end; cp += PREFETCH_STRIDE) prefetch(cp); #endif }
|
Атрибуты переменных
В
дополнение к атрибутам функций, обсуждавшимся ранее в этой статье, в
GCC также имеются атрибуты переменных и определения типов. Один из
наиболее важных атрибутов - это aligned , который
используется для выравнивания объектов в памяти. Помимо того, что
использование выравнивания объектов в памяти важно для
производительности, оно может быть необходимо для определенных
устройств и конфигураций "железа". Атрибут aligned имеет один аргумент, описывающий желаемый тип выравнивания.
Следующий пример используется для программной приостановки выполнения (из
./linux/arch/i386/mm/init.c). Объект PAGE_SIZE представляет собой требуемое выравнивание страницы.
char __nosavedata swsusp_pg_dir[PAGE_SIZE] __attribute__ ((aligned (PAGE_SIZE)));
|
Пример в листинге 7 иллюстрирует пару моментов, связанных с оптимизацией:
- Атрибут
packed упаковывает элементы структуры таким образом, чтобы она занимала как можно меньше места.
Это значит, что если определена переменная типа char , она будет занимать не больше чем байт (8 бит). Битовые поля сжимаются до одного бита, вместо того чтобы занимать больше места. - В этом коде оптимизация осуществляется с помощью одной спецификации
__attribute__ , которая определяет несколько разделенных запятой атрибутов.
Листинг 7. Упаковка структур и задание множественных атрибутов
static struct swsusp_header { char reserved[PAGE_SIZE - 20 - sizeof(swp_entry_t)]; swp_entry_t image; char orig_sig[10]; char sig[10]; } __attribute__((packed, aligned(PAGE_SIZE))) swsusp_header;
|
|