我想定义一个有效的整数下限函数,即从float或double转换为向无穷大的截断。
我们可以假设这些值使得没有整数溢出发生。到目前为止,我有几个选择
广播到int;这需要对负值进行特殊处理,因为转换会截断为零。
I= int(F); if (I < 0 && I != F) I--;
将floor的结果转换为int;
int(floor(F));
强制转换为整数以获得正数(对于较大的值,这可能会返回错误的结果);
int(F + double(0x7fffffff)) - 0x7fffffff;
众所周知,向int铸造很慢。如果测试也是如此。我尚未设置发言权功能的时间,但是看到帖子声称它也很慢。
您能在速度,准确性或允许范围方面考虑更好的选择吗?它不需要是便携式的。目标是最新的x86 / x64体系结构。
答案 0 :(得分:46)
众所周知,向int铸造很慢。
也许您从x86-64开始就生活在一块石头上,或者以其他方式错过了x86上一段时间以来并非如此。 :)
SSE / SSE2具有一条指令,该指令可以将带有截断的转换为(而不是默认的舍入模式)。 ISA精确地支持此操作,因为在实际的代码库中使用C语义进行转换并不罕见。 x86-64代码使用SSE / SSE2 XMM寄存器进行标量FP数学运算,而不使用x87寄存器,因为这样做以及其他一些提高效率的事情。甚至现代的32位代码也使用XMM寄存器进行标量数学运算。
在为x87编译时(没有SSE3 fisttp
),编译器过去不得不将x87舍入模式更改为截断,FP存储到内存,然后再次将舍入模式更改回。 (然后再从内存(通常是从堆栈中的本地重新加载该整数,如果用它做更多的事情的话。))x87对此很糟糕。
是的,很慢,例如在2006年,当您编写@Kirjain答案中的链接时,如果您仍然有32位CPU或使用x86-64 CPU运行32位代码。
不直接支持采用舍入或截断或默认值(最近)以外的舍入模式进行转换,直到SSE4.1 roundps
/ roundpd
为止,您最好的选择是{{ 3}},来自@Kirjain的答案。
有些不错的技巧,但仅适用于double
-> 32位整数。如果您有double
,则可能不值得扩展到float
。
或更通常地,只需添加一个大数字以触发舍入,然后再次将其减去以返回原始范围。这可以在float
上使用而无需扩展到double
,但是我不确定使floor
的工作有多么容易。
无论如何,这里显而易见的解决方案是_mm256_floor_ps()
和_mm256_cvtps_epi32
(vroundps
和vcvtps2dq
)。此版本的非AVX版本可以与SSE4.1一起使用。
我不确定我们是否可以做得更好;如果您有大量数组要处理(并且无法将这项工作与其他工作进行交错处理),则您可以将MXCSR舍入模式设置为“ towards -Inf”(底数),只需使用vcvtps2dq
(使用当前的舍入模式)。然后重新设置。但是,最好是缓存阻止转换或在生成数据时立即进行转换,这大概是来自其他需要将FP舍入模式设置为默认的Nearest的FP计算中。
roundps
/ pd / ss / sd在Intel CPU上为2 oups,但在AMD Ryzen上仅为1 uop(每128位通道)。 cvtps2dq
也是1 uop。打包的double-> int转换还包括一个改组。标量FP-> int转换(复制到整数寄存器)通常也为此花费额外的成本。
因此,在某些情况下,魔术数字技巧有可能获胜。也许值得研究_mm256_floor_ps()
+ cvt是否是关键瓶颈的一部分(或者如果您有两倍并想要int32,则更有可能)。
@CássioRenan的int foo = floorf(f)
实际上可以用gcc -O3 -fno-trapping-math
(或-ffast-math
),-march=
和具有SSE4.1或AVX的东西进行自动矢量化。 the 2006 link
如果您将其与未手动向量化的其他标量代码一起使用,这可能很有用。尤其是如果您希望编译器将对整个过程进行自动向量化。
答案 1 :(得分:19)
看看magic numbers。网页上提出的算法应该比简单的转换有效得多。我自己从未使用过它,但这是它们在网站上提供的性能比较(xs_ToInt和xs_CRoundToInt是建议的功能):
Performing 10000000 times:
simple cast 2819 ms i.e. i = (long)f;
xs_ToInt 1242 ms i.e. i = xs_ToInt(f); //numerically same as above
bit-twiddle(full) 1093 ms i.e. i = BitConvertToInt(f); //rounding from Fluid
fistp 676 ms i.e. i = FISTToInt(f); //Herf, et al x86 Assembly rounding
bit-twiddle(limited) 623 ms i.e. i = FloatTo23Bits(f); //Herf, rounding only in the range (0...1]
xs_CRoundToInt 609 ms i.e. i = xs_CRoundToInt(f); //rounding with "magic" numbers
此外,显然修改了xs_ToInt,从而提高了性能:
Performing 10000000 times:
simple cast convert 3186 ms i.e. fi = (f*65536);
fistp convert 3031 ms i.e. fi = FISTToInt(f*65536);
xs_ToFix 622 ms i.e. fi = xs_Fix<16>::ToFix(f);
“魔术数字”方法的简要说明:
“基本上,为了添加两个浮点数,您的处理器将这些数字的小数点“对齐”,以便可以轻松地添加这些位。它通过“归一化”这些数字以使最高位有效位被保留,即较小的数字“归一化”以匹配较大的数字。因此,xs_CRoundToInt()使用的“魔术数”转换的原理是:我们添加了足够大的浮点数(一个如此大的数字)仅在小数点前有有效数字,而在小数点后没有任何有效数字)到您要转换的数字,使得:(a)处理器将数字标准化为与其相等的整数,并且(b)不将其相加删除您要转换的数字中的整数有效位(即XX00 + 00YY = XXYY)。”
引用来自同一网页。
答案 2 :(得分:4)
如果您要批量执行此操作,则编译器可以自动将其矢量化(如果您知道自己在做什么)。例如,这是一个小型实现,可以在GCC上自动将浮点数转换为整数:
#include <cmath>
// Compile with -O3 and -march=native to see autovectorization
__attribute__((optimize("-fno-trapping-math")))
void testFunction(float* input, int* output, int length) {
// Assume the input and output are aligned on a 32-bit boundary.
// Of course, you have to ensure this when calling testFunction, or else
// you will have problems.
input = static_cast<float*>(__builtin_assume_aligned(input, 32));
output = static_cast<int*>(__builtin_assume_aligned(output, 32));
// Also assume the length is a multiple of 32.
if (length & 31) __builtin_unreachable();
// Do the conversion
for (int i = 0; i < length; ++i) {
output[i] = floor(input[i]);
}
}
这是为x86-64生成的程序集(带有AVX512指令):
testFunction(float*, int*, int):
test edx, edx
jle .L5
lea ecx, [rdx-1]
xor eax, eax
.L3:
# you can see here that the conversion was vectorized
# to a vrndscaleps (that will round the float appropriately)
# and a vcvttps2dq (thal will perform the conversion)
vrndscaleps ymm0, YMMWORD PTR [rdi+rax], 1
vcvttps2dq ymm0, ymm0
vmovdqa64 YMMWORD PTR [rsi+rax], ymm0
add rax, 32
cmp rax, rdx
jne .L3
vzeroupper
.L5:
ret
如果您的目标不支持AVX512,则即使您有目标,它仍将使用SSE4.1指令进行自动矢量化。这是-O3 -msse4.1
的输出:
testFunction(float*, int*, int):
test edx, edx
jle .L1
shr edx, 2
xor eax, eax
sal rdx, 4
.L3:
roundps xmm0, XMMWORD PTR [rdi+rax], 1
cvttps2dq xmm0, xmm0
movaps XMMWORD PTR [rsi+rax], xmm0
add rax, 16
cmp rax, rdx
jne .L3
.L1:
ret
答案 3 :(得分:1)
为什么不只使用这个:
#include <cmath>
auto floor_(float const x) noexcept
{
int const t(x);
return t - (t > x);
}
答案 4 :(得分:1)
这是CássioRenan出色答案的一种变体。它用标准C ++代替了所有特定于编译器的扩展,并且从理论上讲,它可移植到任何符合要求的编译器中。另外,它检查参数是否正确对齐,而不是假定对齐。它会优化为相同的代码。
#include <assert.h>
#include <cmath>
#include <stddef.h>
#include <stdint.h>
#define ALIGNMENT alignof(max_align_t)
using std::floor;
// Compiled with: -std=c++17 -Wall -Wextra -Wpedantic -Wconversion -fno-trapping-math -O -march=cannonlake -mprefer-vector-width=512
void testFunction(const float in[], int32_t out[], const ptrdiff_t length)
{
static_assert(sizeof(float) == sizeof(int32_t), "");
assert((uintptr_t)(void*)in % ALIGNMENT == 0);
assert((uintptr_t)(void*)out % ALIGNMENT == 0);
assert((size_t)length % (ALIGNMENT/sizeof(int32_t)) == 0);
alignas(ALIGNMENT) const float* const input = in;
alignas(ALIGNMENT) int32_t* const output = out;
// Do the conversion
for (int i = 0; i < length; ++i) {
output[i] = static_cast<int32_t>(floor(input[i]));
}
}
在GCC上,它的优化效果不如使用不可移植扩展名的原始优化。 C ++标准确实支持alignas
说明符,对对齐数组的引用以及std::align
函数,该函数返回缓冲区中的对齐范围。但是,这些都不会使我测试过的任何编译器生成对齐的,而不是生成未对齐的矢量加载和存储。
尽管alignof(max_align_t)
在x86_64上仅为16,并且可以将ALIGNMENT
定义为常数64,但这并不能帮助任何编译器生成更好的代码,因此我着眼于可移植性。强制编译器假定Poitner对齐的可移植方式最接近的方法是使用<immintrin.h>
中的类型,大多数x86编译器都支持这些类型,或者使用{{1 }}说明符。通过检查预定义的宏,您还可以在Linux编译器上将宏扩展为struct
,在Windows编译器上将宏扩展为alignas
,在我们不知道的编译器上可以放一些安全的东西,但是GCC需要在类型实际上可以生成对齐的负载和存储。
另外,原始示例调用bulit-in来告诉GCC,__attribute__ ((aligned (ALIGNMENT)))
不可能不是32的倍数。如果您__declspec (align (ALIGNMENT))
或调用诸如{ {1}},GCC,Clang和ICC均不会做出相同的推论。因此,他们生成的大多数代码都可以处理length
不是向量宽度的整数倍的情况。
一个可能的原因是,没有一种优化不能使您获得如此快的速度:具有对齐地址的未对齐内存指令在Intel CPU上速度很快,并且处理assert()
不是一个很好的整数的情况的代码是几个字节长,并在恒定时间内运行。
作为脚注,与abort()
中实现的宏相比,GCC能够更好地优化length
中的内联函数。
GCC 9.1需要一组特定的选项来生成AVX512代码。默认情况下,即使使用length
,它也会首选256位向量。它需要<cmath>
才能生成512位代码。 (感谢彼得·科德斯(Peter Cordes)指出这一点。)它跟随矢量化循环,并展开了代码以转换数组的所有剩余元素。
这是矢量化的主循环,减去了仅会运行一次的一些恒定时间初始化,错误检查和清理代码:
<math.c>
老鹰眼将注意到与CássioRenan程序生成的代码有两个区别:它使用%zmm而不是%ymm寄存器,并且它以未对齐的-march=cannonlake
而不是对齐的{{1 }}。
带有相同标志的Clang 8.0.0对展开循环做出了不同的选择。每次迭代都对八个512位向量(即128个单精度浮点数)进行操作,但是用于展开剩余数据的代码并未展开。如果在那之后至少剩下64个浮点数,它将使用另外四个AVX512指令,然后使用未向量化的循环清除所有多余内容。
如果您使用Clang ++编译原始程序,它将毫无保留地接受它,但是不会进行相同的优化:它仍然不会假设-mprefer-vector-width=512
是向量宽度的倍数,也不会指针对齐。
即使没有.L7:
vrndscaleps zmm0, ZMMWORD PTR [rdi+rax], 1
vcvttps2dq zmm0, zmm0
vmovdqu32 ZMMWORD PTR [rsi+rax], zmm0
add rax, 64
cmp rax, rcx
jne .L7
,它也比AVX256更喜欢AVX512代码。
vmovdqu32
ICC 19还生成AVX512指令,但与vmovdqa64
有很大不同。它使用魔术常数进行更多设置,但不会展开任何循环,而是对512位向量进行操作。
此代码还可以在其他编译器和体系结构上使用。 (尽管MSVC仅支持最高AVX2的ISA,并且不能自动对循环进行矢量化。)例如,在具有length
的ARM上,它会生成具有-mprefer-vector-width=512
和 test rdx, rdx
jle .LBB0_14
cmp rdx, 63
ja .LBB0_6
xor eax, eax
jmp .LBB0_13
.LBB0_6:
mov rax, rdx
and rax, -64
lea r9, [rax - 64]
mov r10, r9
shr r10, 6
add r10, 1
mov r8d, r10d
and r8d, 1
test r9, r9
je .LBB0_7
mov ecx, 1
sub rcx, r10
lea r9, [r8 + rcx]
add r9, -1
xor ecx, ecx
.LBB0_9: # =>This Inner Loop Header: Depth=1
vrndscaleps zmm0, zmmword ptr [rdi + 4*rcx], 9
vrndscaleps zmm1, zmmword ptr [rdi + 4*rcx + 64], 9
vrndscaleps zmm2, zmmword ptr [rdi + 4*rcx + 128], 9
vrndscaleps zmm3, zmmword ptr [rdi + 4*rcx + 192], 9
vcvttps2dq zmm0, zmm0
vcvttps2dq zmm1, zmm1
vcvttps2dq zmm2, zmm2
vmovups zmmword ptr [rsi + 4*rcx], zmm0
vmovups zmmword ptr [rsi + 4*rcx + 64], zmm1
vmovups zmmword ptr [rsi + 4*rcx + 128], zmm2
vcvttps2dq zmm0, zmm3
vmovups zmmword ptr [rsi + 4*rcx + 192], zmm0
vrndscaleps zmm0, zmmword ptr [rdi + 4*rcx + 256], 9
vrndscaleps zmm1, zmmword ptr [rdi + 4*rcx + 320], 9
vrndscaleps zmm2, zmmword ptr [rdi + 4*rcx + 384], 9
vrndscaleps zmm3, zmmword ptr [rdi + 4*rcx + 448], 9
vcvttps2dq zmm0, zmm0
vcvttps2dq zmm1, zmm1
vcvttps2dq zmm2, zmm2
vcvttps2dq zmm3, zmm3
vmovups zmmword ptr [rsi + 4*rcx + 256], zmm0
vmovups zmmword ptr [rsi + 4*rcx + 320], zmm1
vmovups zmmword ptr [rsi + 4*rcx + 384], zmm2
vmovups zmmword ptr [rsi + 4*rcx + 448], zmm3
sub rcx, -128
add r9, 2
jne .LBB0_9
test r8, r8
je .LBB0_12
.LBB0_11:
vrndscaleps zmm0, zmmword ptr [rdi + 4*rcx], 9
vrndscaleps zmm1, zmmword ptr [rdi + 4*rcx + 64], 9
vrndscaleps zmm2, zmmword ptr [rdi + 4*rcx + 128], 9
vrndscaleps zmm3, zmmword ptr [rdi + 4*rcx + 192], 9
vcvttps2dq zmm0, zmm0
vcvttps2dq zmm1, zmm1
vcvttps2dq zmm2, zmm2
vcvttps2dq zmm3, zmm3
vmovups zmmword ptr [rsi + 4*rcx], zmm0
vmovups zmmword ptr [rsi + 4*rcx + 64], zmm1
vmovups zmmword ptr [rsi + 4*rcx + 128], zmm2
vmovups zmmword ptr [rsi + 4*rcx + 192], zmm3
.LBB0_12:
cmp rax, rdx
je .LBB0_14
.LBB0_13: # =>This Inner Loop Header: Depth=1
vmovss xmm0, dword ptr [rdi + 4*rax] # xmm0 = mem[0],zero,zero,zero
vroundss xmm0, xmm0, xmm0, 9
vcvttss2si ecx, xmm0
mov dword ptr [rsi + 4*rax], ecx
add rax, 1
cmp rdx, rax
jne .LBB0_13
.LBB0_14:
pop rax
vzeroupper
ret
.LBB0_7:
xor ecx, ecx
test r8, r8
jne .LBB0_11
jmp .LBB0_12
的向量化循环。 / p>