VisualDSP: полезные директивы #pragma Печать
Добавил(а) microsin   

В этой статье приведены наиболее часто используемые директивы #pragma. Подробнее про директивы #pragma написано в руководстве [1], раздел Compiler -> C/C++ Compiler Language Extensions -> Pragmas.

[Общие сведения]

Компилятор Blackfin C/C++ поддерживает прагмы. Это специальные директивы, которые модифицируют поведение компилятора. Есть 2 способа использования прагм: директивы pragma и операторы pragma.

Директивы pragma имеют следующий синтаксис:

#pragma pragma-directive pragma-directive-operands new-line

Операторы pragma имеют следующий синтаксис:

_Pragma (string-literal)

Когда обрабатывается оператор pragma, компилятор превращает его в директиву pragma directive, используя не строковую версию операнда строкового литерала string-literal. Это означает, что следующая директива pragma:

#pragma linkage_name mylinkname

может быть также быть заменена следующим эквивалентным оператором pragma operator:

_Pragma ("linkage_name mylinkagename")

Примеры в этом руководстве используют форму директивы.

Компилятор языка C поддерживает прагмы для следующих целей:

• Управление выравниванием данных.
• Управление выводом диагностических сообщений компилятора.
• Определение функций, которые могут служить обработчиками прерываний.
• Изменение уровня оптимизации, которое может применяться внутри модуля кода.
• Изменение того, как будет слинкована видимая внешняя функция.
• Предоставление заголовочного файла для конфигураций и свойств.
• Предоставление дополнительной информации для цикла в целях улучшения его оптимизаций.

Компилятор выдает предупреждения, когда он встречает не распознанные директивы или операторы pragma.

[Управление выравниванием данных]

Может использоваться перед различными декларациями структур и полей. Прикладывается непосредственно к переменной, которая сразу следует за директивой #pragma align. Синтаксис:

#pragma align num

Эта прагма дает эффект, что следующая за ней переменная или поле принудительно будут выровнены по границе адреса блока памяти, как это указано числовым параметром num, следующим образом:

• Если pragma прикладывается к локальной переменной (которая сохраняется в стеке), то выравнивание переменной будет изменено только в том случае, когда число num не больше, чем выравнивание стека, т. е. 4 байта. Если же num больше выравнивания стека, то компилятор выдаст предупреждение, и pragma будет игнорирована.

• Если num больше, чем выравнивание, которое нормально требуется для выравнивания следующей декларируемой переменной или поля, то переменная или поле будут выравнены в памяти по границе с адресом, кратным num.

• Если num меньше, чем нормально требуемое выравнивание, то выравнивание переменной или поля при декларации будет изменено в соответствии с num, и компилятором будет выдано предупреждение, что выравнивание было уменьшено.

Вместо числового параметра num, который означает кратность адреса в байтах, могут быть указаны ключевые слова, задающие возможные выравнивания:

_WORD задает 32-разрядное выравнивание (соответствует num==4)
_LONG задает 64-разрядное выравнивание (соответствует num==8)
_QUAD задает 128-разрядное выравнивание (соответствует num==16)

Если в настоящий момент активно действие директив #pragma pack или #pragma pad pragma (см. врезки далее), то выравнивание этих директив будет изменено непосредственно следующей декларацией поля.

В следующих примерах показано, как использовать #pragma align.

struct s{
#pragma align 8 /* поле a выравнено по 8-байтной границе адреса */
   int a;
   int bar;
 
#pragma align 16 /* поле b выровнено по 16-байтной границе адреса */
   int b;
} t[2];
 
#pragma align 256
int arr[128]; /* декларируется массив чисел int с выравниванием 256 */

Следующий пример показывает допустимую директиву, однако на неё будет выдано предупреждение компилятора.

#pragma align 1
int warns; /* декларируется int с байтовым выравниванием, */
           /* что вызывает предупреждение компилятора */

Следующий пример показывает недопустимое использование #pragma align. Поскольку выравнивание не является степенью числа 2, компилятор не позволит его использовать и выдаст ошибку.

