Оптимизация с несколькими включениями
Я пытаюсь понять, как многократная оптимизация работает с gcc. В последнее время я читал много кода, который включает в себя защиту для стандартных заголовочных файлов, например
#ifndef _STDIO_H_
#include <stdio.h>
#endif
и я пытаюсь выяснить, имеет ли эта конструкция какие-либо преимущества.
Вот пример, который я написал, чтобы понять это немного лучше.
header1.h
#ifndef _HDR_H_
#define _HDR_H_
#define A (32)
#endif
header2.h
#ifndef _HDR_H_
#define _HDR_H_
#define A (64)
#endif
hdr.c
#include <stdio.h>
#include "header1.h"
#include "header2.h"
int main()
{
printf("%d\n", A);
return 0;
}
Обратите внимание, что оба header1.h
а также header2.h
использовать то же самое включить охрану. Как и ожидалось, эта программа выводит значение A
определено в header1.h; header2.h пропускается, так как он использует тот же защитный элемент include.
Вот что я пытаюсь понять
- В какой момент при анализе header2.h препроцессор пропускает этот файл? Насколько я понимаю, он пропускает этот файл сразу после
#if
директива в строке 1, то есть она не должна ждать соответствия#endif
, Это правильно? - Что я могу добавить к примеру выше, чтобы продемонстрировать, как это работает?
РЕДАКТИРОВАТЬ: Спасибо всем за ответы. Это начинает иметь больше смысла сейчас. Последующий вопрос. Страница, на которую ссылается первая строка этого поста, имеет следующий текст
Препроцессор замечает такие файлы заголовков, так что, если файл заголовка появляется в последующей директиве #include и определяется FOO, он игнорируется и не выполняет предварительную обработку или даже повторное открытие файла во второй раз. Это называется оптимизацией нескольких включений.
Если я правильно понимаю, это означает, что любой заголовочный файл читается только один раз, даже если он включен несколько раз для данного процесса компиляции. И поэтому дополнительные элементы защиты в коде приложения или заголовочном файле не дают никакой выгоды.
5 ответов
В какой момент при анализе header2.h препроцессор пропускает этот файл?
Файл не пропущен.
Насколько я понимаю, он пропускает этот файл сразу после директивы #if в строке 1, то есть ему не нужно ждать соответствующего #endif. Это правильно?
Да и нет. Некоторые компиляторы идентифицируют макрос sentry, когда он анализирует первый файл заголовка, и если он находит его во втором файле, он немедленно прекращает синтаксический анализ. Другие компиляторы снова проанализируют заголовок (ищет подходящие #endif
).
Что я могу добавить к примеру выше, чтобы продемонстрировать, как это работает?
Добавьте печатное сообщение внутри и снаружи макроса часового
#ifdef _HEADER_INCLUDED
#define _HEADER_INCLUDED
...
#pragma message ("inside sentry in " __FILE__ "\n")
#endif //#ifdef _HEADER_INCLUDED
#pragma message ("outside sentry in " __FILE__ "\n")
Соответствующий материал:
- Ты можешь использовать
#pragma once
вместо часового макроса. Более быстрая компиляция, так как анализируется очень мало файлов. Не беспокойтесь о конфликтах имен макросов. Вы можете обернуть включаемые проверки проверкой в макросе, чтобы файл заголовка не загружался снова. Обычно это используется в заголовках библиотек, которые включают несколько заголовков много раз. Может значительно ускорить компиляцию за счет некрасивого кода:
#ifndef __LIST_H_
#include "list.h"
#endif
В какой момент при анализе header2.h препроцессор пропускает этот файл?
Как говорит @Sean, header2.h
никогда не будет пропущен, но содержание между ifndef ... endif
будет проигнорировано в этом случае.
Что я могу добавить к примеру выше, чтобы продемонстрировать, как это работает?
Добавить что-то (например, #define B 123
) после #endif
в header2.h
, Теперь попробуйте получить доступ к нему в main
, Это будет доступно.
Теперь попробуйте добавить его до #endif
, Вы увидите, что он недоступен в `main.
Препроцессор начинает блокировать весь ввод, который следует за ложным #if[[n]def]
перейти к последующим шагам компилятора.
Однако препроцессор продолжает читать входные данные, чтобы отслеживать глубину вложения всех этих условных компиляций. #
-directives.
Когда он находит соответствующий #endif
, где он начал блокировать ввод, он просто прекращает блокировку.
Препроцессор никогда не пропустит header2.h. Он всегда будет включать его, а при расширении будет игнорировать материал в #ifndef
блок.
В вашем примере A
будет 32, как #define
в herader2.h никогда не будет достигнуто. Если бы это было достигнуто, вы получите какую-то "ошибку переопределения макроса", так как у вас было бы несколько #define
с "А". Чтобы это исправить, вам нужно #undef
A.
Большинство компиляторов поддерживают #pragma once
Директива в эти дни, чтобы избавить вас от необходимости писать включить охранники в заголовочных файлах.
Если я правильно понимаю, это означает, что любой заголовочный файл читается только один раз, даже если он включен несколько раз для данного процесса компиляции. И поэтому дополнительные элементы защиты в коде приложения или заголовочном файле не дают никакой выгоды.
Ни один компилятор gcc не выполняет эту оптимизацию только для файлов, которые, как он знает, безопасны в соответствии с правилами:
- За пределами управляющей пары #if-#endif не должно быть никаких токенов, но разрешены пробелы и комментарии.
- За пределами пары управляющих директив не должно быть никаких директив, но допустима нулевая директива (строка, не содержащая ничего, кроме одного символа "#" и, возможно, пробела).
Директива открытия должна иметь форму
#ifndef FOO
или же
#if !defined FOO [equivalently, #if !defined(FOO)]