Применение специальных возможностей GCC в ядре Linux

В ядре 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;