【问题标题】:How are chained macros resolved in C?在 C 中如何解决链式宏?
【发布时间】:2016-03-21 17:41:06
【问题描述】:

如果我想使用预处理器 #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)

【问题讨论】:

  • 预处理器不会优化它,但是任何值得它的盐的编译器都会把它折叠成一个单一的常量值。
  • 所以如果我使用诸如 gcc 之类的,我应该期望它被视为单个值?有没有办法检查它的作用?
  • 这些定义不是嵌套的;它们是链式扩展:使用宏的扩展,其扩展使用更多宏。如果存在嵌套,则意味着宏的主体可以定义宏。
  • 您应该从sec 的定义中删除分号,以便它扩展为一个表达式。否则sec(x) + 10 之类的东西将不起作用。

标签: c macros constants c-preprocessor


【解决方案1】:

它扩展为:

(t / (60L * 60L * 60L * 24L)) % 7L)

这是因为宏由预处理器处理,预处理器只是将宏扩展为它们的值(必要时递归)。

但这并不意味着整个计算将在您使用 sec(t) 的每个点重复。这是因为计算发生在编译时。因此,您无需在运行时付出代价。编译器预先计算此类常量计算,并在生成的代码中使用计算值。

【讨论】:

    【解决方案2】:

    宏扩展只不过是简单的文本替换。宏展开后,编译器将解析结果并执行其通常的优化,其中应包括常量折叠。

    但是,此示例说明了初学者在 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)
    

    现在这实际上会达到预期的效果。编译器应该对乘法进行常量折叠,重新划分为一个除法,然后是一个模数。

    编辑:看起来缺少的括号已被添加到原始帖子中。没有他们,它根本行不通。此外,原帖中多余的分号已被删除。

    【讨论】:

    • 其他人提到了这一点,我更新了我的代码。或许我应该留下最初的错误并在原文下方更新我的帖子,以便其他人可以看到思路?
    • 我不确定最好的办法是什么。也许在底部添加一个“编辑”注释,提及所做的更改?
    • 无论如何,我知道括号是任何链式常量宏正常运行的关键部分。谢谢你的解释。
    【解决方案3】:

    参见gcc preprocessor macro docs,特别是类似对象的宏

    我认为编译器也在这里发挥作用。例如,如果我们只考虑预处理器,那么它应该扩展为

    (t / (60L * 60L * 60L * 24L)) % 7L)

    但是,编译器(不管优化如何?)可能会解决这个问题

    (t / 5184000L) % 7L)

    因为这些是独立的常量,因此代码执行会更快/更简单。

    注意1:您应该在定义中使用“(t)”来防止意外的扩展/解释。 注意2:另一个最佳实践是避免使用undef,因为这会降低代码的可读性。请参阅有关宏扩展如何受此影响的说明(Object-like Macros 部分)。

    更新:来自Object-like Macros部分:

    当预处理器扩展宏名称时,宏的扩展替换宏调用,然后检查扩展以查找更多要扩展的宏。例如,

    #define TABLESIZE BUFSIZE #define BUFSIZE 1024 TABLESIZE ==> BUFSIZE ==> 1024 首先扩展 TABLESIZE 以生成 BUFSIZE,然后扩展该宏以生成最终结果 1024。

    请注意,在定义 TABLESIZE 时未定义 BUFSIZE。 TABLESIZE 的“#define”完全使用您指定的扩展——在本例中为 BUFSIZE——并且不检查它是否也包含宏名称。 只有当您使用 TABLESIZE 时,它的扩展结果才会扫描更多宏名称。

    (强调我的)

    【讨论】:

    • 这正是我一直在寻找但找不到的那种文档!谢谢,我会仔细阅读以获取更多说明。
    • 很高兴为您提供帮助!我还发布了一个更新,提到了您可能感兴趣的关键部分。
    • 您的回答似乎最能解决我的具体问题,而且启动时我不知道定义可以定义为“乱序”,可以这么说,但仍然可以正确解决。这让我对使用链式宏使代码可读、更容易维护和适当高效的想法充满信心。
    【解决方案4】:

    预处理器只是进行文本替换。它将评估为带有“额外”乘法的第二个表达式。但是,编译器通常会尝试优化常量之间的算术运算,只要它可以在不改变答案的情况下这样做。

    为了最大限度地提高优化的机会,您需要注意保持常量“彼此相邻”,以便它可以看到优化,尤其是浮点类型。换句话说,如果t 是一个变量,你会喜欢30 * 20 * t 而不是30 * t * 20

    【讨论】:

    • 那么使用我的例子,这是否意味着在大多数情况下,TICKS_PER_DAY 会被编译器从60L * 60L * 60L * 24L 优化到5184000L? IE。它可能会像您演示的那样将常量值折叠在一起吗?
    • 差不多。您应该在宏定义中的整个表达式周围加上括号,使其为(60L * 60L * 60L *24L)。由于乘法和除法运算的操作顺序,您拥有t / TICKS_PER_DAY 的方式不会给出您期望的答案。
    猜你喜欢
    • 1970-01-01
    • 2021-05-19
    • 1970-01-01
    • 2023-03-19
    • 1970-01-01
    • 2023-04-07
    • 1970-01-01
    • 1970-01-01
    • 2021-07-20
    相关资源
    最近更新 更多