c - 在C中,如何将 float 或 double 除以2的i次方?

标签 c arm double exponent

我已经用 C 语言为 Atmel 的微 Controller 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 次幂,但我找不到任何可以让我显式读取、操作、并写回 double 的指数。执行所有这些步骤实际上可能并不比执行乘法更快,因此从指数中直接减去 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 一起使用,它们的运行时间完全相同。

最佳答案

如果您可以断言 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 调用转换为单个指令。

关于c - 在C中,如何将 float 或 double 除以2的i次方?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/63440960/

相关文章:

c - 结构函数中的初始值设定项无效

用于 Revel 框架的 Go 编程交叉编译

c++ - Nintendo DS 平铺图形中的奇怪条纹

java - 读取文件,将字符串转换为 double ,存储在二维数组中

c# - 如何将 double 值格式化为具有指定指数的 E 格式?

c - 写入文件,其中输出是制表符而不是空格

c - 算法 : Unimodal Streak

c - 使用链表将字符串插入堆栈

linux - 嵌入式 Linux 中的看门狗定时器 > 60 秒

c# - 为什么在 C# 中将 double 转换为 float 时我的精度会下降?