【问题标题】:Rounding integers routine舍入整数例程
【发布时间】:2016-03-01 07:56:14
【问题描述】:

教程中的整数运算让我感到困惑。准确的说是整数除法。

看似首选的方法是将除数转换为浮点数,然后将浮点数四舍五入为最接近的整数,然后再将其转换回整数:

#include <cmath>

int round_divide_by_float_casting(int a, int b){
    return  (int) std::roundf( a / (float) b);
}

然而,这似乎是用右手挠左耳。我用:

int round_divide (int a, int b){
    return a / b + a % b * 2 / b;
}

这不是突破,但它不标准的事实让我怀疑我是否遗漏了什么?

尽管我进行了(尽管有限)测试,但我找不到任何两种方法给我不同结果的场景。有人遇到过 int → float → int 转换产生更准确结果的情况吗?

【问题讨论】:

  • 对我来说,第一个要清楚得多。我知道它想做什么。我知道它是如何做到的。如果没有在纸上运行它,我不知道第二个在做什么。我也提示第一个更快,因为第二个做了几个算术运算
  • 我同意清晰很重要。但至于操作复杂性(和速度),我不确定。

标签: c++ math integer rounding


【解决方案1】:

算术解

如果有人定义了你的函数应该返回什么,她会将其描述为“f(a, b) 返回在实际除数环中a 除以b 的最接近的整数。”

因此,问题可以概括为:我们能否仅使用整数除法来定义这个最接近的整数。我想我们可以。

恰好有两个候选者作为最接近的整数a / b(a / b) + 1(1)。选择很简单,如果a % b 更接近0b,那么a / b 就是我们的结果。如果不是,(a / b) + 1 是。

然后可以编写类似的东西,忽略优化和良好实践:

int divide(int a, int b)
{
    const int quot = a / b;
    const int rem = a % b;
    int result;

    if (rem < b - rem) {
        result = quot;
    } else {
        result = quot + 1;
    }
    return result;
}

虽然此定义满足了需求,但可以通过使用 std::div() 计算 a 除以 b 的两倍来优化它:

int divide(int a, int b)
{
    const std::div_t dv = std::div(a, b);
    int result = dv.quot;

    if (dv.rem >= b - dv.rem) {
        ++result;
    }
    return result;
}

我们之前对问题所做的分析向我们保证了我们实现的明确定义的行为。

(1)只有最后一件事要检查:当ab 为负时,它的行为如何?这留给读者;)。

基准测试

#include <iostream>
#include <iomanip>
#include <string>

// solutions
#include <cmath>
#include <cstdlib>

// benchmak
#include <limits>
#include <random>
#include <chrono>
#include <algorithm>
#include <functional>

//
// Solutions
//
namespace
{
    int round_divide_by_float_casting(int a, int b) {
        return  (int)roundf(a / (float)b);
    }

    int round_divide_by_modulo(int a, int b) {
        return a / b + a % b * 2 / b;
    }

    int divide_by_quotient_comparison(int a, int b)
    {
        const std::div_t dv = std::div(a, b);
        int result = dv.quot;

        if (dv.rem >= b - dv.rem)
        {
            ++result;
        }
        return result;
    }
}

//
// benchmark
//
class Randomizer
{
    std::mt19937 _rng_engine;
    std::uniform_int_distribution<int> _distri;

public:
    Randomizer() : _rng_engine(std::time(0)), _distri(std::numeric_limits<int>::min(), std::numeric_limits<int>::max())
    {
    }

    template<class ForwardIt>
    void operator()(ForwardIt begin, ForwardIt end)
    {
        std::generate(begin, end, std::bind(_distri, _rng_engine));
    }
};

class Clock
{
    std::chrono::time_point<std::chrono::steady_clock> _start;

public:
    static inline std::chrono::time_point<std::chrono::steady_clock> now() { return std::chrono::steady_clock::now(); }

    Clock() : _start(now())
    {
    }

    template<class DurationUnit>
    std::size_t end()
    {
        return std::chrono::duration_cast<DurationUnit>(now() - _start).count();
    }
};

