【问题标题】:Efficient division of an int by intmax通过 intmax 有效划分 int
【发布时间】:2019-08-20 08:44:54
【问题描述】:

我有一个uint32_t 类型的整数,我想将它除以uint32_t 的最大值并获得float 的结果(范围为0..1)。

当然,我可以做到以下几点:

float result = static_cast<float>(static_cast<double>(value) / static_cast<double>(std::numeric_limits<uint32_t>::max()))

然而,这需要大量的转换,而且分割本身可能很昂贵。

有没有办法更快地实现上述操作,而不需要除法和多余的类型转换?或者也许我不应该担心,因为现代编译器已经能够生成高效的代码?

编辑:除以MAX+1,有效地给我一个[0..1) 范围内的浮点数也可以。


更多上下文:

我在对时间要求严格的循环中使用上述转换,uint32_t 由相对较快的随机数生成器(例如 pcg)生成。我预计上述转换的转换/除法可能会对我的代码性能产生一些明显的负面影响,尽管不是压倒性的。

【问题讨论】:

  • 无论如何,您是否必须除以maxint 而不是maxint + 1?如果maxint+1 有效,则可能可以做一些魔术,因为它是由2^31 相除的,这实际上是数字的指数部分的减法。
  • 在循环过程中是否需要将数字转换为浮点数? value 不是代表value / std::numeric_limits&lt;std::uint32_t&gt;::max() 的分子吗?
  • 另请注意,在许多架构(尤其是 Intel x86 和 x64)上,存储 float 值比存储 double 值慢 - 无论如何,算术运算通常在 double 中完成。所以也许完全避免float
  • @dyukha 是的,使用maxint+1 就足够了。
  • @CygnusX1 请注意,使用float(value) / float(1ul &lt;&lt; 32ul) 很可能会为您提供[0, 1] 的范围,而不是[0, 1)(这是实际发行版通常需要的)。 uniform_real_distribution 为您提供 [0, 1)(除非您使用旧的错误版本的 g++ 或 clang++`)。

标签: c++


【解决方案1】:

这听起来像是一份工作:

std::uniform_real_distribution<float> dist(0.f, 1.f);

我相信这可以尽可能高效地在[0, 1) 范围内为您提供到float 的无偏见转换。如果您希望范围为[0, 1],您可以使用:

std::uniform_real_distribution<float> dist(0.f, std::nextafter(1.f, 2.f))

这是一个示例,其中包含两个非随机数字生成器实例,它为uint32_t 生成最小值和最大值:

#include <iostream>
#include <limits>
#include <random>

struct ui32gen {
    constexpr ui32gen(uint32_t x) : value(x) {}
    uint32_t operator()() { return value; }
    static constexpr uint32_t min() { return 0; }
    static constexpr uint32_t max() { return std::numeric_limits<uint32_t>::max(); }
    uint32_t value;
};

int main() {
    ui32gen min(ui32gen::min());
    ui32gen max(ui32gen::max());

    std::uniform_real_distribution<float> dist(0.f, 1.f);

    std::cout << dist(min) << "\n";
    std::cout << dist(max) << "\n";
}

输出:

0
1

有没有办法实现运算更快,不用除法 和过多的类型转换?

如果您想手动执行与uniform_real_distribution 类似的操作(但要快得多,并且稍微偏向较低的值),您可以定义如下函数:

// [0, 1)  the common range
inline float zero_to_one_exclusive(uint32_t value) {
    static const float f_mul =
        std::nextafter(1.f / float(std::numeric_limits<uint32_t>::max()), 0.f);

    return float(value) * f_mul;
}

它使用乘法而不是除法,因为这通常比您最初的建议要快一点(比您最初的建议)并且只有一种类型转换。这是division vs. multiplication的比较。

如果您真的希望范围为[0, 1],您可以执行以下操作,与std::uniform_real_distribution&lt;float&gt; dist(0.f, std::nextafter(1.f, 2.f)) 产生的值相比,这也会略微偏向较低的值:

// [0, 1]  the not so common range
inline float zero_to_one_inclusive(uint32_t value) {
    static const float f_mul = 1.f/float(std::numeric_limits<uint32_t>::max());

    return float(value) * f_mul;
}

这是一个 benchmark 比较 uniform_real_distributionzero_to_one_exclusivezero_to_one_inclusive

【讨论】:

  • 创造性的非随机使用随机。虽然有点冗长。
  • @MSalters 这个想法是让 OP 用他/她提到的真正的 RNG(例如 PCG)替换示例中 ui32gen 的使用,所以之后可能没有那么有创意全部:-)
  • 这很聪明,但我不认为std::uniform_real_distribution 必须以这种方式工作。 AFAIK 允许使用更聪明的映射,有一些状态,甚至多次调用ui32gen::operator()
  • @HolyBlackCat 如果它多次调用operator(),我会感到惊讶。这将大大减慢随机数的生成速度。它可能会使用一些巧妙的映射,但它应该是一个线性变换。我通过观察(不是通过查看代码)得出结论,它与我刚刚在g++clang++ 中添加的zero_to_one_exclusive 函数匹配。
  • 如果我能得到提示,我不介意投反对票。我。这回答了 OP:s 的问题。 uniform_real_distribution 设计用于与 RNG:s(如 PCG)一起使用。我还为通常的半开区间和闭区间提供了更快但不那么准确的替代方案。
【解决方案2】:

其中两个演员表是多余的。当您分配给float 时,您不需要转换为float。此外,强制转换操作数之一以避免整数运算就足够了。所以我们就剩下了

float result = static_cast<double>(value) / std::numeric_limits<int>::max();

你无法避免最后一次强制转换(否则你会得到整数算术)。

也许我不应该担心,因为现代编译器能够 已经生成高效的代码了吗?

绝对是和否!是的,请相信它最了解优化代码并首先编写可读性的编译器。不,不要盲目相信。查看编译器的输出。比较不同的版本并进行测量。

有没有办法更快地实现上述操作,无需除法 [...]?

可能是的。除以std::numeric_limits&lt;int&gt;::max() 非常特别,如果编译器带有一些技巧,我不会太惊讶。我的第一种方法是再次查看编译器的输出,并可能比较不同的编译器。只有当编译器的输出结果不是最优时,我才会费心输入一些手动位摆弄。

对于进一步阅读,这可能会感兴趣:How expensive is it to convert between int and double?。 TL;DR:这实际上取决于硬件。

【讨论】:

  • 可能会考虑乘以倒数,但我怀疑智能编译器已经为常数除数执行此操作。
  • std::numeric_limits&lt;int&gt;::max() 大概是(1 &lt;&lt; 32) - 1 (小于二的幂),并且可能没有很好的方法来简化浮点运算。但是,(1 &lt;&lt; 32) 作为除数是 2 的普通幂,因此具有精确的浮点逆数,因此您可以进行乘法而不是除法。
  • @schnaader 除非您放弃算术运算符通常具有的准确性保证,否则不会考虑:godbolt.org/z/N9Jmnj
  • 您从代码中删除的两个强制转换,它们仍然执行对吗?我将它们包括在内只是为了在所需的转换中更加明确。
【解决方案3】:

如果性能是一个真正的问题,我想我会倾向于在它自己的类中表示这个“实际上是分数的整数”,并且只在必要时执行任何转换。

例如:

#include <iostream>
#include <cstdint>
#include <limits>

struct fraction
{
    using value_type = std::uint32_t;

    constexpr explicit fraction(value_type num = 0) : numerator_(num) {}

    static constexpr auto denominator() -> value_type { return std::numeric_limits<value_type>::max(); }

    constexpr auto numerator() const -> value_type { return numerator_; }

    constexpr auto as_double() const -> double {
        return double(numerator()) / denominator();
    }

    constexpr auto as_float() const -> float {
        return float(as_double());
    }

private:

    value_type numerator_;
};

auto generate() -> std::uint32_t;

int main()
{
    auto frac = fraction(generate());

    // use/manipulate/display frac here ...

    // ... and finally convert to double/float if necessary

    std::cout << frac.as_double() << std::endl;
}

但是,如果您查看 godbolt 上的代码生成器,您会发现 CPU 的浮点指令负责转换。在您冒着浪费时间进行早期优化的风险之前,我倾向于衡量性能。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-08-08
    • 1970-01-01
    • 2014-04-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-11-05
    相关资源
    最近更新 更多