【问题标题】:How to force pow(float, int) to return float如何强制 pow(float, int) 返回 float
【发布时间】:2018-01-16 12:04:21
【问题描述】:

重载函数 float pow(float base, int iexp ) 在 C++11 中被删除,现在 pow 返回一个 double。在我的程序中,我正在计算很多这些(单精度),并且我对最有效的方法很感兴趣。

是否有带有上述签名的特殊函数(在标准库或任何其他函数中)?

如果不是,最好(就单精度性能而言)在任何其他操作之前将pow 的结果显式转换为float(这会将其他所有内容转换为double)或转换iexp进入float 并使用重载函数float pow(float base, float exp)?

编辑:为什么我需要float而不使用double

主要原因是 RAM —— 我需要数十或数百 GB,因此这种减少是巨大的优势。所以我需要从float 得到float。现在我需要最有效的方法来实现这一点(更少的演员,使用已经优化的算法等)。

【问题讨论】:

  • 您是否将此视为性能问题?根据目标,您可能会发现double 版本更快
  • @Caninonos 实际上,根据cpp reference,所有这些整数函数都已被删除......(重载7的评论:“如果任何参数具有整数类型,则它被强制转换加倍 [...]")
  • This answer 可能很有趣...考虑到这一点,我倾向于推荐 float+float 重载,但如果不对这两种变体进行基准测试,就不可能得到明确的答案(结果是很可能是特定于编译器的...)。
  • @Non-maskable Interrupt 关于 Caleth 的评论,我曾经有一个实际案例,doublefloat 快得多。我们预计它会比使用双精度更快,因为 SSE 指令可以在一条指令中处理更多数据,而且我们的数据是 24 位浮点数。检查汇编代码后,问题是结果总是用特殊指令四舍五入到浮点精度,这些消耗了大量时间,而双精度运算没有使用任何舍入。有编译器选项可以改变这一点。
  • @LưuVĩnhPhúc 如果 OP 正在执行 pow() 的数百 GB 管道,最好的解决方案似乎是将其移至 GPU 计算着色器。

标签: c++ c++11 pow


【解决方案1】:

您可以使用exponentiation by squaring 轻松编写自己的fpow

float my_fpow(float base, unsigned exp)
{
    float result = 1.f;
    while (exp)
    {
        if (exp & 1)
            result *= base;
        exp >>= 1;
        base *= base;
    }

    return result;
}


无聊的部分:

这个算法给出了最好的准确率,可以用float类型归档|base| > 1

证明:

让我们计算pow(a, n),其中a 是基数,n 是指数。
让我们定义 b1=a1, b2=a2, b3=a4, b4=a 8,以此类推。

那么 an 是所有此类 bi 的乘积,其中 ith sub> 位在 n 中设置。

所以我们订购了集合 B={bk1,bk1,...,bkn} em> 并且对于任何 j 位 kjn 中设置。

以下明显的算法A可用于最小化舍入误差:

  • 如果B包含单个元素,那么就是结果
  • B 中选择两个元素 pq 以最小模数
  • B 中删除它们
  • 计算积 s = p*q 并放入 B
  • 进入第一步

现在,让我们证明 B 中的元素可以从左到右相乘而不会失去准确性。事实证明:

bj > b1*b2*...*bj-1

因为 bj=bj-1*bj-1=bj-1 sub>*bj-2*bj-2=...=bj-1*bj-2 *...*b1*b1

因为,b1 = a1 = a 并且它的模数大于一:

bj > b1*b2*...*bj-1

因此我们可以得出结论,在从左到右的乘法过程中,累加器变量小于 B 中的任何元素。

然后,表达式result *= base;(肯定是第一次迭代除外)将B 中的两个最小数字相乘,因此舍入误差很小。因此,代码采用算法A

【讨论】:

  • 这将不如pow 的任何重载准确,并且您没有提供任何表明它比标准pow 更快的数字。
  • @hvd 但是,为什么?除了double result.,libstdc++ 做同样的事情
  • 因为这引入了额外的舍入:它在每个单独的乘法之后舍入。你是说额外的四舍五入肯定不会影响结果吗?如果是这样,如果您可以在答案中包含原因,我将很乐意删除我的反对票。但是当我使用随机数进行测试时,我确实看到了答案不同的情况。
  • @hvd 您假设 pow 函数应尽可能接近 a*a*a*...,其中 a 是实数。考虑到pow definition 应该做a*a*a*a... 其中a 是一个浮点数pow 应用于双精度并不会给出相同的结果这一事实相反并没有取消资格。实际上,其功能中唯一的错误来源是float multiplication is not associative
  • 你可以做的是使用double进行内部计算并转换回float。单独来说,double 通常不会比float 慢(除了东西划分)。 float double 转换速度很快,每个方向只发生一次。
【解决方案2】:

另一个只能用“错误问题”诚实回答的问题。或者至少:“你真的愿意去那里吗?”。 float 理论上需要大约。减少 80% 的裸片空间(对于相同的周期数),因此批量处理的成本要低得多。出于这个原因,GPU 喜欢 float

但是,让我们看看 x86(诚然,您没有说明您使用的是什么架构,所以我选择了最常见的)。模具空间的价格已经支付。通过使用float 进行计算,您实际上一无所获。实际上,您甚至可能失去吞吐量,因为需要从floatdouble 的额外扩展,并且需要额外舍入到中间float 精度。换句话说,您需要支付额外费用才能获得不太准确的结果。这通常是要避免的,除非您需要与其他程序最大程度地兼容。

也请参阅 Jens 的评论。这些选项允许编译器忽略某些语言规则以实现更高的性能。不用说,这有时会适得其反。

在 x86 上,float可能在两种情况下效率更高:

  • GPU(包括 GPGPU),事实上许多 GPU 甚至不支持double,如果支持,通常会慢得多。然而,只有在进行大量此类计算时才会注意到。
  • CPU SIMD 又名矢量化

如果您使用 GPGPU,您就会知道。使用编译器内在函数进行显式矢量化也是一种选择——当然,您可以做出这样的选择,但这需要进行相当多的成本效益分析。可能您的编译器能够自动矢量化某些循环,但这通常仅限于“显而易见的”应用程序,例如将vector<float> 中的每个数字乘以另一个float,这种情况在 IMO 中并不那么明显。即使您将pow 这样的向量中的每个数字都使用相同的int,编译器也可能不够聪明,无法有效地对其进行向量化,特别是如果pow 驻留在另一个翻译单元中,并且没有有效的链接时间代码生成。

如果您还没有准备好考虑更改程序的整个结构以允许有效使用 SIMD(包括 GPGPU),并且您不是在默认情况下float 确实便宜得多的架构上,我建议您一定要坚持使用double,并考虑float充其量一种可能有助于节省 RAM 或改善缓存局部性的存储格式(当你有很多时 em> 他们)。即便如此,测量也是一个绝妙的主意。

也就是说,您可以尝试使用 ivaigult 的算法(仅使用 double 作为中间体和结果),它与称为 Egyptian multiplication(以及其他各种名称)的经典算法有关,只是操作数相乘而不是相加。我不知道pow(double, double) 究竟是如何工作的,但可以想象这种算法在某些情况下可能会更快。同样,您应该对基准测试有强迫症。

【讨论】:

  • 我从doublefloat 的主要原因是RAM——我真的需要几十或几百GB,所以节省1/2 是巨大的优势。因为我经常打电话给pow,所以我需要确保float -> pow -> float 尽可能高效。
  • @Michal 因为你有 GB 的数据,你必须使用 SIMD。使用 AVX,您可以一次计算 8 个浮点数的功率(SSE 为 4 个,AVX-512 为 16 个)。 Some libraries for this。更好的选择是 GPGPU,因为它们的执行单元比 CPU 多得多
【解决方案3】:

如果你的目标是GCC,你可以试试

float __builtin_powif(float, int)

我不知道它的性能有多强。

【讨论】:

  • 很好,您找到了内置函数,但这不是处理 GB 数据的好方法
【解决方案4】:

是否有具有上述签名的特殊功能(在标准库或任何其他功能中)?

很遗憾,我不知道。


但是,正如许多人已经提到的,基准测试是必要的,以了解是否存在问题。

我已经组装了一个快速基准 online。基准代码:

#include <iostream>
#include <boost/timer/timer.hpp>
#include <boost/random/mersenne_twister.hpp>
#include <boost/random/uniform_real_distribution.hpp>
#include <cmath>

int main ()
{
    boost::random::mt19937 gen;
    boost::random::uniform_real_distribution<> dist(0, 10000000);

    const size_t size = 10000000;
    std::vector<float> bases(size);
    std::vector<float> fexp(size);
    std::vector<int> iexp(size);
    std::vector<float> res(size);

    for(size_t i=0; i<size; i++)
    {
        bases[i] = dist(gen);
        iexp[i] = std::floor(dist(gen));
        fexp[i] = iexp[i];
    }

    std::cout << "float pow(float, int):" << std::endl;
    {
        boost::timer::auto_cpu_timer timer;
        for(size_t i=0; i<size; i++)
            res[i] = std::pow(bases[i], iexp[i]);
    }

    std::cout << "float pow(float, float):" << std::endl;
    {
        boost::timer::auto_cpu_timer timer;
        for(size_t i=0; i<size; i++)
            res[i] = std::pow(bases[i], fexp[i]);
    }
    return 0;
}

基准测试结果(快速结论):

  • gcc: c++11 始终比 c++03 快。
  • clang:确实int-version of c++03 似乎快了一点。我不确定它是否在误差范围内,因为我只在线运行基准测试。
  • 两者:即使在 c++11 中调用 powint 似乎性能更高。

如果其他人能够验证这是否也适用于他们的配置,那就太好了。

【讨论】:

    【解决方案5】:

    尝试使用 powf() 代替。这是 C99 函数,在 C++11 中也应该可用。

    【讨论】:

    • powf 与他在问题中提到的 std::pow(float, float) 具有相同的签名,并且这个答案并没有真正提供任何证据,它可能比 std::pow(float , int)
    猜你喜欢
    • 2019-08-25
    • 2017-12-08
    • 1970-01-01
    • 2021-03-02
    • 1970-01-01
    • 1970-01-01
    • 2011-10-11
    • 1970-01-01
    • 2021-07-19
    相关资源
    最近更新 更多