//
// Entry point
//
int main()
{
    Randomizer randomizer;
    std::array<int, 1000> dividends; // SCALE THIS UP (1'000'000 would be great)
    std::array<int, dividends.size()> divisors;
    std::array<int, dividends.size()> results;
    randomizer(std::begin(dividends), std::end(dividends));
    randomizer(std::begin(divisors), std::end(divisors));

    {
        Clock clock;
        auto dividend = std::begin(dividends);
        auto divisor = std::begin(divisors);
        auto result = std::begin(results);
        for ( ; dividend != std::end(dividends) ; ++dividend, ++divisor, ++result)
        {
            *result = round_divide_by_float_casting(*dividend, *divisor);
        }
        const float unit_time = clock.end<std::chrono::nanoseconds>() / static_cast<float>(results.size());
        std::cout << std::setw(40) << "round_divide_by_float_casting(): " << std::setprecision(3) << unit_time << " ns\n";
    }
    {
        Clock clock;
        auto dividend = std::begin(dividends);
        auto divisor = std::begin(divisors);
        auto result = std::begin(results);
        for ( ; dividend != std::end(dividends) ; ++dividend, ++divisor, ++result)
        {
            *result = round_divide_by_modulo(*dividend, *divisor);
        }
        const float unit_time = clock.end<std::chrono::nanoseconds>() / static_cast<float>(results.size());
        std::cout << std::setw(40) << "round_divide_by_modulo(): " << std::setprecision(3) << unit_time << " ns\n";
    }
    {
        Clock clock;
        auto dividend = std::begin(dividends);
        auto divisor = std::begin(divisors);
        auto result = std::begin(results);
        for ( ; dividend != std::end(dividends) ; ++dividend, ++divisor, ++result)
        {
            *result = divide_by_quotient_comparison(*dividend, *divisor);
        }
        const float unit_time = clock.end<std::chrono::nanoseconds>() / static_cast<float>(results.size());
        std::cout << std::setw(40) << "divide_by_quotient_comparison(): " << std::setprecision(3) << unit_time << " ns\n";
    }
}

输出

g++ -std=c++11 -O2 -Wall -Wextra -Werror main.cpp && ./a.out
       round_divide_by_float_casting(): 54.7 ns
              round_divide_by_modulo(): 24 ns
       divide_by_quotient_comparison(): 25.5 ns

Demo

这两种算术解决方案的性能无法区分(当您扩大工作台大小时,它们的基准会收敛)。

【讨论】:

  • 您好,感谢您的建议。这比 int->float->int 转换要快,但比我发布的模运算要慢。
  • @AdlA 当启用优化时,一个好的编译器可能会生成类似的程序集。区别在于可读性和人们在证明它行为正确时可以找到的容易程度。
  • 我明白你的意思。本质上 a % b * 2 / b 返回 01 ,不费吹灰之力就能看到它等同于 if (rem &gt; b - rem) {return quot;} else {return quot + 1;}。它的性能差异必须在其他地方
  • 第一个代码 sn-p 只是作为描述给出。您应该将您的算术解决方案与divide(int,int) 的第二个定义进行比较。
  • 我就是这么做的 :)
【解决方案2】:

这实际上取决于处理器,以及更好的整数范围(使用double 可以解决大部分范围问题)

对于像 x86-64 和 ARM 这样的现代“大”CPU,整数除法和浮点除法大致相同,将整数转换为浮点数或反之亦然并不是一项“硬”任务(并且正确至少在该转换中直接舍入),因此很可能是结果运算。

atmp = (float) a;
btmp = (float) b;
resfloat = divide atmp/btmp;
return = to_int_with_rounding(resfloat)

大约四个机器指令。

另一方面,您的代码使用两个除法,一个取模和一个乘法,在这样的处理器上很可能更长。

tmp = a/b;
tmp1 = a % b;
tmp2 = tmp1 * 2;
tmp3 = tmp2 / b;
tmp4 = tmp + tmp3;

所以 5 条指令,其中 3 条是“除法”(除非编译器足够聪明,可以将 a / b 重用于 a % b - 但它仍然是两个不同的除法)。

当然,如果您超出浮点数或双精度数可以保持而不会丢失数字的位数范围(浮点数为 23 位,双精度数为 53 位),那么您的方法可能会更好(假设没有溢出在整数数学中)。

