【问题标题】:Runtime of Sorting-Algorithms gets faster (in Java)排序算法的运行时间变得更快(在 Java 中)
【发布时间】:2022-01-26 17:37:06
【问题描述】:

排序算法变得更快(在 Java 中)?!

我已经实现了一些排序算法和一个 getNanoTime 方法,它给出了这个排序算法的 NanoTime。

我想计算一个平均值。我意识到平均时间与一次测试算法的时间不同。

我以为我做错了什么。

但后来我找到了。

做的时候:

int length = 5000;
int bereich = 1000;

long time;

time = Bubblesort.getNanoTime(length, bereich);
System.out.println("BUBBLESORT:    " + (1.0 * time / 1_000_000) + " ms");

time = Insertionsort.getNanoTime(length, bereich);
System.out.println("INSERTIONSORT: " + (1.0 * time / 1_000_000) + " ms");

time = Mergesort.getNanoTime(length, bereich);
System.out.println("MERGESORT:     " + (1.0 * time / 1_000_000) + " ms");

time = Quicksort.getNanoTime(length, bereich);
System.out.println("QUICKSORT:     " + (1.0 * time / 1_000_000) + " ms");

time = Selectionsort.getNanoTime(length, bereich);
System.out.println("SELECTIONSORT: " + (1.0 * time / 1_000_000) + " ms");

我明白了:

冒泡排序:75.7835 毫秒

插入排序:27.250875 毫秒

合并:17.450083 毫秒

快速排序:7.092709 毫秒

选择排序:967.638792 毫秒


但是在做例子时:

for (int i = 0; i < 20; i++) {
    System.out.println(1.0 * Bubblesort.getNanoTime(5000, 1000) / 1_000_000);
}

我明白了:

85.473625 毫秒

62.681959 毫秒

68.866542 毫秒

48.737333 毫秒

47.402708 毫秒

47.368708 毫秒

47.567792 毫秒

47.018042 毫秒

45.1795 毫秒

47.871416 毫秒

49.570208 毫秒

50.285875 毫秒

56.37975 毫秒

50.342917 毫秒

50.262833 毫秒

50.036959 毫秒

50.286542 毫秒

51.752708 毫秒

50.342458 毫秒

51.511541 毫秒

第一次总是高(这里是第一次85毫秒),第一次之后的次数较低。 所以,我认为,机器学习,它变得更快

可以吗? 你知道更多吗?

【问题讨论】:

  • 这称为 JIT(及时)优化。搜索该词以获取您需要的所有信息。

标签: java algorithm sorting runtime


【解决方案1】:

我认为,机器学习,它变得更快

是的。

查看Just-in-time compilation,当你在做这件事的时候,花几周时间成为一名火箭科学家,这样你就可以完全了解CPU caches 的工作原理。

或者,如果您不想在接下来的 10 周内学习,但您确实想更好地了解其中的任何工作原理,请阅读此答案的其余部分,然后查看 this talk by Douglas Hawkins about JVM performance puzzlers。我敢打赌,看完这 40 分钟后,您会完全理解这个难题。

