【发布时间】: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