#pragma align 3
int errs; /* НЕДОПУСТИМО: декларируется int с выравниванием, не являющимся */
          /* степенью двойки, компилятор выдаст ошибку */

Внимание: #pragma align прикладывается только с непосредственно следующей переменной при декларации, даже если она является частью списка. Например:

#pragma align 8
int i1, i2, i3; // pragma align будет применена только к переменной i1

Директива pack используется для упаковки полей структуры. Синтаксис:

#pragma pack (alignopt)

Прагма pack может применяться к определениям структур (struct). Она прикладывается ко всем определениям структур, которые следуют после неё, пока не будет восстановлено выравнивание по умолчанию, когда в директиве пропущен параметр alignopt (например, когда задана #pragma pack() с пустыми круглыми скобками).

Прагма pack используется для уменьшения выравнивания по умолчанию в структуре до значения alignopt. Если поля в структуре имеют выравнивание по умолчанию больше, то их выравнивание будет уменьшено до alignopt. Если поля в структуре имеют выравнивание меньше alignopt, то их выравнивание останется без изменения.

Если указано значение alignopt, то нельзя использовать #pragma pad, пока не будет восстановлено значение выравнивания по умолчанию. Компилятор генерирует сообщение об ошибке, если прагмы pad и pack используются так, что получается конфликт.

В следующем примере показано, как использовать #pragma pack:

#pragma pack(1)
/* Минимальное выравнивание в структуре теперь 1 байт, так что использование
   "#pragma pad" теперь приведет к ошибке компиляции */
 
struct is_packed {
   char a;
   /* в этом месте обычно компилятор добавил бы 3 лишних байта для выравнивания,
      но теперь этого не будет, потому что ранее была использована pragma pack(1) */
   int b;
} t[2]; /* упакованное определение t займет 10 байт */
 
#pragma pack()
 
/* Теперь минимальное выравнивание структуры не 1 байт,
так что может легально использоваться
"#pragma pad" */
struct is_packed u[2]; /* определение u требует 10 упакованных байт */
 
/* Далее идет структура not_packed, которая задает новый тип,
   так что она не будет упакована. */
struct not_packed {
   char a;
   /* в этом месте компилятор вставит 3 лишних байта */
   int b;
} w[2]; /* определение w потребует 16 байт */

Процессор Blackfin не поддерживает доступы к не выровненным участкам памяти на аппаратном уровне; поэтому компилятор генерирует дополнительные код, чтобы корректно обработать чтения из не выровненных полей структуры (и записи в них). Сгенерированный код не обязательно будет таким же эффективным, как и чтение/запись выровненных полей, но это вынужденный компромисс, на который приходится идти для доступа к упакованным структурам.

Компилятором будет корректно обработаны только прямые чтения/записи не выровненных полей структуры. В результате, если взять адрес от не выровненного поля и присвоить его указателю, то это приведет к выдаче предупреждения компилятором. Причина предупреждения в том, что обращение по такому указателю не будет детектировано компилятором и может привести к ошибке, если адрес не выровненного поля получен и сохранен в указателе другого типа для той же структуры.

Поскольку #pragma pack уменьшает требования к выравниванию, и таким образом уменьшает необходимость для добавления нулевых байт к полям структуры, общий размер структуры может быть уменьшен; фактически именно это уменьшение как раз и является причиной использования этой прагмы. Однако имейте в виду, что уменьшенное выравнивание может быть применено к структуре в целом, так что экземпляры структуры могут начинаться на границах alignopt вместо границ по умолчанию, эквивалентных для не упакованных структур.

Для предыдущей версии VisualDSP++ 4.0 это работало не так. Компилятор уменьшал внутреннее выравнивание, но сохранял общее выравнивание. Начиная с VisualDSP++ 4.0 упакованные структуры могут начаться с разных границ от не упакованных структур. Чтобы сохранить общее начальное выравнивание, используйте #pragma align на первом поле структуры.

Синтаксис:

#pragma pad (alignopt)

Прагма pad pragma может применяться к определениям структур. Она прикладывается к определениям структур, которые следуют выравниванию по умолчанию, восстановленному пропуском alignopt (например, #pragma pad() с пустыми скобками).

Прагма pad является эффективной заменой для размещения #pragma align перед каждым полем внутри определения структуры. Наподобие прагмы pack, она уменьшает выравнивание полей, у которых по умолчанию выравнивание больше, чем alignopt.

Однако, в отличие от прагмы pack, она также увеличивает выравнивание полей, которые по умолчанию меньше, чем alignopt. Также #pragma pack (alignopt) выдает предупреждение, когда выравнивание поля уменьшено, а #pragma pad (alignopt) этого не делает.

Если указан параметр alignopt, то нельзя использовать #pragma pack, пока не будет восстановлено выравнивание по умолчанию.

Следующий пример показывает, как использовать #pragma pad().

#pragma pad(4)
struct {
   int i;
   int j;
} s = {1,2};
#pragma pad()

[Управление диагностикой]

Компилятор поддерживает директивы #pragma diag, которые позволяют селективный выбор важности диагностических сообщений компилятора. У этой директивы есть 3 формы:

• Модификация важности отдельных диагностик.
• Модификация поведения целого класса диагностик.
• Сохранение или восстановление текущего поведения всех диагностик.

Модификация важности отдельных диагностик. Эта форма директивы имеет следующий синтаксис:

#pragma diag(ACTION: DIAG [, DIAG ...][: STRING])

Здесь ACTION является квалификатором, который может быть одним из ключевых слов, перечисленных в таблице 1-32.

Таблица 1-32. Ключевые слова для квалификатора ACTION.

Ключевое слово Действие
suppress Подавляет все указанные диагностические проверки.
remark Изменяет важность указанной диагностики к комментарию.
warning Изменяет важность указанной диагностики к предупреждению.
error Изменяет важность указанной диагностики к ошибке.
restore Восстанавливает важность диагностики к оригинальному значению, которое было при запуске компиляции (состояние, полученное после обработки всех опций командной строки компилятора).

Если не используется режим MISRA-C, то квалификатор DIAG может быть одним или большим количеством перечисленных через запятую номеров диагностических сообщений компилятора без предшествующих префиксов "cc" или нулей. Выбор номеров ошибок ограничен теми, у которых может быть изменена их важность (это те, которые отображают "{D}" в сообщении об ошибке).

Дополнительно некоторые диагностики являются глобальными (например, диагностики, выданные бакэндом компилятора после лексического анализа и парсинга, или перед началом парсинга), и у этих глобальных диагностик нельзя поменять их важность через прагмы, управляющие диагностикой. Чтобы модифицировать важность глобальных диагностик, используйте опции командной строки, управляющие диагностикой. Подробнее см. описание опции -W{error|remark|suppress|warn}.

В режиме MISRA-C квалификатор DIAG является списком номеров правил MISRA-C в форме misra_rule_6_3 и misra_rule_19_4 для правил 6.3 и 19.4, и так далее. Правила 10.1 и 10.2 являются специальным случаем, в котором оба правила разделяются на 4 отдельные проверки правил. Например, 10.1(c) должно быть обозначено как misra_rule_10_1_c.

Третий, не обязательный аргумент это строковый литерал для вставки комментария, относящегося к использованию #pragma diag.

Модификация поведения целого класса диагностик. Эта форма директивы имеет следующий синтаксис, который не разрешен в режиме MISRA-C:

#pragma diag(ACTION)

Эффект получается следующий:

#pragma diag(errors). Эта прагма может быть использована, чтобы запретить все последующие сообщения и ремарки (эквивалентна опции -w командной строки).

#pragma diag(remarks). Эта прагма может быть использована для разрешения последующих ремарок и предупреждений (эквивалентна опции -Wremarks командной строки).

#pragma diag(warnings). Эта прагма может быть использована для восстановления поведения по умолчанию, когда не заданы ни одна из опций -w или -Wremarks, которые отображают предупреждения, но запрещают ремарки.

Сохранение или восстановление текущего поведения всех диагностик. Эта прагма имеет следующий синтаксис:

#pragma diag(push)
#pragma diag(pop)

Эффект получается следующий:

#pragma diag(push). Эта прагма может использоваться для сохранения текущего состояния важности всех диагностических сообщений об ошибках.

#pragma diag(pop). Эта прагма восстанавливает все диагностические сообщения, которые были ранее сохранены ближайшим предыдущим push.

Все директивы #pragma diag(push) должны соответствовать такому же количеству последующих директив #pragma diag(pop) всего юнита трансляции, но не должны быть соответствующими в отдельных исходных файлах, за исключением режима MISRA-C. Обратите внимание, что порог ошибки (установленный на ключевые слова remarks, warnings или errors) также сохранится и восстановится этими директивами.

Длительность таких модификаций важности диагностики продолжается от следующей строки за директивой pragma или до конца юнита трансляции, или до следующей директивы #pragma diag(pop), или до следующей директивы #pragma diag() с тем же номером ошибки. Эти прагмы могут использоваться могут использоваться в любых местах кода, и на них не влияют обычные правила области видимости (normal scoping rules).

Сначала обрабатываются все изменения диагностики, задаваемые из командной строки, и любые последующие директивы #pragma diag() получают приоритет, когда действие восстановления откатывает обработку важности обратно к моменту начала компиляции, когда были обработаны опции командной строки.

Примечание: директивы для модификации отдельных диагностик сингулярны (упоминаются в единственном числе, например "error"), и директивы для модификации классов диагностик плюральны (во множественном числе, "errors").

[Передача компилятору атрибутов]

Прагма file_attr указывает компилятору выдать специальные атрибуты, когда он обрабатывает файл, содержащий эту прагму. В одной строке допускается использовать несколько директив #pragma file_attr. Синтаксис:

#pragma file_attr("name[=value]" [, "name[=value]" [...]])

Если опущено "=value", то будет использоваться значение по умолчанию "1".

Значение атрибута это все символы, которые следуют после символа '=' и перед закрывающего символа ", включая пробелы. Будет выдано предупреждение, если были предшествующие или завершающие пробелы в значении атрибута, поскольку это скорее всего будет ошибкой.

Подробнее про атрибуты см. раздел "File Attributes" руководства [1]. Также см. [3].

Прагмы управления файлами заголовка (hdrstop, no_implicit_inclusion, no_pch, once и system_header) помогают компилятору обрабатывать заголовочные файлы (header files).

Прагма hdrstop используется вместе с опцией -pch (precompiled header). Опция -pch инструктирует компилятор на просмотр предварительно скомпилированного заголовка (precompiled header, файл с расширением .pch file), и, если он не найден, генерировать файл для использования в последующих компиляциях. Файл .pch содержит снимок всего кода, предшествующего точке header stop.

По умолчанию точка header stop это первая точка в модуле кода, где заканчивается препроцессинг. Директива #pragma hdrstop может использоваться для установки точки пораньше.

В следующем примере точка header stop по умолчанию header stop находится в начале декларации переменной i.

#include "standard_defs.h"
#include "common_data.h"
#include "frequently_changing_data.h"
 
int i;

Такой вариант может быть не очень хорошим выбором, потому что если в файле frequently_changing_data.h могут быть часто изменяемые данные, то это приведет к частой регенерации файла .pch, и в результате получится потеря выгоды от использования precompiled header. Тогда hdrstop pragma может использоваться для переноса точки header stop в более подходящее место.

В следующем примере так и сделано - файл предварительно скомпилированных заголовков не будет включать содержимое frequently_changing_data.h, потому что он подключается после прагмы hdrstop, так что не нужно будет регенерировать precompiled header всякий раз, когда был модифицирован frequently_changing_data.h.

#include "standard_defs.h"
#include "common_data.h"
#pragma hdrstop
#include "frequently_changing_data.h"
 
int i;

[Управление оптимизацией]

В коде можно применять директивы all_aligned, different_banks, extra_loop_loads, optimize_off, optimize_for_speed и другие директивы. Подробнее см. [4].

[Управление линкером]

Прагма retain_name показывает, что декларация функции или переменной, которая сразу следует за этой прагмой, не должна быть удалена линкером, если нигде не используется в коде. Обычно если разрешен межпроцедурный анализ (IPA), или если линкеру разрешено удалять неиспользуемые функции или переменные (linker elimination), то инструментарий VisualDSP++ определит неиспользуемые функции и переменные, и удалит их из результирующего выходного исполняемого кода с целью экономии памяти. Прагме retain_name инструктирует инструментарий, что указанный код/переменная должны быть сохранены.

Следующий пример показывает, как использовать эту прагму.

int delete_me(int x)
{
   return x-2;
}
 
#pragma retain_name
int keep_me(int y)
{
   return y+2;
}
 
int main(void)
{
   return 0;
}

Поскольку программа не использует ни функцию delete_me(), ни keep_me(), то обычно компилятор удалил бы из выходного кода обе эти функции. Но в данном примере компилятор удалит только функцию delete_me(), но функция keep_me() будет сохранена, потому что перед ней стоит эта прагма retain_name. Для функции main() директива retain_name не нужна.

Эта прагма допустима только для глобальных символов. Она недопустима для следующего типа символов:

• Символы со статически классом размещения (static storage class).
• Параметры функции.
• Символы с автоматическим классом размещения (auto storage class, локальные переменные). Они будут размещены в стеке во время выполнения кода.
• Члены/поля структур (structs), объединений (union), классов (class).
• Декларации типа.

Подробнее про межпроцедурный анализ (IPA) см. [5].

[Управление размещением кода и данных]

Прагмы компилятора section и default_section предоставляют качественный контроль над секциями памяти, куда компилятор помещает символы.

Прагма section(СТРОКАСЕКЦИИ [, QUALIFIER, ...]) используется для переопределения целевой секции для любого глобального или статического символа, который сразу за ней следует. Прагма позволяет лучше управлять квалификатором секции в сравнении с ключевым словом section.

Прагма default_section(ВИДСЕКЦИИ [, СТРОКАСЕКЦИИ [, QUALIFIER, ...]]) используется для переназначения секций по умолчанию, в которые компилятор помещает свои символы.

Секции по умолчанию попадают в категории, перечисленные под ВИДСЕКЦИИ. За исключением категории STI, эта прагма остается в силе для категории секции до её следующего использования с отдельной категорией, или до конца файла. STI является исключением, только в одном STI можно определить default_section, и его область действия весь файл, не только часть после использования STI. Будет выдаваться предупреждение, если в одном и том же файле будет указано несколько секций STI.

Пропуск имени секции приводит к тому, секция по умолчанию будет сброшена к секции, которая была в использовании с начала файла, которая может быть либо значением по умолчанию для компилятора, либо значением, установленным пользователем через опцию командной строки -section (например, -section ВИДСЕКЦИИ=СТРОКАСЕКЦИИ).

Во всех случаях (включая STI), прагма default_section перезаписывает значение, указанное опцией -section в командной строке.

#pragma default_section(DATA, "NEW_DATA1")
int x;
#pragma default_section(DATA, "NEW_DATA2")
int x=5;
#pragma default_section(DATA, "NEW_DATA3")
int x;

В этом случае x размещается в NEW_DATA2 потому, что определение x попадает в её действие. Прагма default_section может быть использована только в глобальной области действия, где разрешены глобальные переменные.

ВИДСЕКЦИИ может быть одним из ключевых слов, перечисленных в таблице 1-29.

Таблица 1-29. Виды секций для параметра ВИДСЕКЦИИ.

Ключевое слово Описание
CODE Секция используется для кода процедур и функций программы.
ALLDATA Краткое указание на секции DATA, CONSTDATA, BSZ, STRINGS и AUTOINIT.
DATA Секция, которая содержит "обычные" данные (переменные).
CONSTDATA Секция, которая содержит данные только для чтения (константы).
BSZ Секция, которая содержит не инициализируемые данные.
SWITCH Секция используется для таблиц перехода реализации операторов switch языка C/C++.
VTABLE Секция используется для таблиц виртуальных функций C++.
STI Секция содержит код, необходимый для выполнения инициализаций C++. Подробнее см. раздел справки Constructors and Destructors of Global Class Instances.
STRINGS Секция, которая содержит строковые литералы.
AUTOINIT Секция содержит данные, используемые для инициализации агрегатов C++.

СТРОКАСЕКЦИИ является строкой, заключенной в двойные кавычки, содержащей имя секции, полностью как она появляется в файле ассемблера.

Изменение одного вида секции не влияет на другие виды секции. Например, даже при том, что виды секций STRINGS и CONSTDATA по умолчанию помещаются компилятором в одну и ту же секцию, если изменена секция по умолчанию для CONSTDATA, то это никак не повлияет на данные STRINGS.

Обратите внимание, что ALLDATA не реальная секция, а псевдо-вид, который соответствует DATA, CONSTDATA, STRINGS, AUTOINIT и BSZ. Изменение ALLDATA эквивалентно изменению всех этих видов секций. Таким образом,

#pragma default_section(ALLDATA, params)

будет эквивалентно последовательности:

#pragma default_section(DATA, params)
#pragma default_section(CONSTDATA, params)
#pragma default_section(STRINGS, params)
#pragma default_section(AUTOINIT, params)
#pragma default_section(BSZ, params)

QUALIFIER может быть одним из ключевых слов, перечисленных в таблице 1-30.

Таблица 1-30. Ключевые слова для QUALIFIER.

Ключевое слово Описание
ZERO_INIT Секция инициализируется нулями при запуске программы.
NO_INIT Секция не инициализируется при запуске программы.
RUNTIME_INIT Секция, инициализируемая пользователем при запуске программы.
DOUBLE32 Секция может содержать 32-разрядные, но не 64-разрядные числа типа double.
DOUBLE64 Секция может содержать 64-разрядные, но не 32-разрядные числа типа double.
DOUBLEANY Секция может содержать либо 32-разрядные, либо 64-разрядные числа типа double.

Может быть любое количество квалификаторов секции, следующих друг за другом через запятую с такими прагмами, но они не должны конфликтовать друг с другом. Квалификаторы также должны быть непротиворечивыми по прагмам для одинаковых имен секций, и пропуск квалификаторов не допускается, даже если как минимум один такой квалификатор появлялся в предыдущей прагме для той же секции. Если не указан какой-либо квалификатор для отдельной секции к концу блока трансляции, компилятор использует квалификаторы по умолчанию, подходящие для целевого процессора.

В следующем коде указано, что f() должна быть размещена в секции foo, квалифицированной как DOUBLEANY:

#pragma section("foo", DOUBLEANY)
void f() {}

Компилятор всегда пытается соблюдать прагму section как с высшим приоритетом, и прагма default_section всегда имеет приоритет меньше. Например, следующий код в результате приведет к тому, что функция f будет размещена в секции foo:

#pragma default_section(CODE, "bar")
#pragma section("foo")
void f() {}

В следующем коде x будет помещен в секции zeromem:

#pragma default_section(BSZ, "zeromem")
int x; 

Примечание: в случаях, когда объект C++ STL требуется разместить в какой-то определенной секции памяти, использование #pragma section/default_section не работает. Вместо этого должна использоваться куча не по умолчанию (non-default heap), как это разъяснено в руководстве "Allocating C++ STL Objects to a Non-Default Heap".

[Ссылки]

1. VisualDSP++ 5.0 C/C++ Compiler and Library Manual for Blackfin® Processors site:analog.com.
2. VisualDSP: использование секций памяти.
3. VDK: атрибуты файла.
4. VisualDSP: управление оптимизацией кода.
5Что такое Interprocedural Analysis (IPA).