我想在 GHC Haskell 编译器中实现相当于 C 的uint
-to -cast。double
我们已经实现了int
-to- double
using FILD
or CVTSI2SD
。这些操作是否有无符号版本,或者我应该uint
在转换之前将最高位归零(从而失去范围)?
6 回答
正如有人所说,“好艺术家抄袭,伟大艺术家偷窃”。所以我们可以看看其他编译器作者是如何解决这个问题的。我使用了一个简单的片段:
volatile unsigned int x;
int main()
{
volatile double y = x;
return y;
}
(添加挥发物以确保编译器不会优化转换)
结果(跳过了不相关的说明):
Visual C++ 2010 cl /Ox (x86)
__real@41f0000000000000 DQ 041f0000000000000r ; 4.29497e+009
mov eax, DWORD PTR ?x@@3IC ; x
fild DWORD PTR ?x@@3IC ; x
test eax, eax
jns SHORT $LN4@main
fadd QWORD PTR __real@41f0000000000000
$LN4@main:
fstp QWORD PTR _y$[esp+8]
所以基本上编译器会添加一个调整值,以防符号位被设置。
Visual C++ 2010 cl /Ox (x64)
mov eax, DWORD PTR ?x@@3IC ; x
pxor xmm0, xmm0
cvtsi2sd xmm0, rax
movsdx QWORD PTR y$[rsp], xmm0
无需在此处进行调整,因为编译器知道rax
将清除符号位。
Visual C++ 2012 cl /Ox
__xmm@41f00000000000000000000000000000 DB 00H, 00H, 00H, 00H, 00H, 00H, 00H
DB 00H, 00H, 00H, 00H, 00H, 00H, 00H, 0f0H, 'A'
mov eax, DWORD PTR ?x@@3IC ; x
movd xmm0, eax
cvtdq2pd xmm0, xmm0
shr eax, 31 ; 0000001fH
addsd xmm0, QWORD PTR __xmm@41f00000000000000000000000000000[eax*8]
movsd QWORD PTR _y$[esp+8], xmm0
这使用无分支代码添加 0 或根据符号位是否被清除或设置进行魔术调整。
有一个更好的方法
__m128d _mm_cvtsu32_sd(__m128i n) {
const __m128i magic_mask = _mm_set_epi32(0, 0, 0x43300000, 0);
const __m128d magic_bias = _mm_set_sd(4503599627370496.0);
return _mm_sub_sd(_mm_castsi128_pd(_mm_or_si128(n, magic_mask)), magic_bias);
}
您可以利用 IEEE 双精度格式的某些属性并将无符号值解释为尾数的一部分,同时添加一些精心设计的指数。
Bits 63 62-52 51-0
S Exp Mantissa
0 1075 20 bits 0, followed by your unsigned int
1075 来自双精度数的 IEEE 指数偏差 (1023) 和尾数的 52 位“移位”量。请注意,尾数前面有一个隐含的“1”,需要稍后减去。
所以:
double uint32_to_double(uint32_t x) {
uint64_t xx = x;
xx += 1075ULL << 52; // add the exponent
double d = *(double*)&xx; // or use a union to convert
return d - (1ULL << 52); // 2 ^^ 52
}
如果您的平台上没有本机 64 位,则使用 SSE 进行整数步长的版本可能是有益的,但这当然取决于。
在我的平台上,它编译为
0000000000000000 <uint32_to_double>:
0: 48 b8 00 00 00 00 00 movabs $0x4330000000000000,%rax
7: 00 30 43
a: 89 ff mov %edi,%edi
c: 48 01 f8 add %rdi,%rax
f: c4 e1 f9 6e c0 vmovq %rax,%xmm0
14: c5 fb 5c 05 00 00 00 vsubsd 0x0(%rip),%xmm0,%xmm0
1b: 00
1c: c3 retq
看起来不错。这0x0(%rip)
是神奇的双常数,如果内联一些指令,如高位 32 位归零和常数重载将消失。
我们已经使用 FILD 实现了 int-to-double ...
这些操作是否有无符号版本
如果您想要使用 x87 FILD 操作码,只需将 uint64 转换为 uint63(div 2),然后将其乘以 2,但已经是两倍,因此 x87 uint64 到双倍的转换需要在开销中执行一次 FMUL。
示例:0xFFFFFFFFFFFFFFFFU -> +1.8446744073709551e+0019
它无法以严格的表单规则发布代码示例。我稍后会尝试。
//inline
double u64_to_d(unsigned _int64 v){
//volatile double res;
volatile unsigned int tmp=2;
_asm{
fild dword ptr tmp
//v>>=1;
shr dword ptr v+4, 1
rcr dword ptr v, 1
fild qword ptr v
//save lsb
//mov byte ptr tmp, 0
//rcl byte ptr tmp, 1
//res=tmp+res*2;
fmulp st(1),st
//fild dword ptr tmp
//faddp st(1),st
//fstp qword ptr res
}
//return res;
//fld qword ptr res
}
VC 产生 x86 输出
//inline
double u64_to_d(unsigned _int64 v){
55 push ebp
8B EC mov ebp,esp
81 EC 04 00 00 00 sub esp,04h
//volatile double res;
volatile unsigned int tmp=2;
C7 45 FC 02 00 00 00 mov dword ptr [tmp], 2
_asm{
fild dword ptr tmp
DB 45 FC fild dword ptr [tmp]
//v>>=1;
shr dword ptr v+4, 1
D1 6D 0C shr dword ptr [ebp+0Ch],1
rcr dword ptr v, 1
D1 5D 08 rcr dword ptr [v],1
fild qword ptr v
DF 6D 08 fild qword ptr [v]
//save lsb
// mov byte ptr [tmp], 0
//C6 45 FC 00 mov byte ptr [tmp], 0
// rcl byte ptr tmp, 1
//D0 55 FC rcl byte ptr [tmp],1
//res=tmp+res*2;
fmulp st(1),st
DE C9 fmulp st(1),st
// fild dword ptr tmp
//DB 45 FC fild dword ptr [tmp]
// faddp st(1),st
//DE C1 faddp st(1),st
//fstp qword ptr res
//fstp qword ptr [res]
}
//return res;
//fld qword ptr [res]
8B E5 mov esp,ebp
5D pop ebp
C3 ret
}
我发布了(可能我手动删除了文本文件中所有不正确的 ascii 字符)。
如果我对您的理解正确,您应该能够将您的 32 位 uint 移动到堆栈上的临时区域,将下一个 dword 清零,然后使用 fild qword ptr 将现在的 64 位无符号整数加载为双精度。
在 AVX-512 之前,x86 没有无符号 <-> FP 指令。
(对于 AVX-512F,请参阅vcvtusi2sd
和vcvtsd2usi
,以及它们各自的ss
版本。还打包了涉及 64 位整数的 SIMD 转换,这也是新的;在 AVX-512F 之前,打包的转换转换可以去往/来自 int32_t。)
在 64 位代码中,无符号 32 位 -> FP 很简单:只需将 u32 零扩展为 i64 并使用有符号 64 位转换。 每个 uint32_t 值都可以表示为非负 int64_t。
对于反向,转换 FP -> i64 并截断为 u32,如果您对超出范围 FP 输入的情况感到满意。(包括 i64 超出范围时的 0,否则采用 2 的补码 i64 位模式的 low32。)
u32 -> FP:请参阅@Igor Skochinsky 的编译器输出答案。x86-64 GCC 和 Clang 使用与 x64 MSVC 相同的技巧。关键部分是将其零扩展为 64 位并进行转换。请注意,写入 32 位寄存器会隐式零扩展为 64 位mov r32, r32
,因此如果您知道该值是使用 32 位操作写入的,则可能不需要。(或者如果您必须自己从内存中加载它)。
; assuming your input starts in EDI, and that RDI might have garbage in the high half
; like a 32-bit function arg.
mov eax, edi ; mov-elimination wouldn't work with edi,edi
vcvtsi2sd xmm0, xmm7, rax ; where XMM7 is some cold register to avoid a false dep
mov edi,edi
除了(如果您需要单独的零扩展指令)之外的任何选择都是由 mov-elimination 不工作在相同,相同的寄存器情况下推动的:请参阅x86 的 MOV 真的可以“免费”吗?为什么我根本无法重现这个?.
如果您没有 AVX,或者不知道要使用的不是最近编写的寄存器,您可能希望pxor xmm0, xmm0
在设计不佳的cvtsi2sd
合并到它之前使用。GCC 虔诚地打破了错误的 dep,clang 非常随意,除非循环携带的 dep 链将存在于单个函数中。因此,它可能会因单独的非内联函数之间的交互而减慢,这些函数可能碰巧在循环中被调用。请参阅为什么添加 xorps 指令会使此函数使用 cvtsi2ss 并增加约 5 倍的速度?例如,这会叮叮当当(但 GCC 很好。)
该答案还链接了一些 GCC 错过优化错误报告,我在其中写了更多关于重用“冷”寄存器以避免转换中的错误依赖关系以及类似[v]sqrtsd
1 输入操作的想法的详细信息。
32 位模式:
不同的编译器有不同的策略。 gcc -O3 -m32 -mfpmath=sse -msseregparm
是查看 GCC 功能的好方法,使其返回 XMM0 而不是 ST0,因此它仅在实际上更方便时才使用 x87。(例如,对于 64 位 -> FP 使用fild
)。
我使用 gcc 和 clang在 Godbolt 上放置了一些 u32 和 u64 -> float 或 double 测试功能,但这个答案主要是为了回答问题的 x86-64 部分,其他答案没有很好地涵盖,而不是过时的 32 位代码生成器。所以我不打算在这里复制代码和汇编并剖析它。
我会提到它double
可以精确地表示 every u32
,这允许一个简单的(double)(int)(u32 - 2^31) + double(2^31)
技巧来进行有符号转换的范围移位。但是u32
->float
并不是那么容易。