这是否意味着Java的Math.floor非常慢?

19

我不怎么会Java。

我正在编写一些优化的数学代码,但我的分析器结果让我感到震惊。我的代码收集值,交错数据,然后根据此选择值。Java比我的C++和MATLAB实现运行得更慢。

我正在使用javac 1.7.0_05 和 Sun/Oracle JDK 1.7.05。

代码中存在一个执行相关任务的floor函数。 java math.floor profile results

  1. 有人知道修复这个问题的典型方法吗?
  2. 我注意到我的floor()函数是用一种叫做StrictMath的东西定义的。是否有类似于-ffast-math的东西可以用于Java?我期望能找到一种更加计算合理的方法来改变floor函数,而不用自己编写它。

    public static double floor(double a) {
        return StrictMath.floor(a); // default impl. delegates to StrictMath
    }
    

编辑

有几个人建议我尝试进行转换。 我尝试了这个,但墙上的时间完全没有改变。

private static int flur(float dF)
{
    return (int) dF;
}

413742 转型 floor 函数

394675 Math.floor

这些测试是在没有分析器的情况下进行的。尽管曾经尝试使用分析器,但运行时间发生了显著变化(超过15分钟,因此我放弃了)。


2
如果您必须使用StrictMath.*(通常比更准确的Math.*慢) - 如果需要对floor进行重复计算,则可以将结果缓存到映射中。 - Nishant
5
您能否在代码周围提供更多的背景信息?调用了多少次 Math.floor() 函数?您处理哪种类型的数字?另外,C++ 或 MATLAB 在数学运算方面更快并不奇怪。 - posdef
1
@Nishant,OP指出Math.floor()委托给了StrictMath.floor(),请参见:http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/6-b14/java/lang/Math.java#Math.floor%28double%29 - posdef
@posdef 嘿,感谢你指出这一点。我看到 StrictMath 使用本地方法 -- public static native double floor(double a); - Nishant
1
即使在C/C++中, floor() 被认为非常慢。并不是因为取整操作本身很慢,而是因为需要检查和处理许多边角情况。虽然有位运算技巧可以使其极其快速,但仍需注意许多细节。SSE4.1指令集添加了原生的舍入操作指令,包括 floor()。但要访问这些指令,需要使用具有编译器内部函数或者汇编语言的C/C++。 - Mysticial
显示剩余2条评论
6个回答

8
你可能想尝试FastMath
这是一篇关于Java和Javascript中Math性能的文章performance of Math in Java vs. Javascript。其中有一些关于为什么默认数学库很慢的好提示。他们讨论的不仅仅是floor操作,但我猜他们的发现可以泛化。我觉得这很有趣。 编辑 根据此错误记录,在7(b79),6u21(b01)中实现了纯Java代码的floor,导致性能更好。JDK 6中的floor代码仍然比FastMath中的代码长一些,但可能不会导致这样的性能下降。您使用的是哪个JDK?您可以尝试使用更高版本的JDK吗?

我正在使用Sun/Oracle JDK 1.7.05,运行在一台i3 Gentoo系统上。 - Mikhail

6

以下是您假设代码真正花费99%时间在floor函数的合理性检查。 假设您有Java和C ++版本的算法,它们在产生输出方面都是正确的。 为了论证,让我们假设这两个版本调用相同次数的等效floor函数。 因此,时间函数为

t(input) = nosFloorCalls(input) * floorTime + otherTime(input)

floorTime是在平台上调用floor函数所需的时间。

如果你的假设是正确的,且在Java平台上floorTime比C++平台更加昂贵(以至于它占据了大约99%的执行时间),那么你会发现Java版本的应用程序运行速度比C++版本慢很多倍(50倍或更多)。如果你没有看到这一点,那么你的假设很可能是错误的。


