如果我想使用预处理器#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_DAY
定义的TICKS_PER_SEC * SEC_PER_MIN * MIN_PER_HR * HRS_PER_DAY
,则会在我调用sec(t)
的代码中的任何位置执行此操作:< / p>
(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)
答案 0 :(得分:2)
预处理器只进行文本替换。它将使用“额外”乘法来评估第二个表达式。编译器通常会尝试优化常量之间的算术,但是,只要它可以这样做而不改变答案。
为了最大化优化的机会,您需要注意保持常量“彼此相邻”,以便它可以看到优化,尤其是浮点类型。换句话说,如果t
是变量,您希望30 * 20 * t
代替30 * t * 20
。
答案 1 :(得分:1)
请参阅gcc preprocessor macro docs,特别是类似对象的宏。
我认为编译器也在这里发挥作用。例如,如果我们只考虑预处理器,那么它应该扩展到
(t / (60L * 60L * 60L * 24L)) % 7L)
但是,编译器(无论优化?)可能会将其解析为
(t / 5184000L) % 7L)
因为它们是独立的常量,因此代码执行速度更快/更简单。
注意1:您应该在定义中使用“(t)”来防止意外的扩展/解释。
注意2:另一个最佳做法是避免使用undef
,因为这会降低代码的可读性。请参阅有关宏扩展如何受此影响的说明(类似对象的宏部分)。
更新:来自Object-like Macros部分:
当预处理器扩展宏名称时,宏的扩展将替换宏调用,然后检查扩展以查找更多要扩展的宏。例如,
#define TABLESIZE BUFSIZE #define BUFSIZE 1024 TABLESIZE ==> BUFSIZE ==> 1024
首先扩展TABLESIZE以生成BUFSIZE,然后扩展该宏以生成最终结果1024。请注意,在定义TABLESIZE时未定义BUFSIZE。 TABLESIZE的'#define'完全使用您指定的扩展 - 在本例中为BUFSIZE - 并且不检查它是否也包含宏名称。 仅当您使用TABLESIZE时,才会扫描其扩展名以获取更多宏名称。
(强调我的)
答案 2 :(得分:1)
宏扩展只不过是简单的文本替换。在宏扩展之后,编译器将解析结果并执行其通常的优化,其中应包括常量折叠。
但是,这个例子说明了初学者在用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
这样的上下文中:
#define sec(t) ((t / TICKS_PER_DAY) % DAYS_PER_WEEK)
现在让我们看看如何通过这些错误修复来扩展sec(x)
:
((x / (60L * 60L * 60L * 24L)) % 7L)
现在这实际上会按预期进行。编译器应该对乘法进行常数折叠,在单个除法中进行弹性,然后是单个mod。
编辑:看起来丢失的括号已添加到原始帖子中。如果没有它们,根本就行不通。另外的分号也已从原帖中删除。
答案 3 :(得分:1)
它扩展为:
(t / (60L * 60L * 60L * 24L)) % 7L)
这是因为宏由预处理器处理,它只是将宏扩展为它们的值(必要时递归)。
但这并不意味着在你使用sec(t)的每个点重复整个计算。这是因为计算在编译时发生。所以你不要在运行时付出代价。编译器预先计算这种常量计算,并在生成的代码中使用计算值。