在 C 中,如何将浮点数或双精度数除以 2 的 i 次幂?

Posted

技术标签:

【中文标题】在 C 中,如何将浮点数或双精度数除以 2 的 i 次幂?【英文标题】:In C, how to divide a float or double number by 2 raised to the power i? 【发布时间】:2020-08-16 19:10:00 【问题描述】:

我用 C 语言为 Atmel 的微控制器 SAM E70 编写了代码,它处理 32 位宽的整数值。为了进一步计算,我将整数值标准化为 0...1.0,如下所示:

#define DIV4294967296 ((double) 1.0) / ((double) 4294967296.0)
.
.
double doubleValue;
doubleValue = ((double) intValue) * DIV4294967296;

我知道我可以从doubleValue 的指数中减去 32,从而避免更昂贵的乘法。我知道ldexp() 允许将指数乘以 2 的幂 i 但我找不到任何可以让我显式读取、操作和写回 a 的指数的东西双倍的。执行所有这些步骤实际上可能并不比执行乘法更快,因此从指数中直接减去 32 是理想的。这通常如何在 C 中完成?更重要的是,ARM 的 Cortex V7 指令集如何做到最好?

附录:回答 Eric 的问题,这是 Atmel Studio 7 向我展示的反汇编代码,用于使用 ldexpscalbn 以及与 0x1p-32 的乘法:

uint32_t intV = 123456;
 ldr    r3, [pc, #424]
 str    r3, [r7, #28]
double doubleV0 = ((double) intV) * DIV4096;
 ldr    r3, [r7, #36]        
 vmov   s15, r3      
 vcvt.f64.u32   d7, s15      
 vldr   d6, [pc, #272]       
 vmul.f64   d7, d7, d6       
 vstr   d7, [r7, #24]       
double doubleV1 = ldexp(intV, -32);
 ldr    r3, [r7, #28]
 vmov   s15, r3
 vcvt.f64.u32   d7, s15
 mvn    r0, #31
 vmov.f64   d0, d7 
 ldr    r3, [pc, #408]
 blx    r3
 vstr   d0, [r7, #16]
double doubleV2 = scalbn(intV, -32);
 ldr    r3, [r7, #28]
 vmov   s15, r3
 vcvt.f64.u32   d7, s15
 mvn    r0, #31 
 vmov.f64   d0, d7
 ldr    r3, [pc, #384]
 blx    r3
 vstr   d0, [r7, #8]
double doubleV3 = intV * 0x1p-32;
 ldr    r3, [r7, #28]
 vmov   s15, r3
 vcvt.f64.u32   d7, s15
 vldr   d6, [pc, #164]
 vmul.f64   d7, d7, d6
 vstr   d7, [r7]

看起来这些都不匹配任何 ARM 指令(就像 C 函数 fabs() 直接编译为汇编指令 vabs)。 ldexpscalbn 的编码方式相同。与0x1p-32 的乘法的编码方式与我最初提出的问题的乘法相同。

附录 2: 根据 chqrlie 的建议显示它编译成的代码:

double doubleV4 = ((double) intV);
 vstr   d7, [r7]    
*(uint64_t *)&doubleV4 -= 32ULL << 52;
 mov    r3, r7       
 ldrd   r2, r3, [r3]         
 mov    r1, r7       
 adds   r4, r2, #0       
 adc    r5, r3, #4261412864      
 strd   r4, r5, [r1]

在我看来这是最便宜的实现。

最终判决:我喜欢 chqrlie 的回答,因为它可能对我们当中乘法太慢的人有用。不过,在我的情况下,我运行了一个基于中断的例程并测量了我的初始代码和 chqrlie 的替代代码的执行时间,如果最佳优化 (-O3) 与 GCC 9.3.1 一起使用,它们的运行时间完全相同。

【问题讨论】:

您的编译器为ldexp(intValue, −32) 生成了什么?它为scalbn(intValue, −32) 生成了什么?还是intValue * 0x1p-32 问题是“如何划分floatdouble”但示例代码是uint32_t intV ... uint32_t intV。 dividend的真实类型是什么? 您是否尝试过将您的数字除以 1u 如果您可以断言字节顺序,您可以直接对 32 位半部分甚至包含指数的 16 位部分进行操作。这可能会生成更少的代码。 【参考方案1】:

如果您可以断言 double 是使用 IEEE 754 double-precision binary floating-point format: binary64 存储的,具有与 64 位整数相同的字节顺序和对齐要求,并且它的值足够大以使结果仍然是正常值,你可以直接用这个表达式破解表示,它应该编译成 2 或 3 条指令:

*(uint64_t *)&doubleValue -= 32ULL << 52;

然而,这种形式的类型双关语可能会给积极的优化器带来麻烦,因为它违反了 C 别名规则,因为类型 double 的值是通过指向不同类型的指针而不是字符指针来访问的。可以通过union 使用更好的类型双关语形式,它可以在大多数编译器中正常工作:

union  double d; uint64_t u;  u = doubleValue;
u.u -= 32ULL << 52;
doubleValue = u.d;

要完全避免 C 别名问题,您可以使用memcpy

uint64_t u;
memcpy(&u, &doubleValue, sizeof u);
u -= 32ULL << 52;
memcpy(&doubleValue, &u, sizeof u);

一个好的优化编译器应该将这些 memcpy 调用转换为单个指令。

【讨论】:

...如果double 对齐要求不超过uint64_t...(也许在ARM 中这不是问题。) 不过,基于类型的别名可能会咬你一口。最好正确地执行双关语。 你不能像这样访问错误类型的对象。您可以将其存储到联合中并在联合中进行操作,或者 memcpy 将其存储到 `uint64_t` 对象并返回 要使代码符合C的别名规则,可以使用double foo(int x) union double d; uint64_t u; t = x ; t.u -= (uint64_t) 32 &lt;&lt; 52; return t.d; @chux-ReinstateMonica:你的意思是如果uint64_t的对齐要求不超过double的对齐要求

以上是关于在 C 中,如何将浮点数或双精度数除以 2 的 i 次幂?的主要内容,如果未能解决你的问题,请参考以下文章

如何比较浮点数或双精度数? [复制]

可以采用整数或浮点数或双精度数或任何其他可转换为浮点数的 C++ 函数

如何将浮点数转换为长度为 4 的字节数组(char* 数组)?

为啥将浮点数除以整数返回 0.0?

如何在没有不必要的十进制 0 的情况下很好地将浮点数格式化为字符串

如何将浮点数保存为 2 个字节?