如果假设是错误的,则有两种替代解释来解释分析结果。

  1. 这是一种测量异常;即分析器出现了错误。尝试使用另一个分析器。

  2. 在Java版本的代码中存在一个bug,导致它调用floor的次数比C++版本的代码多得多。


检查对 floor() 调用的次数也是一个很好的措施,以查看是否有比预期更多的方法调用。 - posdef
1
问题通过使用强制类型转换得到解决,但最初的问题涉及分析分析器结果。有趣的是,所有优化的C++版本运行速度约为13倍。最后,当使用强制类型转换时,ArrayList.add函数所占用的时间比预期多了66%。 - Mikhail

5

Math.floor()在我的电脑上非常快,每次调用只需大约7纳秒(Windows 7,Eclipse,Oracle JDK 7)。我希望它在几乎所有情况下都非常快,并且如果它成为瓶颈,我会感到非常惊讶。

一些想法:

  • 我建议重新运行一些基准测试没有运行分析器。有时,在二进制代码中插入分析器会产生虚假的开销-特别是对于像Math.floor()这样可能被编译器内联的小函数。
  • 尝试几种不同的JVM,您可能已经遇到了一个晦涩的错误
  • 尝试使用绝佳的Apache Commons Math库中的FastMath类,其中包括floor的新实现。我会真的很惊讶,如果它比Math.floor()更快,但你永远不知道。
  • 检查您是否正在运行任何虚拟化技术或类似技术,这可能会干扰Java调用本机代码的能力(在一些java.lang.Math函数中使用,包括Math.floor()

1
我认为自JDK 7(b79)和6u21(b01)以来,他们改进了floor的性能。请参见http://bugs.sun.com/view_bug.do?bug_id=6908131。 - ewernli

4
首先:您的分析器显示您99%的CPU时间都在floor函数中。这并不表示floor很慢。如果您只是使用floor(),那么完全没有问题。由于其他语言似乎更有效地实现了floor,因此您的假设可能是正确的。
我从学校知道,可以通过将数字强制转换为整数/长整型来进行floor的天真实现(仅适用于正数,并且负数会少1)。这是与语言无关的通识知识,来自CS课程。
以下是一些微型基准测试。在我的机器上工作,并支持我在学校所学到的东西;)
rataman@RWW009 ~/Desktop
$ javac Cast.java && java Cast
10000000 Rounds of Casts took 16 ms

rataman@RWW009 ~/Desktop
$ javac Floor.java && java Floor
10000000 Rounds of Floor took 140 ms

public class Cast/Floor {

    private static final int ROUNDS = 10000000;

    public static void main(String[] args)
    {
        double[] vals = new double[ROUNDS];
        double[] res = new double[ROUNDS];

        // awesome testdata
        for(int i = 0; i < ROUNDS; i++)
        {
            vals[i] = Math.random() * 10.0;
        }

        // warmup
        for(int i = 0; i < ROUNDS; i++)
        {
            res[i] = floor(vals[i]);
        }

        long start = System.currentTimeMillis();
        for(int i = 0; i < ROUNDS; i++)
        {
            res[i] = floor(vals[i]);
        }
        System.out.println(ROUNDS + " Rounds of Casts took " + (System.currentTimeMillis() - start) +" ms");
    }

    private static double floor(double arg)
    {
        // Floor.java
        return Math.floor(arg);
        // or Cast.java
        return (int)arg;
    }

}


我无法确定它是否是两倍快或11倍快。他必须自己对其进行分析。注意不要用完问号! :) - atamanroman
哈哈 :)... 我会记住的。但是,你的回答基于不确定性,答案变成了一个建议而没有确定性。 - Starx
2
你真的应该用可验证的研究来支持你的答案,否则它很可能会被踩或者可能被删除。 - Tim Post
已编辑以使答案更完整。 - atamanroman

