为何long double占用更多内存却未比double提供更高精度?
我编写了一个测试程序,用来验证C语言中各类浮点类型能无精度损失存储的最大整数:
#include <stdio.h> #include <stdlib.h> #include <float.h> #define FLOATTYPE long double #define ONE ((FLOATTYPE)1.0) #define TWO ((FLOATTYPE)2.0) int main(int argc,char*argv[]){ int i; FLOATTYPE x; x = ONE; for(i=0;;++i){ printf("1.0<<%3d: x=%.0Lf",i,(long double)x); if((x+ONE)!=x && (x+ONE)- x == ONE){ printf(" ... can increment without loss of precision\n"); }else{ printf(" ... cannot increment without loss of precision\n"); break; } x *= TWO; } printf("FLT_RADIX = %d\n",FLT_RADIX); printf("FLT_MANT_DIG = %d\n",FLT_MANT_DIG); printf("DBL_MANT_DIG = %d\n",DBL_MANT_DIG); printf("LDBL_MANT_DIG = %d\n",LDBL_MANT_DIG); printf("\nsizeof(FLOATTYPE) = %lu\n",sizeof(x)); }
测试结果与异常现象
用gcc-9 (Ubuntu 9.4.0-1ubuntu1~16.04)测试后,以下结果符合预期:
- 当
FLOATTYPE为float时:sizeof为4,循环在i==24时退出,与FLT_MANT_DIG值一致。 - 当
FLOATTYPE为double时:sizeof为8,循环在i==53时退出,与DBL_MANT_DIG值一致。 - 当
FLOATTYPE为__float128时:sizeof为16,循环在i==113时退出。
但当FLOATTYPE为long double时出现异常:sizeof为16,循环却在i==53时退出,和LDBL_MANT_DIG(值为64)不符——明明占用了更多内存,却没提供更高精度。
补充测试信息
测试环境是Windows 10 Pro下的WSL1 Ubuntu 16.04,编译命令为gcc-9 test_precision0100.c -o test_precision0100_longdouble.exe -DFLOATTYPE="long double",用gcc-9和内置gcc v5.4.0都得到这个异常结果,但WSL2 Ubuntu 18.04下结果正常。
测试输出末尾内容:
1.0<< 50: x=1125899906842624 ... can increment without loss of precision 1.0<< 51: x=2251799813685248 ... can increment without loss of precision 1.0<< 52: x=4503599627370496 ... can increment without loss of precision 1.0<< 53: x=9007199254740992 ... cannot increment without loss of precision FLT_RADIX = 2 FLT_MANT_DIG = 24 DBL_MANT_DIG = 53 LDBL_MANT_DIG = 64 sizeof(FLOATTYPE) = 16
对应的汇编代码:
.file "test_precision0100.c" .text .section .rodata .LC1: .string "1.0<<%3d: x=%.0Lf" .align 8 .LC2: .string " ... can increment without loss of precision" .align 8 .LC3: .string " ... cannot increment without loss of precision" .LC4: .string "FLT_RADIX = %d\n" .LC5: .string "FLT_MANT_DIG = %d\n" .LC6: .string "DBL_MANT_DIG = %d\n" .LC7: .string "LDBL_MANT_DIG = %d\n" .LC8: .string "\nsizeof(FLOATTYPE) = %lu\n" .text .globl main .type main, @function main: .LFB2: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 subq $48, %rsp movl %edi, -36(%rbp) movq %rsi, -48(%rbp) fld1 fstpt -16(%rbp) movl $0, -20(%rbp) .L5: movl -20(%rbp), %eax pushq -8(%rbp) pushq -16(%rbp) movl %eax, %esi movl $.LC1, %edi movl $0, %eax call printf addq $16, %rsp fldt -16(%rbp) fld1 faddp %st, %st(1) fldt -16(%rbp) fucomip %st(1), %st jp .L9 fldt -16(%rbp) fucomip %st(1), %st fstp %st(0) je .L2 jmp .L7 .L9: fstp %st(0) .L7: fldt -16(%rbp) fld1 faddp %st, %st(1) fldt -16(%rbp) fsubrp %st, %st(1) fld1 fucomip %st(1), %st jp .L10 fld1 fucomip %st(1), %st fstp %st(0) jne .L2 movl $.LC2, %edi call puts fldt -16(%rbp) fadd %st(0), %st fstpt -16(%rbp) addl $1, -20(%rbp) jmp .L5 .L10: fstp %st(0) .L2: movl $.LC3, %edi call puts nop movl $2, %esi movl $.LC4, %edi movl $0, %eax call printf movl $24, %esi movl $.LC5, %edi movl $0, %eax call printf movl $53, %esi movl $.LC6, %edi movl $0, %eax call printf movl $64, %esi movl $.LC7, %edi movl $0, %eax call printf movl $16, %esi movl $.LC8, %edi movl $0, %eax call printf movl $0, %eax leave .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE2: .size main, .-main .ident "GCC: (Ubuntu 9.4.0-1ubuntu1~16.04) 9.4.0" .section .note.GNU-stack,"",@progbits
原因分析与解决方式
核心原因
这个问题的本质是WSL1的x87浮点模拟限制:
WSL1是在Windows内核上模拟Linux系统调用,它对x86的x87浮点单元(FPU)的模拟存在局限。Linux下long double对应的80位扩展精度浮点数,在WSL1中会被降级处理——内存中虽然存储的是16字节格式,但实际计算时会被截断为64位double的精度执行。
从汇编代码能看到程序使用了fldt、faddp等x87指令,但WSL1的模拟层没有真正实现80位完整精度计算,导致实际运行精度和double一致,循环提前在i=53退出。而WSL2是基于轻量级虚拟机的原生实现,能完整支持x87的80位浮点运算,所以long double可以正常发挥64位尾数的精度。
解决方式
- 升级到WSL2:这是最彻底的方案,WSL2完全原生支持Linux的浮点运算特性,不会有精度降级问题。
- 编译时指定SSE指令集:尝试用
-mfpmath=sse编译选项,让GCC使用SSE指令而非x87指令处理浮点运算。不过SSE默认不支持80位long double,此时GCC会将long double映射为double,至少能让行为符合预期。
内容的提问来源于stack exchange,提问作者Ed Wynn

