十六、代码校验(5)

本章概要

  • 基准测试
    • 微基准测试
    • JMH 的引入

基准测试

我们应该忘掉微小的效率提升,说的就是这些 97% 的时间做的事:过早的优化是万恶之源。—— Donald Knuth

如果你发现自己正在过早优化的滑坡上,你可能浪费了几个月的时间(如果你雄心勃勃的话)。通常,一个简单直接的编码方法就足够好了。如果你进行了不必要的优化,就会使你的代码变得无谓的复杂和难以理解。

基准测试意味着对代码或算法片段进行计时看哪个跑得更快,与下一节的分析和优化截然相反,分析优化是观察整个程序,找到程序中最耗时的部分。

可以简单地对一个代码片段的执行计时吗?在像 C 这样直接的编程语言中,这个方法的确可行。在像 Java 这样拥有复杂的运行时系统的编程语言中,基准测试变得更有挑战性。为了生成可靠的数据,环境设置必须控制诸如 CPU 频率,节能特性,其他运行在相同机器上的进程,优化器选项等等。

微基准测试

写一个计时工具类从而比较不同代码块的执行速度是具有吸引力的。看上去这会产生一些有用的数据。比如,这里有一个简单的 Timer 类,可以用以下两种方式使用它:

  1. 创建一个 Timer 对象,执行一些操作然后调用 Timerduration() 方法产生以毫秒为单位的运行时间。
  2. 向静态的 duration() 方法中传入 Runnable。任何符合 Runnable 接口的类都有一个函数式方法 run(),该方法没有入参,且没有返回。
import static java.util.concurrent.TimeUnit.*;public class Timer {private long start = System.nanoTime();public long duration() {return NANOSECONDS.toMillis(System.nanoTime() - start);}public static long duration(Runnable test) {Timer timer = new Timer();test.run();return timer.duration();}
}

这是一个很直接的计时方式。难道我们不能只运行一些代码然后看它的运行时长吗?

有许多因素会影响你的结果,即使是生成提示符也会造成计时的混乱。这里举一个看上去天真的例子,它使用了 标准的 Java Arrays 库(后面会详细介绍):

