如何在 C 中解析链接的宏?
How are chained macros resolved in C?
如果我想使用预处理器 #define
语句来轻松定义和计算常量和常用函数,并利用较少的 RAM 开销(与使用 const
值相反)。但是,我不确定如果一起使用许多宏,它们是如何解决的。
我正在设计自己的 DateTime
代码处理,类似于 linux 时间戳,但适用于具有代表 1/60 秒的滴答更新的游戏。我更愿意声明链接的值,但想知道硬编码值是否会执行得更快。
#include <stdint.h>
// my time type, measured in 1/60 of a second.
typedef int64_t DateTime;
// radix for pulling out display values
#define TICKS_PER_SEC 60L
#define SEC_PER_MIN 60L
#define MIN_PER_HR 60L
#define HRS_PER_DAY 24L
#define DAYS_PER_WEEK 7L
#define WEEKS_PER_YEAR 52L
// defined using previous definitions (I like his style, write once!)
#define TICKS_PER_MIN TICKS_PER_SEC * SEC_PER_MIN
#define TICKS_PER_HR TICKS_PER_SEC * SEC_PER_MIN * MIN_PER_HR
#define TICKS_PER_DAY TICKS_PER_SEC * SEC_PER_MIN * MIN_PER_HR * HRS_PER_DAY
// ... so on, up to years
//hard coded conversion factors.
#define TICKS_PER_MIN_H 3600L // 60 seconds = 60^2 ticks
#define TICKS_PER_HR_H 216000L // 60 minutes = 60^3 ticks
#define TICKS_PER_DAY_H 5184000L // 24 hours = 60^3 * 24 ticks
// an example macro to get the number of the day of the week
#define sec(t)((t / TICKS_PER_DAY) % DAYS_PER_WEEK)
如果我使用 sec(t)
宏,它使用由 3 个先前的宏 TICKS_PER_SEC * SEC_PER_MIN * MIN_PER_HR * HRS_PER_DAY
定义的 TICKS_PER_DAY
,那么在我调用 sec(t)
的代码中是否到处都有这个地方:
(t / 5184000L) % 7L)
还是每次扩展到:
(t / (60L * 60L * 60L * 24L)) % 7L)
以便在每一步执行额外的乘法指令?这是宏和 const 变量之间的权衡,还是我误解了预处理器的工作原理?
更新:
根据许多有用的答案,扩展为常量表达式的链接宏的最佳设计是将定义包装在 括号 中用于
1.正确的操作顺序:
(t / 60 * 60 * 60 * 24) != (t / (60 * 60 * 60 * 24))
2。通过将常量值组合在一起来鼓励编译器进行常量折叠:
// note parentheses to prevent out-of-order operations
#define TICKS_PER_MIN (TICKS_PER_SEC * SEC_PER_MIN)
#define TICKS_PER_HR (TICKS_PER_SEC * SEC_PER_MIN * MIN_PER_HR)
#define TICKS_PER_DAY (TICKS_PER_SEC * SEC_PER_MIN * MIN_PER_HR * HRS_PER_DAY)
预处理器只进行文本替换。它将计算为第二个表达式与 "extra" 相乘。然而,编译器通常会尝试优化常量之间的算术运算,只要它可以在不更改答案的情况下这样做。
为了最大限度地优化它的机会,您需要注意保持常量 "next to each other" 以便它可以看到优化,尤其是对于浮点类型。换句话说,如果 t
是一个变量,你会喜欢 30 * 20 * t
而不是 30 * t * 20
.
参见 gcc preprocessor macro docs,特别是 类对象宏。
我认为编译器在这里也发挥了作用。例如,如果我们只考虑预处理器,那么它应该扩展为
(t / (60L * 60L * 60L * 24L)) % 7L)
但是,编译器(不管优化如何?)可能会将此解析为
(t / 5184000L) % 7L)
因为这些是独立的常量,因此将是 faster/simpler 代码执行。
注意 1:您应该在定义中使用“(t)”以防止意外 expansions/interpretations。
注 2:另一个最佳实践是避免使用 undef
,因为这会降低代码的可读性。请参阅有关宏扩展如何受此影响的注释(类对象宏部分)。
更新:来自 Object-like Macros:
部分
When the preprocessor expands a macro name, the macro's expansion replaces the macro invocation, then the expansion is examined for more macros to expand. For example,
#define TABLESIZE BUFSIZE
#define BUFSIZE 1024
TABLESIZE
==> BUFSIZE
==> 1024
TABLESIZE is expanded first to produce BUFSIZE, then that macro is expanded to produce the final result, 1024.
Notice that BUFSIZE was not defined when TABLESIZE was defined. The ‘#define’ for TABLESIZE uses exactly the expansion you specify—in this case, BUFSIZE—and does not check to see whether it too contains macro names. Only when you use TABLESIZE is the result of its expansion scanned for more macro names.
(强调我的)
宏扩展无非是简单的文本替换。展开宏后,编译器将解析结果并执行其通常的优化,其中应包括常量折叠。
但是,此示例说明了初学者在 C 中定义宏时常犯的错误。如果宏旨在扩展为表达式,则良好的 C 实践规定值应始终包含在括号中,否则结果将不正确包含暴露的运算符。在这个例子中,看看 TICKS_PER_DAY
:
的定义
#define TICKS_PER_DAY TICKS_PER_SEC * SEC_PER_MIN * MIN_PER_HR * HRS_PER_DAY
现在看sec
(注意分号不应该出现,但我暂时忽略它):
#define sec(t)((t / TICKS_PER_DAY) % DAYS_PER_WEEK);
如果实例化为 sec(x)
,它将扩展为:
((x / 60L * 60L * 60L * 24L) % 7L);
这显然不是我们想要的。它只会除以初始值 60L
,之后将乘以剩余值。
解决此问题的正确方法是修复 TICKS_PER_DAY
的定义以正确封装其内部操作:
#define TICKS_PER_DAY (TICKS_PER_SEC * SEC_PER_MIN * MIN_PER_HR * HRS_PER_DAY)
当然,sec
应该是一个表达式宏并且不应该包含分号,这会阻止它被使用,例如,在 sec(x) + 10
:[=24 这样的上下文中=]
#define sec(t) ((t / TICKS_PER_DAY) % DAYS_PER_WEEK)
现在让我们看看 sec(x)
将如何通过这些错误修复得到扩展:
((x / (60L * 60L * 60L * 24L)) % 7L)
现在这将真正完成预期的工作。编译器应该不断地折叠乘法,在一个除法后跟一个 mod.
编辑:原来缺少的括号似乎已添加到 post。没有他们,它根本行不通。此外,多余的分号已从原始 post.
中删除
扩展为:
(t / (60L * 60L * 60L * 24L)) % 7L)
这是因为宏是由预处理器处理的,它只是将宏扩展为它们的值(如有必要则递归)。
但这并不意味着整个计算将在您使用 sec(t) 的每个点重复。这是因为计算发生在编译时。所以你不用在运行时间付出代价。编译器预先计算这些常量计算,并在生成的代码中使用计算值。
如果我想使用预处理器 #define
语句来轻松定义和计算常量和常用函数,并利用较少的 RAM 开销(与使用 const
值相反)。但是,我不确定如果一起使用许多宏,它们是如何解决的。
我正在设计自己的 DateTime
代码处理,类似于 linux 时间戳,但适用于具有代表 1/60 秒的滴答更新的游戏。我更愿意声明链接的值,但想知道硬编码值是否会执行得更快。
#include <stdint.h>
// my time type, measured in 1/60 of a second.
typedef int64_t DateTime;
// radix for pulling out display values
#define TICKS_PER_SEC 60L
#define SEC_PER_MIN 60L
#define MIN_PER_HR 60L
#define HRS_PER_DAY 24L
#define DAYS_PER_WEEK 7L
#define WEEKS_PER_YEAR 52L
// defined using previous definitions (I like his style, write once!)
#define TICKS_PER_MIN TICKS_PER_SEC * SEC_PER_MIN
#define TICKS_PER_HR TICKS_PER_SEC * SEC_PER_MIN * MIN_PER_HR
#define TICKS_PER_DAY TICKS_PER_SEC * SEC_PER_MIN * MIN_PER_HR * HRS_PER_DAY
// ... so on, up to years
//hard coded conversion factors.
#define TICKS_PER_MIN_H 3600L // 60 seconds = 60^2 ticks
#define TICKS_PER_HR_H 216000L // 60 minutes = 60^3 ticks
#define TICKS_PER_DAY_H 5184000L // 24 hours = 60^3 * 24 ticks
// an example macro to get the number of the day of the week
#define sec(t)((t / TICKS_PER_DAY) % DAYS_PER_WEEK)
如果我使用 sec(t)
宏,它使用由 3 个先前的宏 TICKS_PER_SEC * SEC_PER_MIN * MIN_PER_HR * HRS_PER_DAY
定义的 TICKS_PER_DAY
,那么在我调用 sec(t)
的代码中是否到处都有这个地方:
(t / 5184000L) % 7L)
还是每次扩展到:
(t / (60L * 60L * 60L * 24L)) % 7L)
以便在每一步执行额外的乘法指令?这是宏和 const 变量之间的权衡,还是我误解了预处理器的工作原理?
更新:
根据许多有用的答案,扩展为常量表达式的链接宏的最佳设计是将定义包装在 括号 中用于
1.正确的操作顺序:
(t / 60 * 60 * 60 * 24) != (t / (60 * 60 * 60 * 24))
2。通过将常量值组合在一起来鼓励编译器进行常量折叠:
// note parentheses to prevent out-of-order operations
#define TICKS_PER_MIN (TICKS_PER_SEC * SEC_PER_MIN)
#define TICKS_PER_HR (TICKS_PER_SEC * SEC_PER_MIN * MIN_PER_HR)
#define TICKS_PER_DAY (TICKS_PER_SEC * SEC_PER_MIN * MIN_PER_HR * HRS_PER_DAY)
预处理器只进行文本替换。它将计算为第二个表达式与 "extra" 相乘。然而,编译器通常会尝试优化常量之间的算术运算,只要它可以在不更改答案的情况下这样做。
为了最大限度地优化它的机会,您需要注意保持常量 "next to each other" 以便它可以看到优化,尤其是对于浮点类型。换句话说,如果 t
是一个变量,你会喜欢 30 * 20 * t
而不是 30 * t * 20
.
参见 gcc preprocessor macro docs,特别是 类对象宏。
我认为编译器在这里也发挥了作用。例如,如果我们只考虑预处理器,那么它应该扩展为
(t / (60L * 60L * 60L * 24L)) % 7L)
但是,编译器(不管优化如何?)可能会将此解析为
(t / 5184000L) % 7L)
因为这些是独立的常量,因此将是 faster/simpler 代码执行。
注意 1:您应该在定义中使用“(t)”以防止意外 expansions/interpretations。
注 2:另一个最佳实践是避免使用 undef
,因为这会降低代码的可读性。请参阅有关宏扩展如何受此影响的注释(类对象宏部分)。
更新:来自 Object-like Macros:
部分When the preprocessor expands a macro name, the macro's expansion replaces the macro invocation, then the expansion is examined for more macros to expand. For example,
#define TABLESIZE BUFSIZE #define BUFSIZE 1024 TABLESIZE ==> BUFSIZE ==> 1024
TABLESIZE is expanded first to produce BUFSIZE, then that macro is expanded to produce the final result, 1024.Notice that BUFSIZE was not defined when TABLESIZE was defined. The ‘#define’ for TABLESIZE uses exactly the expansion you specify—in this case, BUFSIZE—and does not check to see whether it too contains macro names. Only when you use TABLESIZE is the result of its expansion scanned for more macro names.
(强调我的)
宏扩展无非是简单的文本替换。展开宏后,编译器将解析结果并执行其通常的优化,其中应包括常量折叠。
但是,此示例说明了初学者在 C 中定义宏时常犯的错误。如果宏旨在扩展为表达式,则良好的 C 实践规定值应始终包含在括号中,否则结果将不正确包含暴露的运算符。在这个例子中,看看 TICKS_PER_DAY
:
#define TICKS_PER_DAY TICKS_PER_SEC * SEC_PER_MIN * MIN_PER_HR * HRS_PER_DAY
现在看sec
(注意分号不应该出现,但我暂时忽略它):
#define sec(t)((t / TICKS_PER_DAY) % DAYS_PER_WEEK);
如果实例化为 sec(x)
,它将扩展为:
((x / 60L * 60L * 60L * 24L) % 7L);
这显然不是我们想要的。它只会除以初始值 60L
,之后将乘以剩余值。
解决此问题的正确方法是修复 TICKS_PER_DAY
的定义以正确封装其内部操作:
#define TICKS_PER_DAY (TICKS_PER_SEC * SEC_PER_MIN * MIN_PER_HR * HRS_PER_DAY)
当然,sec
应该是一个表达式宏并且不应该包含分号,这会阻止它被使用,例如,在 sec(x) + 10
:[=24 这样的上下文中=]
#define sec(t) ((t / TICKS_PER_DAY) % DAYS_PER_WEEK)
现在让我们看看 sec(x)
将如何通过这些错误修复得到扩展:
((x / (60L * 60L * 60L * 24L)) % 7L)
现在这将真正完成预期的工作。编译器应该不断地折叠乘法,在一个除法后跟一个 mod.
编辑:原来缺少的括号似乎已添加到 post。没有他们,它根本行不通。此外,多余的分号已从原始 post.
中删除扩展为:
(t / (60L * 60L * 60L * 24L)) % 7L)
这是因为宏是由预处理器处理的,它只是将宏扩展为它们的值(如有必要则递归)。
但这并不意味着整个计算将在您使用 sec(t) 的每个点重复。这是因为计算发生在编译时。所以你不用在运行时间付出代价。编译器预先计算这些常量计算,并在生成的代码中使用计算值。