【问题标题】:Multiply+Divide using just Multiply+Shift (32 bit)Multiply+Divide 仅使用 Multiply+Shift(32 位)
【发布时间】:2016-10-05 11:55:05
【问题描述】:

我想知道做比例计算最快的方法,就是y = x * a / b,这里所有的值都是32位的,无符号的,ab是固定的(初始化一次,然后不更改)但在编译时未知。结果保证不会溢出(即使中间乘法可能需要 64 位)。编程语言无关紧要,但 Java 最适合我的情况。它需要尽可能快(纳秒很重要)。我目前使用:

int result = (int) ((long) x * a / b);

但是除法很慢。我知道Perform integer division using multiplication,所以最好是一个类型的公式:

int result = (int) (((long) x * factor) >>> shift);

其中factorshift 可以从ab 计算(计算可能很慢)。

我试图简单地替换原始公式的除法部分,但它不起作用,因为两次乘法的结果不适合 64 位:

// init
int shift = 63 - Integer.numberOfLeadingZeros(b);
int factor = ((1L << shift) / b) + 1;
...
// actual calculation
int result = (int) ((long) x * a * factor) >>> shift);

在我的情况下,结果实际上不必完全准确(减一即可)。

【问题讨论】:

  • 我现在有一个(部分)解决方案:shift = 16; factor = ((long) a &lt;&lt; 16) / b + 1; - 但它只适用于某些值范围。如果可能的话,最好有一个适用于所有无符号 32 位值的通用解决方案。
  • 鉴于您知道具有不变除数的整数除法,尚不清楚您卡在哪里。鉴于a, b, x 都是uint32_t,计算无符号uint64_t 乘积x*a,然后对其应用带有常数除数b 的64 位除法。如果b 是编译时常量,只需使用除法运算符并让编译器对其进行优化。最后,将uint64_t 商转换回uint32_t(根据问题中的规范,这可以保证构造成功,无需额外测试)。
  • @njuffa 不幸的是,这些值在编译时是未知的。我一直在努力寻找一个优雅的解决方案,保证边缘情况的行为(a、b 和 x 的高值)。我想我现在已经解决了。

标签: java optimization bit-manipulation division multiplication


【解决方案1】:

怎么样

long a2 = a & 0xFFFFFFFFL;
long b2 = b & 0xFFFFFFFFL;
checkArgument(b2 > 0);
double dFactor = (double) a2 / b2;
shift = 0;
while (dFactor < 1L<<32) {
   dFactor *= 2;
   shift++;
}
factor = (long) dFactor;

准备和

int result = (int) (((x & 0xFFFFFFFFL) * factor) >>> shift);

对于快速部分?现在我们有了2**32 &lt;= factor &lt; 2**33,对于任何int x &gt;= 0,产品x * factor &lt; 2**31 * 2**33 = 2**64 正好适合unsigned long。没有比特被浪费。 dFactorlong 的转换向下舍入,这可能不是最理想的。


准备工作肯定会加快,尤其是可以通过先查看前导零来消除循环。我不会为消除 double 而烦恼,因为它使事情变得简单。

【讨论】:

  • 工作正常(a = 0 除外,在这种情况下会出现无限循环)。多亏了您的解决方案,我找到了一个更简单的解决方案,它具有固定的移位,并且不使用浮点。
  • @ThomasMueller 恕我直言,任何固定班次都会导致精度下降。消除double 听起来很容易,但我不确定这是否值得,因为除法速度很慢并且使用浮点数变化不大。
【解决方案2】:

我认为如果使用公式(x * factor) &gt;&gt;&gt; shift,不可能总是得到准确的结果:对于某些极端情况,结果要么是 1 太低,要么是 1 太高。为了总能得到正确的结果,公式需要更复杂。我找到了一个不需要浮点的解决方案,这里是一个测试用例:

static final Set<Integer> SOME_VALUES = new TreeSet<Integer>();

static {
    Set<Integer> set = SOME_VALUES;
    for (int i = 0; i < 100; i++) {
        set.add(i);
    }
    set.add(Integer.MAX_VALUE);
    set.add(Integer.MAX_VALUE - 1);
    for (int i = 1; i > 0; i += i) {
        set.add(i - 1);
        set.add(i);
        set.add(i + 1);
    }
    for (int i = 1; i > 0; i *= 3) {
        set.add(i);
    }
    Random r = new Random(1);
    for (int i = 0; i < 100; i++) {
        set.add(r.nextInt(Integer.MAX_VALUE));
    }
}

private static void testMultiplyDelete() {
    for (int a : SOME_VALUES) {
        for (int b : SOME_VALUES) {
            if (b == 0) {
                continue;
            }
            int shift = 32;
            // sometimes 1 too low
            long factor = (1L << shift) * a / b;
            // sometimes 1 too high
            // long factor = ((1L << shift) * a / b) + 1;

            // sometimes 1 too low
            // double dFactor = (double) a / b;
            // int shift = 0;
            // while (dFactor > 0 && dFactor < (1L << 32)) {
            //     dFactor *= 2;
            //     shift++;
            // }
            // long factor = (long) dFactor;

            for (int x : SOME_VALUES) {
                long expectedResult = (long) x * a / b;
                if (expectedResult < 0 ||
                        expectedResult >= Integer.MAX_VALUE) {
                    continue;
                }
                int result = (int) ((x * factor) >>> shift);
                if (Math.abs(result - expectedResult) > 1) {
                    System.out.println(x + "*" + a + "/" + b +
                            "=" + expectedResult + "; " +
                            "(" + x + "*" + factor + ")>>>" + shift + "=" + result);
                }
            }
        }
    }
}

【讨论】:

    【解决方案3】:

    由于ab 都是固定的,您可以只进行一次除法并重复使用结果(这可能已经在幕后自动发生):

    int c = a / b;
    int y1 = x1 * c;
    int y2 = x2 * c;
    ...
    

    如果您确实需要优化它,请考虑在GPU 上运行它(例如使用CUDA 的java 绑定),这将允许您并行计算,尽管这更难实现。

    最后,在测试时添加计时器总是一个好主意,这样您就可以运行基准测试以确保优化确实提高了性能。

    【讨论】:

    • int 不像数学数字 - 特别是,您不能先进行除法并期望得到相同的结果。例如,如果 a 小于 b,您的代码将始终生成零,而不是正确缩放的数量。
    猜你喜欢
    • 2017-09-06
    • 2014-02-08
    • 1970-01-01
    • 2012-05-11
    • 2021-08-31
    • 2022-12-27
    • 2014-10-03
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多