最重要的是,由于“每个人”都使用第一种形式,它是编译器识别并可以优化的形式。

显然,结果取决于所使用的编译器及其运行的处理器,但这些是我运行上面发布的代码的结果,通过clang++ 编译(v3.9-pre-release,非常接近发布 3.8)。

   round_divide_by_float_casting(): 32.5 ns
          round_divide_by_modulo(): 113 ns
   divide_by_quotient_comparison(): 80.4 ns

但是,当我查看生成的代码时,我发现了一件有趣的事情:

xorps   %xmm0, %xmm0
cvtsi2ssl   8016(%rsp,%rbp), %xmm0
xorps   %xmm1, %xmm1
cvtsi2ssl   4016(%rsp,%rbp), %xmm1
divss   %xmm1, %xmm0
callq   roundf
cvttss2si   %xmm0, %eax
movl    %eax, 16(%rsp,%rbp)
addq    $4, %rbp
cmpq    $4000, %rbp             # imm = 0xFA0
jne .LBB0_7

round实际上是一个电话。这真的让我感到惊讶,但解释了为什么在某些机器上(尤其是更新的 x86 处理器),它更快。

g++-ffast-math 一起提供更好的结果,它给出了:

  round_divide_by_float_casting(): 17.6 ns
          round_divide_by_modulo(): 43.1 ns
   divide_by_quotient_comparison(): 18.5 ns

(这是计数增加到 100k 值)

【讨论】:

  • 感谢您的解释。我继续进行了一些测试。在我使用 vs2015 的机器(i7)上,模运算的速度大约是原来的两倍。 roundf()里面一定有一些隐藏的操作
  • 感谢您更新您的答案。非常有趣的是,在您的情况下,模算术除法实际上是最慢的。在我所有的基准测试中 - 似乎还有@YSC 制定的基准 - 这是通过铸造圆形浮点数进行的划分,这是最慢的(对你来说是最快的)。我是 C++ 和编译器优化等方面的新手,但我发现性能波动的幅度之大令人着迷。有一天能理解为什么会很棒......干杯!
【解决方案3】:

首选标准溶液。使用在 cstdlib 中声明的 std::div 系列函数。

见:http://en.cppreference.com/w/cpp/numeric/math/div

在某些架构(例如微控制器)上,先转换为浮点数然后转换为整数可能效率很低。

【讨论】:

  • 感谢您的建议。我知道有很多已经制定的例程来完成这样的基本任务。但我的问题是通过强制转换(流行的方式)是否比模算术(不流行的方式)更可靠和/或更快
  • 只是补充一点,经过一些测试,这比 int->float->int 转换要快,但比我发布的模运算要慢。
  • 基准测试可能与硬件架构非常相关。在大多数架构中,您的模数解决方案可能比强制转换方式更快。
【解决方案4】:

感谢到目前为止的建议。为了阐明一些观点,我做了一个测试设置来比较性能。

#include <iostream>
#include <string>
#include <cmath>
#include <cstdlib>
#include <chrono>

using namespace std;

int round_divide_by_float_casting(int a, int b) {
    return  (int)roundf(a / (float)b);
}

int round_divide_by_modulo(int a, int b) {
    return a / b + a % b * 2 / b;
}

int divide_by_quotient_comparison(int a, int b)
{
    const std::div_t dv = std::div(a, b);
    int result = dv.quot;

    if (dv.rem <= b - dv.rem) {
        ++result;
    }
    return result;
}

