C ++

时间:2019-06-02 17:00:16

标签: c++ performance x86-64 processing-efficiency floor

我想定义一个有效的整数下限函数,即从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体系结构。

5 个答案:

答案 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_epi32vroundpsvcvtps2dq)。此版本的非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

See it live on godbolt

答案 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>

Try it for yourself