【问题标题】:Explain how specific C #define works解释具体的 C#define 是如何工作的
【发布时间】:2020-01-23 06:02:08
【问题描述】:

我一直在查看http://www.netlib.org/fdlibm/ 的一些代码以了解某些功能是如何工作的,并且我正在查看 e_log.c 的代码以及它的某些部分代码:

hx = __HI(x);       /* high word of x */
lx = __LO(x);       /* low  word of x */

__HI(x)__LO(x) 的代码是:

#define __HI(x) *(1+(int*)&x)
#define __LO(x) *(int*)&x

我真的不明白,因为我不熟悉这种类型的 C。有人可以向我解释一下 __HI(x)__LO(x) 在做什么吗?

在函数的代码后面还有一条语句:

__HI(x) = hx|(i^0x3ff00000);

谁能给我解释一下:

  • 如何使函数等于某物(我通常使用 python,所以我真的不知道发生了什么)?
  • __HI(x)__LO(x) 在做什么?
  • 程序中x的“高位字”和“低位字”是什么意思?

我分析的最终目的是理解这段代码,以便将其移植到 Python 实现中

【问题讨论】:

  • C 中的 #define name(args) 实际上并不是一个函数,尽管在大多数情况下你可以像使用它一样使用它。 # 语句在编译时被预处理器读取,#define 正在创建一个宏 en.wikipedia.org/wiki/…
  • @RobertoCaboni 我有 64 位...
  • 使用保留标识符会产生未定义的行为。以双下划线开头的标识符(如您的 __HI__LOW)是保留的,因此使用它们会产生未定义的行为。
  • 第二个原因是未定义的行为(除了使用@Peter 指出的保留标识符):它违反了严格的别名。这也是 horrible 代码,这取决于 int 是特定大小(我猜是 32 位),并且架构是 little-endian。 Brrrr。
  • @Peter:这种行为不是“未定义的”,因为有问题的代码是 fdlibm 的一部分,它被设计为 C 实现的一部分,而不是普通 C 程序使用的源代码。因此,它不包含在 C 标准的规则中。 应该使用保留标识符,因为 C 标准保留标识符的部分目的是保留它们以供 C 实现使用,以便实现中使用的名称不会与使用的名称冲突在主题源代码中。

标签: c algorithm function


【解决方案1】:

这些宏使用与编译器相关的属性来访问double 类型的表示。

在 C 中,除了位域之外的所有对象都表示为字节序列。您正在查看的 fdlibm 代码专为以下实现而设计:int 是四个字节,double 类型使用 IEEE-754 浮点规范定义的格式中的八个字节表示。该格式称为 binary64 或 IEEE-754 基本 64 位二进制浮点。它还设计用于 C 编译器保证支持通过指针转换的别名的实现。 (C 标准不保证这一点,但 C 实现可能支持它。)

考虑一个名为xdouble 对象。鉴于这些宏:

#define __HI(x) *(1+(int*)&x)
#define __LO(x) *(int*)&x

在源代码中使用__LO(x) 时,将其替换为*(int*)&x&x 采用x 的地址。 x 的地址类型为 double *。演员(int *) 将其转换为int *,一个指向int 的指针。然后* 取消引用这个指针,导致对int 的引用,它位于x 的低地址部分。

在源代码中使用__HI(x) 时,(int*)&x 再次指向x 的低地址部分。添加 1 会将其更改为指向高地址部分。然后* 取消对它的引用,从而导致对位于高地址部分的int 的引用。

fdlibm 中的例程是特殊的数学例程。要进行操作,他们需要检查和修改代表double 值的字节。 __LO__HI 宏为他们提供了这种访问权限。

__HI__LO 的这些定义适用于以小端顺序存储 double 值的实现(双精度的“最低有效”部分位于较低地址的内存位置)。 fdlibm 代码可能包含大端系统的替代定义,可能由某些#if 语句选择。

在代码__HI(x) = hx|(i^0x3ff00000); 中,值0x3ff00000 是位掩码,用于对double 值的指数(和有效数字的一部分)进行编码。没有上下文,我们无法准确地说出这里发生了什么,但代码似乎正在将hx 与来自i 的一些值合并。它可能正在完成对表示它正在创建的新 double 值的字节的一些计算,并将这些字节存储在 double 对象的“高”部分中。

