在就#pragma once 和#ifndef 守卫之间假定的性能权衡与正确与否的论点进行了扩展讨论之后(基于最近的一些灌输,我站在#pragma once 一边),我决定最终测试#pragma once 更快的理论,因为编译器不必尝试重新#include 已包含的文件。
为了测试,我自动生成了 500 个具有复杂相互依赖关系的头文件,并有一个 .c 文件,其中 #includes 全部。我以三种方式运行测试,一次只使用#ifndef,一次使用#pragma once,一次使用两者。我在一个相当现代的系统上进行了测试(运行 OSX 的 2014 MacBook Pro,使用 XCode 捆绑的 Clang,带有内部 SSD)。
一、测试代码:
#include <stdio.h>
//#define IFNDEF_GUARD
//#define PRAGMA_ONCE
int main(void)
{
int i, j;
FILE* fp;
for (i = 0; i < 500; i++) {
char fname[100];
snprintf(fname, 100, "include%d.h", i);
fp = fopen(fname, "w");
#ifdef IFNDEF_GUARD
fprintf(fp, "#ifndef _INCLUDE%d_H\n#define _INCLUDE%d_H\n", i, i);
#endif
#ifdef PRAGMA_ONCE
fprintf(fp, "#pragma once\n");
#endif
for (j = 0; j < i; j++) {
fprintf(fp, "#include \"include%d.h\"\n", j);
}
fprintf(fp, "int foo%d(void) { return %d; }\n", i, i);
#ifdef IFNDEF_GUARD
fprintf(fp, "#endif\n");
#endif
fclose(fp);
}
fp = fopen("main.c", "w");
for (int i = 0; i < 100; i++) {
fprintf(fp, "#include \"include%d.h\"\n", i);
}
fprintf(fp, "int main(void){int n;");
for (int i = 0; i < 100; i++) {
fprintf(fp, "n += foo%d();\n", i);
}
fprintf(fp, "return n;}");
fclose(fp);
return 0;
}
现在,我的各种测试运行:
folio[~/Desktop/pragma] fluffy$ gcc pragma.c -DIFNDEF_GUARD
folio[~/Desktop/pragma] fluffy$ ./a.out
folio[~/Desktop/pragma] fluffy$ time gcc -E main.c > /dev/null
real 0m0.164s
user 0m0.105s
sys 0m0.041s
folio[~/Desktop/pragma] fluffy$ time gcc -E main.c > /dev/null
real 0m0.140s
user 0m0.097s
sys 0m0.018s
folio[~/Desktop/pragma] fluffy$ time gcc -E main.c > /dev/null
real 0m0.193s
user 0m0.143s
sys 0m0.024s
folio[~/Desktop/pragma] fluffy$ gcc pragma.c -DPRAGMA_ONCE
folio[~/Desktop/pragma] fluffy$ ./a.out
folio[~/Desktop/pragma] fluffy$ time gcc -E main.c > /dev/null
real 0m0.153s
user 0m0.101s
sys 0m0.031s
folio[~/Desktop/pragma] fluffy$ time gcc -E main.c > /dev/null
real 0m0.170s
user 0m0.109s
sys 0m0.033s
folio[~/Desktop/pragma] fluffy$ time gcc -E main.c > /dev/null
real 0m0.155s
user 0m0.105s
sys 0m0.027s
folio[~/Desktop/pragma] fluffy$ gcc pragma.c -DPRAGMA_ONCE -DIFNDEF_GUARD
folio[~/Desktop/pragma] fluffy$ ./a.out
folio[~/Desktop/pragma] fluffy$ time gcc -E main.c > /dev/null
real 0m0.153s
user 0m0.101s
sys 0m0.027s
folio[~/Desktop/pragma] fluffy$ time gcc -E main.c > /dev/null
real 0m0.181s
user 0m0.133s
sys 0m0.020s
folio[~/Desktop/pragma] fluffy$ time gcc -E main.c > /dev/null
real 0m0.167s
user 0m0.119s
sys 0m0.021s
folio[~/Desktop/pragma] fluffy$ gcc --version
Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.12.sdk/usr/include/c++/4.2.1
Apple LLVM version 8.1.0 (clang-802.0.42)
Target: x86_64-apple-darwin17.0.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin
如您所见,带有#pragma once 的版本的预处理速度确实比#ifndef 稍快——只有一个,但是差异可以忽略不计,并且会被实际构建和链接代码所需的时间。也许如果代码库足够大,它实际上可能会导致构建时间的差异只有几秒钟,但是现代编译器能够优化#ifndef 守卫,操作系统具有良好的磁盘缓存这一事实,以及存储技术速度的提高,似乎性能论点是没有实际意义的,至少在当今时代的典型开发人员系统上是这样。较旧且更具异国情调的构建环境(例如,托管在网络共享上的标头,从磁带构建等)可能会在一定程度上改变等式,但在这些情况下,首先简单地创建一个不那么脆弱的构建环境似乎更有用。
事实上,#ifndef 是标准化的标准行为,而#pragma once 不是,#ifndef 也处理奇怪的文件系统和搜索路径的极端情况,而#pragma once 可能会被某些事情搞糊涂,导致程序员无法控制的错误行为。 #ifndef 的主要问题是程序员为他们的守卫选择了不好的名字(名称冲突等),即使这样,API 的使用者也很有可能使用 #undef 覆盖那些不好的名字 - 这不是一个完美的解决方案,也许吧,但它可能,而如果编译器错误地剔除#include,#pragma once 就没有办法了。
因此,尽管 #pragma once 明显(略)快,但我不同意这本身就是使用它而不是 #ifndef 守卫的理由。
编辑:感谢@LightnessRacesInOrbit 的反馈,我增加了头文件的数量并将测试更改为仅运行预处理器步骤,从而消除了由编译和链接过程(以前很简单,现在不存在)。正如预期的那样,差异大致相同。