【问题标题】:Why is the first run always much slower?为什么第一个跑步总是慢得多?
【发布时间】:2018-07-22 08:30:05
【问题描述】:

我编写了一个宏来报告运行给定操作所需的时间。它运行它多次并以纳秒为单位打印出每次运行的时间。第一次运行总是比后续运行花费更多的时间。为什么会这样?

这是 10 x 10 次运行的结果,时间为 Thread.yield()

> (dotimes [x 10] (prn (times 10 (Thread/yield))))

[55395 1659 622 561 591 702 795 719 742 624]
[3255 772 884 677 787 634 605 664 629 657]
[3431 789 965 671 774 767 627 627 521 717]
[2653 780 619 632 616 614 606 602 629 667]
[2373 759 700 676 557 639 659 654 659 676]
[2884 929 627 604 689 614 614 666 588 596]
[2796 749 672 769 667 852 629 589 627 802]
[1308 514 395 321 352 345 411 339 436 315]
[1390 363 328 337 330 321 324 347 333 342]
[1461 416 410 320 414 381 380 388 388 396]

第一批的第一次运行非常慢,我猜这是因为 JIT 第一次看到代码 - 很公平。但是所有后续批次中的第一次运行也明显慢于后续运行。为什么?

times 宏的代码:

(defmacro time
  [expr]
  `(let [t1# (System/nanoTime)]
     ~expr 
     (- (System/nanoTime) t1#)))

(defmacro times
  [reps expr]
  `(loop [reps# ~reps times# []]
     (if (zero? reps#) 
       times#
       (recur (dec reps#) (conj times# (time ~expr))))))

Decompiling 产生以下结果,因此 System.nanoTime() 似乎直接在 Thread.yield() 之前和之后调用,正如预期的那样:

> (decompile (dotimes [x 10] (prn (times 10 (Thread/yield)))))

...

public Object invoke() {
    long reps__1952__auto__2355 = 10L;
    Object times__1953__auto__2356 = PersistentVector.EMPTY;
    while (reps__1952__auto__2355 != 0L) {
        final long dec = Numbers.dec(reps__1952__auto__2355);
        final IFn fn = (IFn)const__3.getRawRoot();
        final Object o = times__1953__auto__2356;
        times__1953__auto__2356 = null;
        final long t1__1946__auto__2354 = System.nanoTime();
        Thread.yield();
        times__1953__auto__2356 = fn.invoke(o, Numbers.num(Numbers.minus(System.nanoTime(), t1__1946__auto__2354)));
        reps__1952__auto__2355 = dec;
    }
    final Object o2 = times__1953__auto__2356;
    times__1953__auto__2356 = null;
    return o2;
}

【问题讨论】:

  • 链接的问答包括对 Java 基准测试中这种行为的各种原因的解释。
  • 正如我所说,链接的答案和链接的文章解释了导致减速的各种因素。 AFAIK,这里没有关闭特定的问题。或者Java特定问题。效果都在 JVM 级别:字节码加载、初始解释、JIT 编译、堆大小调整和稳定。
  • @StephenC 投票重新开放,因为这对我来说不是基准问题,这段代码没有运行那么多次或更少,这是另一回事
  • 这与 JVM 和 Clojure 无关,而是与系统计时器和 OS 调度程序有关。我无法在 Windows 上重现该问题,但我清楚地看到了对 Linux 和 macOS 的影响,即使使用纯 C 程序:gist.github.com/apangin/9b298993d750c2e18ea7e34eb915e244 在循环外完成的工作越多 - 下一个yield 的时间越长。
  • @EugeneBeresovsky 随时在此处发表评论。这还不是答案,但我确信 I/O 本身(例如 System.out.println)并不重要。重要的是在产量之前执行的 CPU 工作量。操作系统调度程序考虑到这一点,试图公平地在进程之间分配 CPU 时间。

标签: java performance clojure benchmarking microbenchmark


【解决方案1】:

第一次运行总是比后续运行花费更多的时间。为什么会这样?

您的基准测试结果中还有另一个棘手的依赖因素:I/O。尝试一些返回时序向量而不是打印它们的测试运行,您应该会看到更符合以下情况的结果:

(for [_ (range 10)]
  (times 10 (Thread/yield)))
=>
([32674 1539 1068 1063 1027 1026 1025 1031 1034 1035]
 [1335 1048 1030 1036 1043 1037 1036 1031 1034 1047]
 [1088 1043 1029 1035 1045 1035 1036 1035 1045 1047]
 [1051 1037 1032 1031 1048 1045 1039 1045 1042 1037]
 [1054 1048 1032 1036 1046 1029 1038 1038 1039 1051]
 [1050 1051 1039 1037 1038 1035 1030 1030 1045 1031]
 [1054 1045 1034 1034 1045 1037 1037 1035 1046 1044]
 [1051 1041 1032 1050 1061 1039 1045 1041 1057 1034]
 [1052 1042 1034 1032 1035 1045 1043 1038 1052 1052]
 [1053 1053 1041 1043 1053 1044 1039 1042 1051 1038])

如果您在基准测试中使用System.out.println 而不是prn,您应该会看到相同的减速行为,但没有那么夸张:

(dotimes [x 10]
  (.println System/out (times 10 (Thread/yield))))
=> nil
[33521 1733 1232 1161 1150 1135 1151 1138 1143 1144]
[1724 1205 1149 1152 1141 1149 1149 1150 1139 1145]
[1368 1156 1141 1139 1147 1149 1141 1147 1141 1149]
[1306 1159 1150 1141 1150 1148 1147 1142 1144 1149]
[1329 1161 1155 1144 1140 1155 1151 1149 1149 1140]
[1319 1154 1140 1143 1147 1154 1156 1149 1148 1145]
[1291 1166 1164 1149 1140 1150 1140 1152 1141 1139]
[4482 1194 1148 1150 1137 1165 1163 1154 1149 1152]
[1333 1184 1162 1163 1138 1149 1150 1151 1137 1145]
[1318 1150 1144 1150 1151 1147 1138 1147 1143 1149]

【讨论】:

  • 有趣。我现在正在另一个盒子上尝试这个,这里的减速 - 对于prn.println - 也不太明显。因此,似乎第一个Thread/yieldSystem/nanoTimeprn(上一批)以某种方式交互......当然我们说的是数百纳秒,所以完全是微基准,但仍然很有趣,会回答我的问题.
【解决方案2】:

即使使用比(Thread/yield) 更便宜且IO 绑定更少的操作,您也可以看到这种效果,例如常量表达式5

user=> (doall (for [_ (range 10)] (times 10 5)))
[[390 132 134 132 109 86 94 109 115 112]
 [115 117 114 112 112 89 112 112 115 89]
 [117 106 109 109 109 86 109 109 111 109]
 [121 106 103 103 109 86 106 106 129 109]
 [117 109 106 109 112 95 111 112 109 89]
 [112 112 111 111 114 92 109 112 109 114]
 [118 111 112 111 115 88 112 109 115 92]
 [112 108 108 111 109 92 109 109 118 89]
 [115 106 112 115 112 89 112 109 114 89]
 [117 109 112 112 114 89 114 112 111 91]]

很有趣,不是吗?第一个表达式总是最慢的,或者至少非常接近最慢的,奇怪的是第六个和第十个往往是最快的。为什么会这样?

我最好的猜测就是 HotSpot 的神秘力量。即使在这个非常短的 sn-p 中,也有许多动态调度方法被调用。您将conj 称为IFn,也许HotSpot 建立了一些信心,即您的大多数IFn 调用将是conj,因此它试图使该用例更快;但是在每 10 次迭代结束时,还会调用一些其他函数,以附加到更大的结果列表中,因此 HotSpot 放弃了它的优化,期望您将开始做其他事情。

或者可能根本不是 HotSpot,而是与 CPU 缓存或操作系统的虚拟内存管理器的某种交互,或者......

当然这个具体的场景都是推测,但重点是,即使你写了非常简单的代码,也要依赖大量非常复杂的系统来为你运行,最终结果基本不知道不投入对所涉及的每个系统进行大量研究。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-06-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-04-09
    相关资源
    最近更新 更多