【讨论】:

    【解决方案2】:

    我添加回复以整合已经存在的回复(而不是替代)。

    hx = __HI(x);       /* high word of x */
    lx = __LO(x);       /* low  word of x */
    

    注释很有用...即使在这种情况下宏名称可能足够清晰。 “high”和“low”指的是整数表示的两半,通常是 16 位或 32 位,因为对于 8 位 int,使用的术语是“nibble”。

    如果我们取一个 16 位无符号整数,范围从 0 到 65535,或十六进制 0x0000 到 0xFFFF,例如 0x1234,则两半是:

      0x1234
          ^^-------------------- lower half, or "low"
        ^^---------------------- upper half, or "high"
    

    请注意,“较低”表示不太重要的部分。假设 16 位,获得两半的正确方法是与 0xFF 进行逻辑(按位)与得到 lo(),然后右移 8 位(除以 256)以获得高电平。

    现在,在 CPU 内部,数字 0x1234 被写入两个连续的位置,如果是大端,则为 0x12,然后是 0x34,或者如果是小端,则为 0x34,然后是 0x12。鉴于此,其他方法可以读取单半部分,直接从内存中读取正确的部分,无需计算。要在小端机器中获取 0x1234 的 lo(),可以读取第一个位置的单个字节。

    来自问题:

    #define __HI(x) *(1+(int*)&x)
    #define __LO(x) *(int*)&x
    

    __LO 被定义为按位与(确定方式),而 __HI 直接在内存中窥视(不太确定)。这很奇怪,因为似乎要分成两部分的整数具有机器字大小的两倍。如果机器是 32 位,则要拆分的整数是 64 位长。还有一个警告:这些宏可以读取两半,但也可以用来分别写入两半。其实从问题来看:

    __HI(x) = hx|(i^0x3ff00000);
    

    结果是只设置 x 的 HI 部分(高位,最重要)。还要注意使用的值 0x3FFF0000,这似乎表明 x 是 128 位,因为用于生成它的 一半 的掩码是 64 位长。

    希望这足够清楚,可以将 C 转换为 python。您应该使用 128 位长的整数。当需要获取 LO() 部分时,使用按位与 0xFFFFFFFF;要获得 HI(),请右移 64 次或进行等效除法。

    当 HI 和 LO 位于赋值的左侧时,只写入一半的值,您应该分别构造两半并将它们相加(或按位或一起) .

    希望对你有帮助...

    【讨论】:

    • 谢谢,这确实有很大帮助。只有 1 个问题,因为 python3 整数是 32 位的,我该怎么做才能得到它们的高低。
    • 对于 LSB 我会做 x&0xFF 和对于 GSB 我会做 x >> 8 其中 x = 任何 32 位整数?
    • 这里的整数是 32 位宽,它们寻址 double(64 位)的两半。您看到的掩码也是 32 位宽。此外,__LO 宏中的 & 是地址,而不是 AND。
    • @J.Doe 如果你的机器是 32 位的,你就不会得到你好和你。您将 hi 和 lo 保留为两个独立的变量,并在需要时将它们并排放置。怎么做?我不知道。也许某些库(python 有很多)能够声明 128 位整数。或者可能是浮点数,我看到一个答案告诉我这些数字是浮点数,而不是我想的整数。
    【解决方案3】:

    #define A B

    是一个预处理器指令,它在编译之前将整个源代码中的文字 A 替换为文字 B。

    #define A(x) B

    是一个类似函数的预处理器宏,它使用参数x 来进行参数化预处理器替换。在这种情况下,B 也可以是 x 的函数。

    您的宏

    #define __HI(x) *(1+(int*)&x)
    #define __LO(x) *(int*)&x
    
    // called as
    __HI(x) = hx|(i^0x3ff00000);
    

    由于只是代码替换的问题,因此分配是完全合法的。为什么?因为在这种情况下,宏在两种情况下都被 R-value 替换。

    在这两种情况下,右值都是int 类型的变量:

    • 获取 x 的地址
    • 将其转换为指向 int 的指针
    • 尊重它(在__LO()的情况下)
    • __HI ()的情况下加1,然后顺从它。

    它实际指向的内容取决于架构,因为指针算法依赖于架构。还必须考虑字节序。

    我们可以说的是,它们的设计目的是为了访问 lowerhigher 两半 大小为 2*sizeof (int) big 的数据类型(因此,例如,如果整数数据是 32 位宽,它们将允许访问低 32 字节和高 32 字节)。此外,从宏名上我们了解到它是一个little-endian架构(LSB在前)。


    为了移植到包含此宏的 Python 代码,您需要在更高级别进行,因为 Python 不支持指针。

    这些提示并不能解决您的具体任务,而是为您提供了一个工作方法来完成这个任务和类似的:

    • 了解宏功能的一种方法是检查它是如何被预处理器实际翻译的。这可以通过-E 编译器选项在大多数编译器上完成。
    • 使用调试器了解功能:在调用宏之前设置断点,并分析其对地址和变量的影响。

    【讨论】:

    • 那么是否有可能以某种方式将其转换为python?
    • @J.Doe 当然,不知何故这是可能的。你需要了解它做了什么,然后你知道该做什么,假设你了解Python。看起来,手头的源代码仅适用于小端架构,并且它正在访问具有更广泛价值的高低(32 位?)两半。你可能更喜欢在 Python 中使用二进制操作,例如为此编写一些真正的函数。
    • @thebusybee:源实际上根据字节序(fdmlib.h)对__HI__LO 进行了不同的定义。无论如何,整个事情都是基于对原始浮点表示的了解。
    • @Hasturkun 因此,OP 不仅需要了解宏为什么以及如何工作,还需要了解在更高级别上会发生什么。但是,现在应该有足够的提示开始。 ;-)
    • @thebusybee 我同意 C-Pyhton 移植只能在更高级别完成的事实,但在这种情况下,需要缩小整个 OP 的功能。但我也认为这是另一个问题(我不会回答,因为我对 python 不太熟练)
    猜你喜欢
    • 1970-01-01
    • 2015-04-10
    • 2017-08-08
    • 2021-10-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-10-07
    • 1970-01-01
    相关资源
    最近更新 更多