为什么Java的Math.min在我的Android应用程序中如此之慢?

时间:2015-02-10 23:48:35

标签: java optimization micro-optimization

我有一些我正在分析的代码,并对Math.min(float, float)花了多少时间感到惊讶。

在我的用例中,我需要得到3个浮点值的最小值,每个值保证不是NAN或其他边缘情况浮点值。

我原来的方法是:

private static float min2(float v1, float v2, float v3) {
    return Math.min(Math.min(v1,v2),v3);
}

但我发现这大约快了5倍:

private static float min1(float v1, float v2, float v3) {
    if (v1 < v2 && v1 < v3) {
        return v1;
    }
    else if (v2 < v3) {
        return v2;
    }
    else {
        return v3;
    }
}

作为参考,这是Math.min的代码:

public static float min(float f1, float f2) {
    if (f1 > f2) {
        return f2;
    }
    if (f1 < f2) {
        return f1;
    }
    /* if either arg is NaN, return NaN */
    if (f1 != f2) {
        return Float.NaN;
    }
    /* min(+0.0,-0.0) == -0.0 */
    /* 0x80000000 == Float.floatToRawIntBits(-0.0f) */
    if (Float.floatToRawIntBits(f1) == 0x80000000) {
        return -0.0f;
    }
    return f2;
}

注意:我的用例是对称的,以上对于max而不是min来说都是正确的。

EDIT1: 事实证明~5倍是夸大其词,但我仍然看到我的应用程序中的速度差异。虽然我怀疑可能是由于没有进行适当的时间测试。

发布此问题后,我在一个单独的项目中编写了一个适当的微优化速度测试。在随机浮点上测试每个方法1000次,它们都花费相同的时间。我不认为发布该代码会有用,因为它只是确认了我们所有人已经想到的内容。

必须有一些特定的项目,我正在努力导致速度差异。

我正在Android应用程序中做一些图形工作,而我正在从3个触摸事件中找到值的最小值/最大值。同样,像-0.0f这样的边缘情况和不同的无穷大这里不是问题。值介于0.0f和3000f之间。

最初我使用Android设备监视器的方法分析工具来分析我的代码,它确实显示了~5倍的差异。但是,这不是我现在所学习的微观代码的最佳方式。

我在我的应用程序中添加了以下代码,以尝试获得更好的数据:

long min1Times = 0L;
long min2Times = 0L;
...
// loop assigning touch values to v1, v2, v3
        long start1 = System.nanoTime();
        float min1 = min1(v1, v2, v3);
        long end1 = System.nanoTime();
        min1Times += end1 - start1;
        long start2 = System.nanoTime();
        float min2 = min2(v1, v2, v3);
        long end2 = System.nanoTime();
        min2Times += end2 - start2;
        double ratio = (double) (min1Times) / (double) (min2Times);
        Log.d("", "ratio: " + ratio);

这会打印每个新触摸事件的运行比率。当我在屏幕上旋转手指时,记录的第一个比率为0.0InfinityNaN。这让我觉得这个测试不是很准确地测量时间。随着收集的数据越来越多,该比率往往会在.851.15之间变化。

3 个答案:

答案 0 :(得分:1)

内置 Math.min 函数的性能问题源于制定 IEEE-754 标准时做出的一些不幸的回顾性决定,尤其是比较和关系运算符的行为。指定的行为适用于某些用途,但不适用于其他一些常见用途。

最值得注意的是,如果计算 x 会产生一个太小而无法表示的正数,而计算 y 会产生一个太小而无法表示的负数,那么就无法直接比较这两个值,它们'不等价。 1.0/x 的计算将被解释为除以无穷小的正数,而“1.0/y”表现为除以无穷小的负数。因此,即使 xy 是无穷小,并且足够接近以至于比较和关系运算符将它们报告为相等,Math.min(x,y)Math.min(y,x) 都应该返回 {{1} } 因为它比 y 小很多。

让我感到疯狂的是,人们仍在设计硬件和编程语言,它们缺乏任何比较浮点值的好方法,以至于所有彼此不完全等效的值对都将是可传递的排名,但不幸的是,这影响了过去几十年浮点数学的状态。如果需要一个函数,在 xx 表示具有非零差异的数字的情况下返回最小值,否则任意返回 y 或 {{1} },这样的函数可以比必须处理涉及正无穷小值和负无穷小值的棘手情况更有效地编写。

答案 1 :(得分:0)

您的实现应该会导致一些非常紧凑的字节码,这很容易被JIT编译器转换为同样快速的汇编语言。使用Math.min的版本有两个子程序调用,因此可能不像你的那样内联。我认为结果是可以预期的。

答案 2 :(得分:0)

问题在于浮点值的精度。

如果您使用参数(0.0f, -0.0f, 0.0f)调用您的方法,它将使您0.0f返回最小的浮点数 - 它不是(浮点数,-0.0f更小)

嵌套的Min-Method将返回预期的结果。

所以,回答你的问题:如果两种方法不是100%相等 - 没有必要比较它们的表现: - )

Java将0.0f == -0.0f处理为true,new Float(0.0)).equals(new Float(-0.0))将为falseMath.Min会考虑这个,你的方法不会。

使用浮点值时,不应使用小于或等于运算符。相反,您应该根据预先选择的增量来比较数字,以将它们视为更小,更大或相等。

float delta = 0.005
if (Math.abs(f1 - f2) < delta) //Consider them equal.
if (Math.abs(f1 - f2) > delta) // not equal. 

这就是Math.min方法结束时发生的事情 - 通过实际检查一个数字是-0.0f - 按位,以非常精确的方式进行。

因此,性能上的缺点只是计算得越精确。

但是,如果你比较像“10”,“5”和“8”这样的浮动值 - 那么不应该有性能差异,因为永远不会命中0检查。