这里发生了两件事(JIT 预热效应和缓存页效应),可能还有更多:

  1. JIT 正在“热身”:java 的工作方式是,它以最愚蠢、最慢、最愚蠢的方式运行您的类文件代码,此外还浪费更多时间来维护代码上的大量簿记,例如“这个if 块多久进入一次它被跳过的频率?”没有充分的理由。一切都像糖蜜一样缓慢地运行。故意的,真的。

  2. 但是...由于所有这些记录,JVM 在某些时候会出现:嗯。从字面上看(我并没有夸大这种情况,这很常见!)99% 的 CPU 时间都花在了这 0.1% 的整个代码库上。

  3. 然后需要一些时间来分析这 0.1% 的日光,创建一个非常精细调整的机器代码版本,该版本非常适合您运行的实际 CPU。这需要很多时间,将使用所有的簿记(毕竟,这不是那么毫无意义!)来执行诸如重新排序代码之类的事情,以便 if/else 块中最常采用的“分支”是一个可以在没有代码跳转的情况下运行(由于管道重置而很慢),甚至会将当前观察到的真相变成假设。就像,代码被“编译”成机器代码,如果这些假设(到目前为止,由于所有的簿记,观察到总是正确的)最终是错误的,那么机器代码将直接不起作用,然后将添加钩子在整个 VM 中,如果任何代码碰巧打破了假设,精心制作的机器代码将被标记为现在无效并且将不再使用。例如,如果您有一个 not final 的类,则可以对其进行扩展。而且java总是动态调度:如果你调用foo.hello(),你会得到foo变量指向的对象的实际类型hello()实现,不是 strong> foo 表达式本身的类型。 java 中的类加载本质上是动态的(类可以随时加载,JVM 永远不会知道它已经“完成加载类”。这意味着一个查找表必须参与。一个昂贵的烦恼!但是,热点优化器绕过它并消除表:如果优化器发现非最终类当前没有扩展,或者所有扩展都没有覆盖有问题的实现,那么它可以省略查找表并仅链接一个方法直接调用实现。它还向类加载器添加了钩子,如果加载任何扩展目标类的类(并更改相关方法的 impl),则直接跳转到 impl 的机器代码无效。那方法的实际性能再次急剧下降,因为 JVM 回到了像糖蜜一样慢的方式。如果它仍然运行很多,不用担心。热点将再次通过,这次考虑到 are 多个实现。

  4. 一旦该机器代码可用,对该方法的所有调用都会重定向到使用该经过微调的机器代码运行。速度非常快;实际上,通常比 -O3 编译的 C 代码更快,因为 JVM 获得了将运行时行为考虑在内的好处,而 C 编译器永远无法做到。

  5. 尽管如此,通常只有大约 1% 的 VM 中的所有代码实际上是在这种模式下运行的。一个简单的事实是,几乎任何应用程序的所有代码都无关性能明智。它不会做任何复杂的事情,不会在“热”时刻运行,它只是。没有。事情。 smartypants 分析仅留给 1% 左右的人,这实际上会大量运行。

这很可能解释了这种差异的很大一部分:你的一堆排序算法的循环在 Dogslow(非热点)模式下运行,而一旦在你的第一次排序运行期间完成热点,下一次排序运行得到从一开始就使用热点代码的好处。

其次,数据需要在缓存页面中,以便 CPU 真正快速地处理。通常重复计算意味着第一次运行会受到 CPU 必须交换一堆缓存页面的惩罚,而所有未来的运行都不需要支付这个代价,因为内存的相关部分已经在缓存中。

结论很简单:像这样的微基准测试非常复杂,你不能只用 System.nanoTime 来计时,JVM 非常复杂,CPU 非常复杂,即使是那些熬夜的工程师编写 JVM 本身的记录表明他们太愚蠢了,无法像这样猜测性能。所以你绝对没有任何机会

幸运的是,解决方案也非常简单。那些相同的 JVM 工程师想知道东西运行的速度有多快,所以他们编写了一个完整的框架,让您可以进行微基准测试,主动检查热点预热,进行大量空运行,确保优化器不会优化您的整个算法(其中可能会发生,如果您对列表进行排序,然后将列表扔进垃圾箱,优化器可能会发现整个排序操作通过完全跳过它是最好的优化,因为,嘿,如果没有人真正关心排序的结果,为什么要排序,对吧?你需要一个“接收器”来确保优化器不会因为数据被丢弃而断定它可以把整个事情搞砸!) - 它被称为JMH。在 JMH 中重写你的基准测试并让它崩溃。你会发现它的时间是一致的,而且这些时间通常是有意义的(与你写的相比,这几乎没有任何意义)。

【讨论】:

  • 非常感谢! :)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-10-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-11-03
  • 2020-07-22
  • 1970-01-01
相关资源
最近更新 更多