【问题标题】:How can I prove that one algorithm is faster than another in Java我如何证明一种算法比Java中的另一种算法快
【发布时间】:2011-10-18 16:36:41
【问题描述】:

在 Java 中是否有任何东西可以让我获取代码片段并让我准确查看执行需要多少“滴答”。我想证明我写的一个算法比另一个更快。

【问题讨论】:

  • @William 回答了你,但这只会衡量这个算法的实现速度有多快,其他不多。
  • 次要的吹毛求疵:您并没有证明一种算法比另一种算法快;相反,对于给定的一组输入,在给定的机器上,您正在证明一种实现比另一种更快。 “算法的速度”通常是指它的渐近时间复杂度,而不是特定实现的执行时间。

标签: java performance-testing


【解决方案1】:

如果您的目的是比较两段代码之间的性能,最好的方法是使用 JMH。您可以通过 maven 导入,现已在 openjdk 12 中正式发布。

https://openjdk.java.net/projects/code-tools/jmh/

【讨论】:

    【解决方案2】:

    我不会像其他一些人建议的那样使用以毫秒为单位的当前时间。 ThreadMXBeans提供的方法比较准确(不敢说100%准确)。

    它们实际上测量的是线程占用的 CPU 时间,而不是经过的系统时间,这可能由于底层操作系统执行的上下文切换而出现偏差。

    Java Performance Testing

    【讨论】:

      【解决方案3】:

      您可能会问两个不同的问题:

      1. 如何衡量 Java 实现的运行时间(基准测试)
      2. 如何证明算法的渐近运行时间

      对于其中的第一个,我不会使用此处发布的解决方案。他们大多不完全正确。 Forst,使用System.nanoTime 可能比使用System.currentTimeMillis 更好。其次,您需要使用 try catch 块。第三,统计你的代码在指标之外多次运行的运行时间,这样你就可以有一个更完整的画面。 多次运行看起来像这样的代码:

      long totalTime = 0;
      long startTime = System.nanoTime();
      try{
         //method to test
      } finally {
         totalTime = System.nanoTime() - startTime;
      }
      

      正确进行基准测试很难。例如,您必须让您的代码“预热”几分钟,然后再对其进行测试。尽早并经常进行基准测试,但不要过分相信您的基准。特别是小型微型基准几乎总是以这样或那样的方式存在。

      解释问题的第二种方法是关于渐近运行时间。事实是这几乎与 Java 无关,它是一般的计算机科学。这里我们要问的问题是:哪些曲线根据输入大小来描述我们算法的运行时间行为。

      首先要理解 Big-Oh 符号。我会尽力而为,但 SO 不支持数学符号。 O(f(n)) 表示一组算法,使得n 趋于无穷大f(n) 在算法运行时间上限的常数因子内。正式地,T(n)O(f(n)) 中,如果存在一些常量n0 和一些常量c,那么对于所有n > n0 c*f(n) >= n。大欧米茄是一样的,除了上限,大 Theta f(n) 只是意味着它既大哦f(n) 和大欧米茄f(n)。这并不难。

      好吧,它变得有点复杂,因为我们可以讨论不同类型的运行时间,即“平均情况”、最佳情况和最坏情况。例如,normall quicksort 在最坏的情况下是O(n^2),但对于随机列表是O(n log n)

      所以我跳过了T(n) 的含义。基本上它是“滴答声”的数量。一些机器指令(比如从内存中读取)比其他机器指令(比如添加)花费的时间要长得多。但是,只要它们彼此之间只是一个常数因子,我们就可以将它们都视为 big Oh 的目的,因为它只会改变c 的值。

      证明渐近界并不难。对于简单的结构化编程问题,您只需数数

      public int square(int n){
         int sum = 0
         for(int i = 0, i < n, i++){
           sum += n
         }
         return sum
      }
      

      在本例中,我们各有一条指令:初始化 sum、初始化 i 和返回值。循环发生n 次,每次我们进行比较、加法和增量时。所以我们有 O(square(n)) = O(3 + 3n) 使用 2 的 n0 和 4 的 c,我们可以很容易地证明这是在 O(n) 中。您始终可以通过删除多余的常数项并除以常数倍数来安全地简化大 Oh 表达式。

      当您遇到递归函数时,您必须解决递归关系。如果你有像T(n) = 2*T(n/2) + O(1) 这样的函数,你想找到一个封闭形式的解决方案。有时您必须手动或使用计算机代数系统来完成。对于这个例子,使用前向替换,我们可以看到模式(在符号的滥用中)T(1) = O(1), T(2) = O(3), T(4) = O(7), T(8) = (15) 这看起来很像O(2n - 1),以证明这是正确的值:

       T(n) = 2*T(n/2) + 1
       T(n) = 2*(2(n/2) - 1) + 1
       T(n) = 2*(n-1) + 1
       T(n) = 2n - 2 + 1
       T(n) = 2n - 1
      

      正如我们之前看到的,您可以将O(2n -1) 简化为O(n)

      虽然您可以更经常地使用主定理,它是一种数学工具,可以节省您解决此类问题的时间。如果您检查wikipedia,您可以找到主定理,如果您即插即用上面的示例,您会得到相同的答案。

      如需了解更多信息,请查看 Levitin 的“算法设计与分析”等算法教科书

      【讨论】:

        【解决方案4】:

        如果这两种算法对宏观级别“tick”的定义相同(例如,遍历树中的一个节点),并且您的目标是证明您的算法在这些宏观级别的 tick 数量少于另一个,那么到目前为止,最好的方法是只检测每个实现来计算这些滴答声。这种方法是理想的,因为它不会奖励可以使代码执行得更快但与算法无关的低级实现技巧。

        如果您没有那么奢侈,但您正在尝试计算哪种方法使用最少的 CPU 资源解决问题,与此处列出的涉及 System.currentTimeMillis 等的方法相反,我将使用外部方法: linux time 命令将是理想的。您让每个程序在同一组(大型)输入上运行,最好花费几分钟或几小时的时间来处理,然后只运行 time java algo1time java algo2

        【讨论】:

          【解决方案5】:

          我愿意:

          • 为当前算法提出几个数据集:一个表现良好的数据集,一个表现良好的数据集,以及一个表现不佳的数据集。您想证明您的新算法在每种情况下都优于当前算法。
          • 多次运行并测量每个算法的性能,以增加三个数据集各自的输入大小,然后取平均值、标准差等。标准差将粗略衡量算法性能的一致性。李>
          • 最后查看数字并根据您的情况决定什么是重要的:哪种算法的性能更适合您大部分时间将使用的输入类型,以及当输入不是最佳时它如何降低性能。李>

          算法的计时不一定是一切——内存占用也很重要吗?一种算法在计算上可能更好,但它可能会在运行时创建更多对象......等等。只是想指出,除了纯粹的时间安排之外,还有更多需要考虑的事情!

          【讨论】:

            【解决方案6】:

            我必须在今年的数据结构课上做这个算法效率证明。

            首先,我像上面提到的那样测量时间。 然后我每次都用平方增加方法的输入数(10,100,1000,...) 最后,我将时间测量值放在 Excel 文件中,并为这些时间值绘制图形。

            通过这种方式,您可以稍微检查一种算法是否比其他算法快。

            【讨论】:

              【解决方案7】:

              您可以使用 System.currentTimeMillis() 或 System.nanoTime()(具有不同的特性)来测量挂壁时间。这相对容易,因为您只需在最后打印出差异即可。

              如果您需要计算特定操作(这在算法中很常见),最简单的方法是在操作完成时简单地增加一个计数器,然后在完成时打印它。 long 非常适合这个。对于多个操作,请使用多个计数器。

              【讨论】:

                【解决方案8】:

                “滴答声”?不。我建议您每次运行几次并比较平均结果:

                public class AlgorithmDriver {
                    public static void main(String [] args) {
                        int numTries = 1000000;
                        long begTime = System.currentTimeMillis();
                        for (int i = 0; i < numTries; ++i) {
                            Algorithm.someMethodCall();
                        }
                        long endTime = System.currentTimeMillis();
                        System.out.printf("Total time for %10d tries: %d ms\n", numTries, (endTime-begTime));
                    }
                }
                

                【讨论】:

                • 不要使用平均或最大运行时间!始终使用最小值。或者你会认为如果你有 [10, 11, 10, 13, 2000] and [30, 33, 35, 34, 40] 作为运行时,第一个通常不是更快的,而一个异常值更多地与调度、gc、分页等有关?
                • 如果我保留每次运行的数据,我肯定会查看它以获取其他信息。我认为这对于第一眼来说已经足够了。中位数对异常值的敏感度低于平均值;我认为这比说“始终使用最小值”更好。动脑筋是最好的建议。
                • 为什么?最小值是执行算法所需的最短时间。如果您经常同时运行两者,那么您应该至少运行一次,而无需从操作系统进行太多推断。当然,假设您会在没有干扰的情况下进行大量运行,中位数将给出与最小值几乎相同的结果,但这更多是偶然的。这一切都假设相同输入的算法的运行时间不会变化太大,但这通常是正确的(如果不是它变得复杂,但我怀疑在这种情况下中位数会那么好)
                • 如果您关于“与最小值几乎相同的结果”的论点是正确的,那么平均值就足够重要了。你的论点很荒谬。我不相信。我坚持我所知道的,因为你没有通过这个线程增加我的知识库。
                • 好吧,我的第一个例子离现实并不远。如果在运行您的算法时,例如 GC 启动,您可能会得到很大的(最多几秒钟!)延迟。即使这种情况只发生在所有情况的 1% 中,平均值也会非常偏斜,这个概念有什么难理解的呢?如果“你所知道的”是在我上面的例子中,第二个算法比第一个算法快 10 倍以上,这对你有好处,但大多数人和基准编写者会不同意.. 真的,我认为为什么 xX
                【解决方案9】:

                我对 Java 框架不太熟悉,但我会这样做:

                1. 定义一组可用于两种算法的测试用例(主要是示例数据)
                2. 实施计时方法来测量特定函数所用的时间量
                3. 创建一个 for 循环并执行方法 A(重复,例如,对整个测试数据执行 1000 次)。测量循环的时间,而不是单个函数的总和,因为时间函数在大量调用时会影响您的结果)
                4. 对方法 B 执行相同操作
                5. 比较您的结果并选择获胜者

                【讨论】:

                  【解决方案10】:

                  您可以使用System.currentTimeMillis() 获取开始和结束时间。

                  long start = System.currentTimeMillis();
                  
                  // your code
                  
                  long end = System.currentTimeMillis();
                  System.out.println( "time: " + (end - start) );
                  

                  【讨论】:

                  • 如果算法已经被优化以产生亚毫秒范围内的结果,这将是无用的;对于这种情况,(end-start) 将为 0。
                  • 好吧,如果你得到 0,那可能意味着你的算法非常好。另一种方法是检查算法的复杂性(即嵌套循环、循环内的语句),看看是否可以减少这些。
                  • duffymo 的回答将是下一步。多次测试并取平均值。
                  • 对于计时算法,您需要使用System.nanoTime()
                  • currentTimeMillis() 在某些系统上可能非常不准确,范围从 10 到 15 毫秒到任何对于短期运行算法来说确实不是最好的主意。
                  猜你喜欢
                  • 1970-01-01
                  • 2011-05-28
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多