import java.util.*;public class BadMicroBenchmark {static final int SIZE = 250_000_000;public static void main(String[] args) {try { // For machines with insufficient memorylong[] la = new long[SIZE];System.out.println("setAll: " + Timer.duration(() -> Arrays.setAll(la, n -> n)));System.out.println("parallelSetAll: " + Timer.duration(() -> Arrays.parallelSetAll(la, n -> n)));} catch (OutOfMemoryError e) {System.out.println("Insufficient memory");System.exit(0);}}
}

在这里插入图片描述

main() 方法的主体包含在 try 语句块中,因为一台机器用光内存后会导致构建停止。

对于一个长度为 250,000,000 的 long 型(仅仅差一点就会让大部分机器内存溢出)数组,我们比较了 Arrays.setAll()Arrays.parallelSetAll() 的性能。这个并行的版本会尝试使用多个处理器加快完成任务(尽管我在这一节谈到了一些并行的概念,但是在 并发编程 章节我们才会详细讨论这些 )。然而非并行的版本似乎运行得更快,尽管在不同的机器上结果可能不同。

BadMicroBenchmark.java 中的每一步操作都是独立的,但是如果你的操作依赖于同一资源,那么并行版本运行的速度会骤降,因为不同的进程会竞争相同的那个资源。

import java.util.*;public class BadMicroBenchmark2 {static final int SIZE = 5_000_000;public static void main(String[] args) {long[] la = new long[SIZE];Random r = new Random();System.out.println("parallelSetAll: " + Timer.duration(() -> Arrays.parallelSetAll(la, n -> r.nextLong())));System.out.println("setAll: " + Timer.duration(() -> Arrays.setAll(la, n -> r.nextLong())));SplittableRandom sr = new SplittableRandom();System.out.println("parallelSetAll: " + Timer.duration(() -> Arrays.parallelSetAll(la, n -> sr.nextLong())));System.out.println("setAll: " + Timer.duration(() -> Arrays.setAll(la, n -> sr.nextLong())));}
}

在这里插入图片描述

SplittableRandom 是为并行算法设计的,它当然看起来比普通的 RandomparallelSetAll() 中运行得更快。 但是看上去还是比非并发的 setAll() 运行时间更长,有点难以置信(也许是真的,但我们不能通过一个坏的微基准测试得到这个结论)。

这只考虑了微基准测试的问题。Java 虚拟机 Hotspot 也非常影响性能。如果你在测试前没有通过运行代码给 JVM 预热,那么你就会得到“冷”的结果,不能反映出代码在 JVM 预热之后的运行速度(假如你运行的应用没有在预热的 JVM 上运行,你就可能得不到所预期的性能,甚至可能减缓速度)。

优化器有时可以检测出你创建了没有使用的东西,或者是部分代码的运行结果对程序没有影响。如果它优化掉你的测试,那么你可能得到不好的结果。

一个良好的微基准测试系统能自动地弥补像这样的问题(和很多其他的问题)从而产生合理的结果,但是创建这么一套系统是非常棘手,需要深入的知识。

JMH 的引入

截止目前为止,唯一能产生像样结果的 Java 微基准测试系统就是 Java Microbenchmarking Harness,简称 JMH。本书的 build.gradle 自动引入了 JMH 的设置,所以你可以轻松地使用它。

你可以在命令行编写 JMH 代码并运行它,但是推荐的方式是让 JMH 系统为你运行测试;build.gradle 文件已经配置成只需要一条命令就能运行 JMH 测试。

JMH 尝试使基准测试变得尽可能简单。例如,我们将使用 JMH 重新编写 BadMicroBenchmark.java。这里只有 **@State ** 和 **@Benchmark ** 这两个注解是必要的。其余的注解要么是为了产生更多易懂的输出,要么是加快基准测试的运行速度(JMH 基准测试通常需要运行很长时间):

// validating/jmh/JMH1.java
package validating.jmh;
import java.util.*;
import org.openjdk.jmh.annotations.*;
import java.util.concurrent.TimeUnit;@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
// Increase these three for more accuracy:
@Warmup(iterations = 5)
@Measurement(iterations = 5)
@Fork(1)
public class JMH1 {private long[] la;@Setuppublic void setup() {la = new long[250_000_000];}@Benchmarkpublic void setAll() {Arrays.setAll(la, n -> n);}public void parallelSetAll() {Arrays.parallelSetAll(la, n -> n);}
}

“forks” 的默认值是 10,意味着每个测试都运行 10 次。为了减少运行时间,这里使用了 **@Fork ** 注解来减少这个次数到 1。我还使用了 **@Warmup ** 和 **@Measurement ** 注解将它们默认的运行次数从 20 减少到 5 次。尽管这降低了整体的准确率,但是结果几乎与使用默认值相同。可以尝试将 **@Warmup @Measurement ** 和 **@Fork ** 都注释掉然后看使用它们的默认值,结果会有多大显著的差异;一般来说,你应该只能看到长期运行的测试使错误因素减少,而结果没有多大变化。

需要使用显式的 gradle 命令才能运行基准测试(在示例代码的根目录处运行)。这能防止耗时的基准测试运行其他的 gradlew 命令:

gradlew validating:jmh

这会花费几分钟的时间,取决于你的机器(如果没有注解上的调整,可能需要几个小时)。控制台会显示 results.txt 文件的路径,这个文件统计了运行结果。注意,results.txt 包含这一章所有 jmh 测试的结果:JMH1.javaJMH2.javaJMH3.java

因为输出是绝对时间,所以在不同的机器和操作系统上结果各不相同。重要的因素不是绝对时间,我们真正观察的是一个算法和另一个算法的比较,尤其是哪一个运行得更快,快多少。如果你在自己的机器上运行测试,你将看到不同的结果却有着相同的模式。

我在大量的机器上运行了这些测试,尽管不同的机器上得到的绝对值结果不同,但是相对值保持着合理的稳定性。我只列出了 results.txt 中适当的片段并加以编辑使输出更加易懂,而且内容大小适合页面。所有测试中的 Mode 都以 avgt 展示,代表 “平均时长”。Cnt(测试的数目)的值是 200,尽管这里的一个例子中配置的 Cnt 值是 5。Unitsus/op,是 “Microseconds per operation” 的缩写,因此,这个值越小代表性能越高。

我同样也展示了使用 warmups、measurements 和 forks 默认值的输出。我删除了示例中相应的注解,就是为了获取更加准确的测试结果(这将花费数小时)。结果中数字的模式应该仍然看起来相同,不论你如何运行测试。

下面是 JMH1.java 的运行结果:

Benchmark Score

JMH1.setAll 196280.2

JMH1.parallelSetAll 195412.9

即使像 JMH 这么高级的基准测试工具,基准测试的过程也不容易,练习时需要倍加小心。这里测试产生了反直觉的结果:并行的版本 parallelSetAll() 花费了与非并行版本的 setAll() 相同的时间,两者似乎都运行了相当长的时间。

当创建这个示例时,我假设如果我们要测试数组初始化的话,那么使用非常大的数组是有意义的。所以我选择了尽可能大的数组;如果你实验的话会发现一旦数组的大小超过 2亿5000万,你就开始会得到内存溢出的异常。然而,在这么大的数组上执行大量的操作从而震荡内存系统,产生无法预料的结果是有可能的。不管这个假设是否正确,看上去我们正在测试的并非是我们想测试的内容。

考虑其他的因素:

C:客户端执行操作的线程数量

P:并行算法使用的并行数量

N:数组的大小:**10^(2_k)_,通常来说,k=1…7 足够来练习不同的缓存占用。

Q:setter 的操作成本

这个 C/P/N/Q 模型在早期 JDK 8 的 Lambda 开发期间浮出水面,大多数并行的 Stream 操作(parallelSetAll() 也基本相似)都满足这些结论:**N_Q_(主要工作量)对于并发性能尤为重要。并行算法在工作量较少时可能实际运行得更慢。

在一些情况下操作竞争如此激烈使得并行毫无帮助,而不管 **N_Q_ 有多大。当 C 很大时,P 就变得不太相关(内部并行在大量的外部并行面前显得多余)。此外,在一些情况下,并行分解会让相同的 C 个客户端运行得比它们顺序运行代码更慢。

基于这些信息,我们重新运行测试,并在这些测试中使用不同大小的数组(改变 N):

// validating/jmh/JMH2.java
package validating.jmh;
import java.util.*;
import org.openjdk.jmh.annotations.*;
import java.util.concurrent.TimeUnit;@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Warmup(iterations = 5)
@Measurement(iterations = 5)
@Fork(1)
public class JMH2 {private long[] la;@Param({"1","10","100","1000","10000","100000","1000000","10000000","100000000","250000000"})int size;@Setuppublic void setup() {la = new long[size];}@Benchmarkpublic void setAll() {Arrays.setAll(la, n -> n);}@Benchmarkpublic void parallelSetAll() {Arrays.parallelSetAll(la, n -> n);}
}

**@Param ** 会自动地将其自身的值注入到变量中。其自身的值必须是字符串类型,并可以转化为适当的类型,在这个例子中是 int 类型。

下面是已经编辑过的结果,包含精确计算出的加速数值:

JMH2 BenchmarkSizeScore %Speedup
setAll10.001
parallelSetAll10.0360.028
setAll100.005
parallelSetAll103.9650.001
setAll1000.031
parallelSetAll1003.1450.010
setAll10000.302
parallelSetAll10003.2850.092
setAll100003.152
parallelSetAll100009.6690.326
setAll10000034.971
parallelSetAll10000020.1531.735
setAll1000000420.581
parallelSetAll1000000165.3882.543
setAll100000008160.054
parallelSetAll100000007610.1901.072
setAll10000000079128.752
parallelSetAll10000000076734.6711.031
setAll250000000199552.121
parallelSetAll250000000191791.9271.040
可以看到当数组大小达到 10 万左右时,parallelSetAll() 开始反超,而后趋于与非并行的运行速度相同。即使它运行速度上胜了,看起来也不足以证明由于并行的存在而使速度变快。

setAll()/parallelSetAll() 中工作的计算量起很大影响吗?在前面的例子中,我们所做的只有对数组的赋值操作,这可能是最简单的任务。所以即使 N 值变大,**N_Q_ 也仍然没有达到巨大,所以看起来像是我们没有为并行提供足够的机会(JMH 提供了一种模拟变量 Q 的途径;如果想了解更多的话,可搜索 Blackhole.consumeCPU)。

我们通过使方法 f() 中的任务变得更加复杂,从而产生更多的并行机会:

// validating/jmh/JMH3.java
package validating.jmh;
import java.util.*;
import org.openjdk.jmh.annotations.*;
import java.util.concurrent.TimeUnit;@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Warmup(iterations = 5)
@Measurement(iterations = 5)
@Fork(1)
public class JMH3 {private long[] la;@Param({"1","10","100","1000","10000","100000","1000000","10000000","100000000","250000000"})int size;@Setuppublic void setup() {la = new long[size];}public static long f(long x) {long quadratic = 42 * x * x + 19 * x + 47;return Long.divideUnsigned(quadratic, x + 1);}@Benchmarkpublic void setAll() {Arrays.setAll(la, n -> f(n));}@Benchmarkpublic void parallelSetAll() {Arrays.parallelSetAll(la, n -> f(n));}
}

f() 方法提供了更加复杂且耗时的操作。现在除了简单的给数组赋值外,setAll()parallelSetAll() 都有更多的工作去做,这肯定会影响结果。

JMH2 BenchmarkSizeScore %Speedup
setAll10.012
parallelSetAll10.0470.255
setAll100.107
parallelSetAll103.8940.027
setAll1000.990
parallelSetAll1003.7080.267
setAll1000133.814
parallelSetAll100011.74711.391
setAll1000097.954
parallelSetAll1000037.2592.629
setAll100000988.475
parallelSetAll100000276.2643.578
setAll10000009203.103
parallelSetAll10000002826.9743.255
setAll1000000092144.951
parallelSetAll1000000028126.2023.276
setAll100000000921701.863
parallelSetAll100000000266750.5433.455
setAll2500000002299127.273
parallelSetAll250000000538173.4254.272

可以看到当数组的大小达到 1000 左右时,parallelSetAll() 的运行速度反超了 setAll()。看来 parallelSetAll() 严重依赖数组中计算的复杂度。这正是基准测试的价值所在,因为我们已经得到了关于 setAll()parallelSetAll() 间微妙的信息,知道在何时使用它们。

这显然不是从阅读 Javadocs 就能得到的。

大多数时候,JMH 的简单应用会产生好的结果(正如你将在本书后面例子中所见),但是我们从这里知道,你不能一直假定 JMH 会产生好的结果。 JMH 网站上的范例可以帮助你开始。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/106062.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【AI视野·今日Robot 机器人论文速览 第五十四期】Fri, 13 Oct 2023

AI视野今日CS.Robotics 机器人学论文速览 Fri, 13 Oct 2023 Totally 45 papers 👉上期速览✈更多精彩请移步主页 Interesting: 📚AI与机器人安全, 从攻击界面、伦理法律和人机交互层面进行了论述。(from 密西西比大学) 📚机器人与图机器学…

华为云云耀云服务器L实例评测|企业项目最佳实践之建议与总结(十二)

华为云云耀云服务器L实例评测|企业项目最佳实践系列: 华为云云耀云服务器L实例评测|企业项目最佳实践之云服务器介绍(一) 华为云云耀云服务器L实例评测|企业项目最佳实践之华为云介绍(二) 华为云云耀云服务器L实例评测&#xff5…

2021年12月 Python(二级)真题解析#中国电子学会#全国青少年软件编程等级考试

Python编程(1~6级)全部真题・点这里 C/C编程(1~8级)全部真题・点这里 一、单选题(共25题,每题2分,共50分) 第1题 执行以下程序 a[33,55,22,77] a.sort() for i in a:print(i)运行…

Nginx:反向代理(示意图+配置)

示意图: 反向代理 反向代理(Reverse Proxy)是代理服务器的一种,它代表服务器接收客户端的请求,并将这些请求转发到适当的服务器。当请求在后端服务器完成之后,反向代理搜集请求的响应并将其传输给客户端。…

MFC-对话框

目录 1、模态和非模态对话框: (1)、对话框的创建 (2)、更改默认的对话框名称 (3)、创建模态对话框 1)、创建按钮跳转的界面 2)、在跳转的窗口添加类 3&#xff0…

