【问题标题】:Efficient float to int without overflow有效的浮点数到int而不会溢出
【发布时间】:2020-04-17 07:00:41
【问题描述】:
using int_type = int;
int_type min = std::numeric_limits<Depth>::min();
int_type max = std::numeric_limits<Depth>::max();

int_type convert(float f) {
    if(f < static_cast<float>(min)) return min; // overflow
    else if(f > static_cast<float>(max)) return max; // overflow
    else return static_cast<int_type>(f);
}

有没有更有效的方法将float f 转换为int_type,同时将其限制为整数类型的最小值和最大值? 例如,没有将 minmax 转换为 float 进行比较。

【问题讨论】:

  • 你在这个函数上花费了多少时间?如果它很重要,我会感到惊讶。

标签: c++ optimization floating-point


【解决方案1】:

有时几乎总是,信任编译器是最好的做法。

这段代码:

template<class Integral>
__attribute__((noinline))
int convert(float f)
{
    using int_type = Integral;
    constexpr int_type min = std::numeric_limits<int_type>::min();
    constexpr int_type max = std::numeric_limits<int_type>::max();

    constexpr float fmin = static_cast<float>(min);
    constexpr float fmax = static_cast<float>(max);

    if(f < fmin) return min; // overflow
    if(f > fmax) return max; // overflow
    return static_cast<int_type>(f);
}

使用 -O2 和 -fomit-frame-pointer 编译,产生:

__Z7convertIiEif:                       ## @_Z7convertIiEif
    .cfi_startproc
    movl    $-2147483648, %eax      ## imm = 0xFFFFFFFF80000000
    movss   LCPI1_0(%rip), %xmm1    ## xmm1 = mem[0],zero,zero,zero
    ucomiss %xmm0, %xmm1
    ja  LBB1_3
    movl    $2147483647, %eax       ## imm = 0x7FFFFFFF
    ucomiss LCPI1_1(%rip), %xmm0
    ja  LBB1_3
    cvttss2si   %xmm0, %eax
LBB1_3:
    retq

我不确定它是否会更有效。

注意此处定义的 LCPI_x:

    .section    __TEXT,__literal4,4byte_literals
    .align  2
LCPI1_0:
    .long   3472883712              ## float -2.14748365E+9
LCPI1_1:
    .long   1325400064              ## float 2.14748365E+9

如何使用 fmin()、fmax() 进行钳位... [感谢 njuffa 的提问]

代码确实变得更有效率,因为条件跳转被移除了。但是,它开始在钳位限制处表现不正确。

考虑:

template<class Integral>
__attribute__((noinline))
int convert2(float f)
{
    using int_type = Integral;
    constexpr int_type min = std::numeric_limits<int_type>::min();
    constexpr int_type max = std::numeric_limits<int_type>::max();

    constexpr float fmin = static_cast<float>(min);
    constexpr float fmax = static_cast<float>(max);

    f = std::min(f, fmax);
    f = std::max(f, fmin);
    return static_cast<int_type>(f);
}

调用

auto i = convert2<int>(float(std::numeric_limits<int>::max()));

结果:

-2147483648

显然我们需要通过 epsilon 来减少限制,因为浮点数无法准确表示 int 的全部范围,所以...

template<class Integral>
__attribute__((noinline))
int convert2(float f)
{
    using int_type = Integral;
    constexpr int_type min = std::numeric_limits<int_type>::min();
    constexpr int_type max = std::numeric_limits<int_type>::max();

    constexpr float fmin = static_cast<float>(min) - (std::numeric_limits<float>::epsilon() * static_cast<float>(min));
    constexpr float fmax = static_cast<float>(max) - (std::numeric_limits<float>::epsilon() * static_cast<float>(max));

    f = std::min(f, fmax);
    f = std::max(f, fmin);
    return static_cast<int_type>(f);
}

应该会更好……

除了现在相同的函数调用产生:

2147483392

顺便说一句,处理这个问题实际上导致我在原始代码中发现了一个错误。由于同样的舍入误差问题,&gt;&lt; 运算符需要替换为 &gt;=&lt;=

像这样:

template<class Integral>
__attribute__((noinline))
int convert(float f)
{
    using int_type = Integral;
    constexpr int_type min = std::numeric_limits<int_type>::min();
    constexpr int_type max = std::numeric_limits<int_type>::max();

    constexpr float fmin = static_cast<float>(min);
    constexpr float fmax = static_cast<float>(max);

    if(f <= fmin) return min; // overflow
    if(f >= fmax) return max; // overflow
    return static_cast<int_type>(f);
}

【讨论】:

  • 在转换为整数之前使用fmin()fmax() 进行钳制怎么样?这不会导致更短的代码吗?如果我没记错的话,有浮点最小值/最大值的硬件指令。
  • @njuffa 试过了。结果很有趣!查看更新的答案。
  • 我知道我的想法哪里出了问题。为了使钳位在浮点空间中正常工作,整数限制必须可以精确地表示为浮点数。显然,32 位整数限制不能在 float 中表示,但它们可以在 double 中表示。因此,我认为这可以用于少于 53 位的int_type,但需要额外的转换,从而抵消任何潜在的性能优势。
  • @njuffa 我不会说你的想法有问题。我认为 epsilon 解决方案实际上是最佳的。让我们面对现实吧,当数字变得如此之高时,无论如何浮点数都是不准确的,那么如果钳位低于 int_max 几百又有什么关系?
  • 我认为这不是非常重要,但也许值得否定一个钳位条件以捕获 NaN。
【解决方案2】:

对于 32 位整数,你可以让 CPU 为你做一些钳位工作。

如果浮点数超出范围,cvtss2si 指令实际上将返回 0x80000000。这可以让您在大多数情况下消除一项测试:

int convert(float value)
{
    int result = _mm_cvtss_si32(_mm_load_ss(&value));
    if (result == 0x80000000 && value > 0.0f)
        result = 0x7fffffff;
    return result;
}

如果您有很多要转换的,那么_mm_cvtps_epi32 允许您一次处理四个(溢出时具有相同的行为)。这应该比一次处理一个要快得多,但是您需要以不同的方式构造代码才能使用它。

【讨论】:

    【解决方案3】:

    如果你想截断,你可以利用avx2和avx指令512:

    #include <float.h>
    
    int main() {
        __m256 a = {5.423423, -4.243423, 423.4234234, FLT_MAX, 79.4234876, 19.7, 8.5454, 7675675.6};
        __m256i b = _mm256_cvttps_epi32(a);
        void p256_hex_u32(__m256i in) {
        alignas(32) uint32_t v[8];
        _mm256_store_si256((__m256i*)v, in);
        printf("v4_u32: %d %d %d %d %d %d %d %d\n", v[0], v[1], v[2], v[3], v[4], v[5], v[6], v[7]);
    }
    

    编译:

    g++ -std=c++17 -mavx2  a.cpp && ./a.out
    

    对于mavx512(我的cpu不支持,所以我不会提供工作测试,请随意编辑):

    _mm512_maskz_cvtt_roundpd_epi64(k, value, _MM_FROUND_NO_EXC);
    

    【讨论】:

      猜你喜欢
      • 2023-03-16
      • 1970-01-01
      • 1970-01-01
      • 2014-01-08
      • 2023-04-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-02-03
      相关资源
      最近更新 更多