int main()
{
    int itr = 1000;

    //while (true) {
        auto begin = chrono::steady_clock::now();
        for (int i = 0; i < itr; i++) {
            for (int j = 10; j < itr + 1; j++) {
                divide_by_quotient_comparison(i, j);
            }
        }
        auto end = std::chrono::steady_clock::now();
        cout << "divide_by_quotient_comparison(,) function took: "
             << chrono::duration_cast<std::chrono::nanoseconds>(end - begin).count()
             << endl;

        begin = chrono::steady_clock::now();
        for (int i = 0; i < itr; i++) {
            for (int j = 10; j < itr + 1; j++) {
                round_divide_by_float_casting(i, j);
            }
        }
        end = std::chrono::steady_clock::now();
        cout << "round_divide_by_float_casting(,) function took: "
             << chrono::duration_cast<std::chrono::nanoseconds>(end - begin).count()
             << endl;

        begin = chrono::steady_clock::now();
        for (int i = 0; i < itr; i++) {
            for (int j = 10; j < itr + 1; j++) {
                round_divide_by_modulo(i, j);
            }
        }
        end = std::chrono::steady_clock::now();
        cout << "round_divide_by_modulo(,) function took: "
             << chrono::duration_cast<std::chrono::nanoseconds>(end - begin).count()
             << endl;
    //}

    return 0;
}

我在我的机器(i7 和 Visual Studio 2015)上得到的结果如下:模运算的速度大约是 int → float → int 转换方法的两倍。依赖 std::div_t 的方法(@YSC 和 @teroi 建议)比 int → float → int 快,但比模运算方法慢。

为了避免@YSC 指出的某些编译器优化,我们进行了第二次测试:

#include <iostream>
#include <string>
#include <cmath>
#include <cstdlib>
#include <chrono>
#include <vector>
using namespace std;

int round_divide_by_float_casting(int a, int b) {
    return  (int)roundf(a / (float)b);
}

int round_divide_by_modulo(int a, int b) {
    return a / b + a % b * 2 / b;
}

int divide_by_quotient_comparison(int a, int b)
{
    const std::div_t dv = std::div(a, b);
    int result = dv.quot;

    if (dv.rem <= b - dv.rem) {
        ++result;
    }
    return result;
}

int main()
{
    int itr = 100;
    vector <int> randi, randj;
    for (int i = 0; i < itr; i++) {
        randi.push_back(rand());
        int rj = rand();
        if (rj == 0)
            rj++;
        randj.push_back(rj);
    }
    vector<int> f, m, q;

    while (true) {
        auto begin = chrono::steady_clock::now();
        for (int i = 0; i < itr; i++) {
            for (int j = 0; j < itr; j++) {
                q.push_back( divide_by_quotient_comparison(randi[i] , randj[j]) );
            }
        }
        auto end = std::chrono::steady_clock::now();
        cout << "divide_by_quotient_comparison(,) function took: "
             << chrono::duration_cast<std::chrono::nanoseconds>(end - begin).count()
             << endl;

        begin = chrono::steady_clock::now();
        for (int i = 0; i < itr; i++) {
            for (int j = 0; j < itr; j++) {
                f.push_back( round_divide_by_float_casting(randi[i], randj[j]) );
            }
        }
        end = std::chrono::steady_clock::now();
        cout << "round_divide_by_float_casting(,) function took: "
             << chrono::duration_cast<std::chrono::nanoseconds>(end - begin).count()
             << endl;

        begin = chrono::steady_clock::now();
        for (int i = 0; i < itr; i++) {
            for (int j = 0; j < itr; j++) {
                m.push_back( round_divide_by_modulo(randi[i], randj[j]) );
            }
        }
        end = std::chrono::steady_clock::now();
        cout << "round_divide_by_modulo(,) function took: "
             << chrono::duration_cast<std::chrono::nanoseconds>(end - begin).count()
             << endl;
        cout << endl;

        f.clear();
        m.clear();
        q.clear();
    }

    return 0;
}

在第二次测试中,最慢的是依赖 std::div_tdivide_by_quotient(),其次是 divide_by_float(),最快的是 divide_by_modulo()。但是,这一次的性能差异要小得多,不到 20%。

【讨论】:

  • 编译器推荐行好吗?
  • 您的基准测试存在严重缺陷:编译器太聪明:它重新排列循环顺序并优化无副作用的相同计算。您应该尝试使用随机数据。
  • 感谢您的建议。您是否设法尝试使用随机数据?我会尽快尝试
  • 在第二个测试中,最慢的是依赖于 std::div_tdivide_by_quotient(),其次是依赖于 roundf()divide_by_float() ,最快的又是divide_by_modulo()。但是这一次,最慢和最快之间的性能差异要低得多,不到 20%。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-05-13
  • 2013-04-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-03-11
相关资源
最近更新 更多