docker安装nessus

注册地址:https://zh-tw.tenable.com/products/nessus/nessus-essentials 临时邮箱:http://24mail.chacuo.net/ 帮助文档:https://docs.tenable.com/nessus/Content/DeployNessusDocker.htmdocker pull tenableofficial/nessusdocker run --name "my-nessus" -d -p 8…

vscode用密钥文件连接ssh:如果一直要输密码怎么办

commandshiftP:打开ssh配置文件 加上这么一段,host就是你给主机起的名字 对IdentityFile进行更改,改成相应的密钥文件 然后commandshiftP链接到主机就可以了 但是有时候它会让输入密码 这是由于你给这个IdentityFile的权限太多了&#xf…

数据结构 - 2(顺序表10000字详解)

一:List 1.1 什么是List 在集合框架中,List是一个接口,继承自Collection。 Collection也是一个接口,该接口中规范了后序容器中常用的一些方法,具体如下所示: Iterable也是一个接口,Iterabl…

【Vue面试题二十三】、你了解vue的diff算法吗?说说看

文章底部有个人公众号:热爱技术的小郑。主要分享开发知识、学习资料、毕业设计指导等。有兴趣的可以关注一下。为何分享? 踩过的坑没必要让别人在再踩,自己复盘也能加深记忆。利己利人、所谓双赢。 面试官:你了解vue的diff算法吗&…