4
值得注意的是,监视一个方法会带来一些开销,在VisualVM中尤其如此。如果你有一个被频繁调用但实际操作很少的方法,它可能会看起来占用大量CPU资源,例如我曾经看到过Integer.hashCode()的CPU使用率非常高;)
在我的机器上,一个floor操作需要不到5.6纳秒,而一个cast操作只需要2.3纳秒。你也可以在你的机器上试一下。
除非你需要处理特殊情况,否则使用普通的cast操作速度更快。
// Rounds to zero, instead of Negative infinity.
public static double floor(double a) {
    return (long) a;
}

public static void main(String... args) {
    int size = 100000;
    double[] a = new double[size];
    double[] b = new double[size];
    double[] c = new double[size];
    for (int i = 0; i < a.length; i++) a[i] = Math.random()  * 1e6;

    for (int i = 0; i < 5; i++) {
        timeCast(a, b);
        timeFloor(a, c);
        for (int j = 0; j < size; j++)
            if (b[i] != c[i])
                System.err.println(a[i] + ": " + b[i] + " " + c[i]);
    }
}

public static double floor(double a) {
    return a < 0 ? -(long) -a : (long) a;
}

private static void timeCast(double[] from, double[] to) {
    long start = System.nanoTime();
    for (int i = 0; i < from.length; i++)
        to[i] = floor(from[i]);
    long time = System.nanoTime() - start;
    System.out.printf("Cast took an average of %.1f ns%n", (double) time / from.length);
}

private static void timeFloor(double[] from, double[] to) {
    long start = System.nanoTime();
    for (int i = 0; i < from.length; i++)
        to[i] = Math.floor(from[i]);
    long time = System.nanoTime() - start;
    System.out.printf("Math.floor took an average of %.1f ns%n", (double) time / from.length);
}

打印
Cast took an average of 62.1 ns
Math.floor took an average of 123.6 ns
Cast took an average of 61.9 ns
Math.floor took an average of 6.3 ns
Cast took an average of 47.2 ns
Math.floor took an average of 6.5 ns
Cast took an average of 2.3 ns
Math.floor took an average of 5.6 ns
Cast took an average of 2.3 ns
Math.floor took an average of 5.6 ns

谢谢您的建议。我尝试过类似的方法,但并没有帮助。 - Mikhail
如我所提到的,Visual VM 可能会提供一些非常短的方法,但它们并不像看起来那么重要。即使在商业分析工具中,您也需要小心处理得到的结果。如果没有分析工具,可以尝试调用两次 Math.floor 函数来查看速度变慢的程度,这将给您一个大致的时间增加量。 - Peter Lawrey
这是有趣的性能分析信息。我会再次查看我的仪器设备。我的运行是没有使用性能分析器完成的。 - Mikhail
1
@misha 你不能移除地板而不改变行为,但你可以调用它两三次来看看它变慢了多少 ;) - Peter Lawrey

0

如果你的算法大量依赖于Math.floor(和Math.ceil),那么它可能会成为一个令人惊讶的瓶颈。

这是因为这些函数处理了你可能不关心的边缘情况(例如负零和正零等)。只需查看这些函数的实现即可了解它们实际上在做什么;其中有相当多的分支。

此外,请考虑Math.floor/ceil仅接受double作为参数并返回double,而你可能不需要。如果你只想要int或long,Math.floor中的一些检查就是多余的。

有人建议简单地将其转换为int,只要你的值是正数(且你的算法不依赖于Math.floor检查的边缘情况),这种方法就可以奏效。如果是这种情况,简单的转换是最快的解决方案(根据我的经验)。

例如,如果你的值可以是负数,并且你想从float中获取int,你可以这样做:

public static final int floor(final float value) {
    return ((int) value) - (Float.floatToRawIntBits(value) >>> 31);
}

(它只是从强制转换中减去浮点数的符号位,使其对负数正确,同时避免了“if”语句)

根据我的经验,这比Math.floor要快得多。如果不是这样,我建议检查您的算法,或者您可能遇到了JVM性能错误(这种情况不太可能发生)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接