【Vue面试题二十一】、Vue中的过滤器了解吗?过滤器的应用场景有哪些?

文章底部有个人公众号:热爱技术的小郑。主要分享开发知识、学习资料、毕业设计指导等。有兴趣的可以关注一下。为何分享? 踩过的坑没必要让别人在再踩,自己复盘也能加深记忆。利己利人、所谓双赢。 面试官:Vue中的过滤器了解吗&am…

Java字符串String

【char】类型代表字符类型,【String】类型代表字符串类型; 1.String类 1.1 声明字符串 在Java中字符串必须包含在一对双引号(“ ")之内。双引号包含的都是字符串。 声明字符串语法: String str; //声明字符串语…

排序算法-选择排序法(SelectionSort)

排序算法-选择排序法(SelectionSort) 1、说明 选择排序法也是枚举法的应用,就是反复从未排序的数列中取出最小的元素,加入另一个数列中,最后的结果即为已排序的数列。选择排序法可使用两种方式排序,即在所…

苍穹外卖(七) Spring Task 完成订单状态定时处理

Spring Task 完成订单状态定时处理, 如处理支付超时订单 Spring Task介绍 Spring Task 是Spring框架提供的任务调度工具,可以按照约定的时间自动执行某个代码逻辑。 应用场景: 信用卡每月还款提醒 火车票售票系统处理未支付订单 入职纪念日为用户发送通知 点外…

操作系统学习笔记4-死锁问题

文章目录 1、死锁逻辑图2、死锁三胞胎3、死锁的原因及必要条件4、死锁处理策略之死锁预防5、死锁处理策略之死锁避免(银行家算法)6、死锁处理策略之死锁检测与解除 1、死锁逻辑图 2、死锁三胞胎 3、死锁的原因及必要条件 4、死锁处理策略之死锁预防 5、死…

python openai宠物名字生成器

文章目录 OpenAICompletion宠物名字生成器提示词工程 prompt enginering 构建应用程序结果展示 OpenAI OpenAI 已经训练了非常擅长理解和生成文本的领先的语言模型。我们的 API 提供对这些模型的访问,可用于处理几乎任何涉及”语言处理“的任务。 Completion 补全…

18 | 生产环境多数据源的处理方法有哪些

工作中我们时常会遇到跨数据库操作的情况,这时候就需要配置多数据源,那么如何配置呢?常用的方式及其背后的原理支撑是什么呢?我们下面来了解一下。 首先看看两种常见的配置方式,分别为通过多个 Configuration 文件、利…

【完美世界】云曦篇开播时间定档,推迟两周,石昊新形态帅翻,怒斩战王

Hello,小伙伴们,我是小郑继续为大家深度解析国漫资讯。 完美世界动画更新最新资讯,石昊在血色平原与云曦重逢并英雄救美。 官方公布了云曦特别篇的先导预告,播出时间推迟了两周。 石昊在特别篇中出现了新形态,以雷帝甲胄为主&…

Leetcode—88.合并两个有序数组【简单】

2023每日刷题(一) Leetcode—88.合并两个有序数组 题解 因为这两个数组已经排好序,我们可以把两个指针分别放在两个数组的末尾,即 nums1 的m − 1 位和 nums2 的 n − 1 位。每次将较大的那个数字复制到 nums1 的后边&#xff0…

Kafka SASL认证授权(六)全方位性能测试

Kafka SASL认证授权(六)全方位性能测试。 官网地址:https://kafka.apache.org/ 一、场景 线上已经有kafka集群,服务运行稳定。但是因为产品升级,需要对kakfa做安全测试,也就是权限验证。 但是增加权限验证,会不会对性能有影响呢?影响大吗?不知道呀! 因此,本文就此…

【Hyper-V】Windows的Hyper-V管理器创建的虚拟机上怎么复制粘贴文件

在hyper-V 管理器中创建的虚拟机里面,发现它是不能直接复制粘贴文件的,那只能粘贴纯文本内容,也就是模拟输入内容,这是虚拟系统是被隔离的,也带来不便之处,要怎么解决这个限制呢。 例如,打开一…