learn.lianglianglee.com/专栏/深入拆解Java虚拟机/22 HotSpot虚拟机的intrinsic.md.html
2022-05-11 18:57:05 +08:00

1105 lines
28 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>22 HotSpot虚拟机的intrinsic.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/深入拆解Java虚拟机/00 开篇词 为什么我们要学习Java虚拟机.md.html">00 开篇词 为什么我们要学习Java虚拟机.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/01 Java代码是怎么运行的.md.html">01 Java代码是怎么运行的.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/02 Java的基本类型.md.html">02 Java的基本类型.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/03 Java虚拟机是如何加载Java类的.md.html">03 Java虚拟机是如何加载Java类的.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/04 JVM是如何执行方法调用的.md.html">04 JVM是如何执行方法调用的.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/05 JVM是如何执行方法调用的.md.html">05 JVM是如何执行方法调用的.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/06 JVM是如何处理异常的.md.html">06 JVM是如何处理异常的.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/07 JVM是如何实现反射的.md.html">07 JVM是如何实现反射的.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/08 JVM是怎么实现invokedynamic的.md.html">08 JVM是怎么实现invokedynamic的.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/09 JVM是怎么实现invokedynamic的.md.html">09 JVM是怎么实现invokedynamic的.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/10 Java对象的内存布局.md.html">10 Java对象的内存布局.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/11 垃圾回收(上).md.html">11 垃圾回收(上).md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/12 垃圾回收(下).md.html">12 垃圾回收(下).md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/13 Java内存模型.md.html">13 Java内存模型.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/14 Java虚拟机是怎么实现synchronized的.md.html">14 Java虚拟机是怎么实现synchronized的.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/15 Java语法糖与Java编译器.md.html">15 Java语法糖与Java编译器.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/16 即时编译(上).md.html">16 即时编译(上).md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/17 即时编译(下).md.html">17 即时编译(下).md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/18 即时编译器的中间表达形式.md.html">18 即时编译器的中间表达形式.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/19 Java字节码基础篇.md.html">19 Java字节码基础篇.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/20 方法内联(上).md.html">20 方法内联(上).md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/21 方法内联(下).md.html">21 方法内联(下).md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/深入拆解Java虚拟机/22 HotSpot虚拟机的intrinsic.md.html">22 HotSpot虚拟机的intrinsic.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/23 逃逸分析.md.html">23 逃逸分析.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/24 字段访问相关优化.md.html">24 字段访问相关优化.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/25 循环优化.md.html">25 循环优化.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/26 向量化.md.html">26 向量化.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/27 注解处理器.md.html">27 注解处理器.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/28 基准测试框架JMH.md.html">28 基准测试框架JMH.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/29 基准测试框架JMH.md.html">29 基准测试框架JMH.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/30 Java虚拟机的监控及诊断工具命令行篇.md.html">30 Java虚拟机的监控及诊断工具命令行篇.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/31 Java虚拟机的监控及诊断工具GUI篇.md.html">31 Java虚拟机的监控及诊断工具GUI篇.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/32 JNI的运行机制.md.html">32 JNI的运行机制.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/33 Java Agent与字节码注入.md.html">33 Java Agent与字节码注入.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/34 Graal用Java编译Java.md.html">34 Graal用Java编译Java.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/35 Truffle语言实现框架.md.html">35 Truffle语言实现框架.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/36 SubstrateVMAOT编译框架.md.html">36 SubstrateVMAOT编译框架.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/尾声丨道阻且长,努力加餐.html.md.html">尾声丨道阻且长,努力加餐.html.md.html</a>
</li>
<li>
<a href="/专栏/深入拆解Java虚拟机/工具篇 常用工具介绍.md.html">工具篇 常用工具介绍.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>22 HotSpot虚拟机的intrinsic</h1>
<p>前不久,有同学问我,<code>String.indexOf</code>方法和自己实现的<code>indexOf</code>方法在字节码层面上差不多,为什么执行效率却有天壤之别呢?今天我们就来看一看。</p>
<pre><code>public int indexOf(String str) {
if (coder() == str.coder()) {
return isLatin1() ? StringLatin1.indexOf(value, str.value)
: StringUTF16.indexOf(value, str.value);
}
if (coder() == LATIN1) { // str.coder == UTF16
return -1;
}
return StringUTF16.indexOfLatin1(value, str.value);
}
</code></pre>
<p>为了解答这个问题,我们来读一下<code>String.indexOf</code>方法的源代码(上面的代码截取自 Java 10.0.2)。</p>
<blockquote>
<p>在 Java 9 之前,字符串是用 char 数组来存储的,主要为了支持非英文字符。然而,大多数 Java 程序中的字符串都是由 Latin1 字符组成的。也就是说每个字符仅需占据一个字节,而使用 char 数组的存储方式将极大地浪费内存空间。</p>
<p>Java 9 引入了 Compact Strings[1] 的概念,当字符串仅包含 Latin1 字符时,使用一个字节代表一个字符的编码格式,使得内存使用效率大大提高。</p>
</blockquote>
<p>假设我们调用<code>String.indexOf</code>方法的调用者以及参数均为只包含 Latin1 字符的字符串,那么该方法的关键在于对<code>StringLatin1.indexOf</code>方法的调用。</p>
<p>下面我列举了<code>StringLatin1.indexOf</code>方法的源代码。你会发现,它并没有使用特别高明的算法,唯一值得注意的便是方法声明前的<code>@HotSpotIntrinsicCandidate</code>注解。</p>
<pre><code>@HotSpotIntrinsicCandidate
public static int indexOf(byte[] value, byte[] str) {
if (str.length == 0) {
return 0;
}
if (value.length == 0) {
return -1;
}
return indexOf(value, value.length, str, str.length, 0);
}
@HotSpotIntrinsicCandidate
public static int indexOf(byte[] value, int valueCount, byte[] str, int strCount, int fromIndex) {
byte first = str[0];
int max = (valueCount - strCount);
for (int i = fromIndex; i &lt;= max; i++) {
// Look for first character.
if (value[i] != first) {
while (++i &lt;= max &amp;&amp; value[i] != first);
}
// Found first character, now look at the rest of value
if (i &lt;= max) {
int j = i + 1;
int end = j + strCount - 1;
for (int k = 1; j &lt; end &amp;&amp; value[j] == str[k]; j++, k++);
if (j == end) {
// Found whole string.
return i;
}
}
}
return -1;
}
</code></pre>
<p>在 HotSpot 虚拟机中,所有被该注解标注的方法都是 HotSpot intrinsic。对这些方法的调用会被 HotSpot 虚拟机替换成高效的指令序列。而原本的方法实现则会被忽略掉。</p>
<p>换句话说HotSpot 虚拟机将为标注了<code>@HotSpotIntrinsicCandidate</code>注解的方法额外维护一套高效实现。如果 Java 核心类库的开发者更改了原本的实现,那么虚拟机中的高效实现也需要进行相应的修改,以保证程序语义一致。</p>
<p>需要注意的是,其他虚拟机未必维护了这些 intrinsic 的高效实现,它们可以直接使用原本的较为低效的 JDK 代码。同样,不同版本的 HotSpot 虚拟机所实现的 intrinsic 数量也大不相同。通常越新版本的 Java其 intrinsic 数量越多。</p>
<p>你或许会产生这么一个疑问:为什么不直接在源代码中使用这些高效实现呢?</p>
<p>这是因为高效实现通常依赖于具体的 CPU 指令,而这些 CPU 指令不好在 Java 源程序中表达。再者,换了一个体系架构,说不定就没有对应的 CPU 指令,也就无法进行 intrinsic 优化了。</p>
<p>下面我们便来看几个具体的例子。</p>
<h2>intrinsic 与 CPU 指令</h2>
<p>在文章开头的例子中,<code>StringLatin1.indexOf</code>方法将在一个字符串byte 数组中查找另一个字符串byte 数组),并且返回命中时的索引值,或者 -1未命中</p>
<p>“恰巧”的是X86_64 体系架构的 SSE4.2 指令集就包含一条指令 PCMPESTRI让它能够在 16 字节以下的字符串中,查找另一个 16 字节以下的字符串,并且返回命中时的索引值。</p>
<p>因此HotSpot 虚拟机便围绕着这一指令,开发出 X86_64 体系架构上的高效实现,并替换原本对<code>StringLatin1.indexOf</code>方法的调用。</p>
<p>另外一个例子则是整数加法的溢出处理。一般我们在做整数加法时,需要考虑结果是否会溢出,并且在溢出的情况下作出相应的处理,以保证程序的正确性。</p>
<p>Java 核心类库提供了一个<code>Math.addExact</code>方法。它将接收两个 int 值(或 long 值)作为参数,并返回这两个 int 值的和。当这两个 int 值之和溢出时,该方法将抛出<code>ArithmeticException</code>异常。</p>
<pre><code>@HotSpotIntrinsicCandidate
public static int addExact(int x, int y) {
int r = x + y;
// HD 2-12 Overflow iff both arguments have the opposite sign of the result
if (((x ^ r) &amp; (y ^ r)) &lt; 0) {
throw new ArithmeticException(&quot;integer overflow&quot;);
}
return r;
}
</code></pre>
<p>在 Java 层面判断 int 值之和是否溢出比较费事。我们需要分别比较两个 int 值与它们的和的符号是否不同。如果都不同,那么我们便认为这两个 int 值之和溢出。对应的实现便是两个异或操作,一个与操作,以及一个比较操作。</p>
<p>在 X86_64 体系架构中大部分计算指令都会更新状态寄存器FLAGS register其中就有表示指令结果是否溢出的溢出标识位overflow flag。因此我们只需在加法指令之后比较溢出标志位便可以知道 int 值之和是否溢出了。对应的伪代码如下所示:</p>
<pre><code>public static int addExact(int x, int y) {
int r = x + y;
jo LABEL_OVERFLOW; // jump if overflow flag set
return r;
LABEL_OVERFLOW:
throw new ArithmeticException(&quot;integer overflow&quot;);
// or deoptimize
}
</code></pre>
<p>最后一个例子则是<code>Integer.bitCount</code>方法,它将统计所输入的 int 值的二进制形式中有多少个 1。</p>
<pre><code>@HotSpotIntrinsicCandidate
public static int bitCount(int i) {
// HD, Figure 5-2
i = i - ((i &gt;&gt;&gt; 1) &amp; 0x55555555);
i = (i &amp; 0x33333333) + ((i &gt;&gt;&gt; 2) &amp; 0x33333333);
i = (i + (i &gt;&gt;&gt; 4)) &amp; 0x0f0f0f0f;
i = i + (i &gt;&gt;&gt; 8);
i = i + (i &gt;&gt;&gt; 16);
return i &amp; 0x3f;
}
</code></pre>
<p>我们可以看到,<code>Integer.bitCount</code>方法的实现还是很巧妙的,但是它需要的计算步骤也比较多。在 X86_64 体系架构中,我们仅需要一条指令<code>popcnt</code>,便可以直接统计出 int 值中 1 的个数。</p>
<h2>intrinsic 与方法内联</h2>
<p>HotSpot 虚拟机中intrinsic 的实现方式分为两种。</p>
<p>一种是独立的桩程序。它既可以被解释执行器利用,直接替换对原方法的调用;也可以被即时编译器所利用,它把代表对原方法的调用的 IR 节点,替换为对这些桩程序的调用的 IR 节点。以这种形式实现的 intrinsic 比较少,主要包括<code>Math</code>类中的一些方法。</p>
<p>另一种则是特殊的编译器 IR 节点。显然,这种实现方式仅能够被即时编译器所利用。</p>
<p>在编译过程中,即时编译器会将对原方法的调用的 IR 节点,替换成特殊的 IR 节点,并参与接下来的优化过程。最终,即时编译器的后端将根据这些特殊的 IR 节点,生成指定的 CPU 指令。大部分的 intrinsic 都是通过这种方式实现的。</p>
<p>这个替换过程是在方法内联时进行的。当即时编译器碰到方法调用节点时,它将查询目标方法是不是 intrinsic。</p>
<p>如果是,则插入相应的特殊 IR 节点;如果不是,则进行原本的内联工作。(即判断是否需要内联目标方法的方法体,并在需要内联的情况下,将目标方法的 IR 图纳入当前的编译范围之中。)</p>
<p>也就是说,如果方法调用的目标方法是 intrinsic那么即时编译器会直接忽略原目标方法的字节码甚至根本不在乎原目标方法是否有字节码。即便是 native 方法,只要它被标记为 intrinsic即时编译器便能够将之 &quot; 内联 &quot; 进来,并插入特殊的 IR 节点。</p>
<p>事实上,不少被标记为 intrinsic 的方法都是 native 方法。原本对这些 native 方法的调用需要经过 JNIJava Native Interface其性能开销十分巨大。但是经过即时编译器的 intrinsic 优化之后,这部分 JNI 开销便直接消失不见,并且最终的结果也十分高效。</p>
<p>举个例子,我们可以通过<code>Thread.currentThread</code>方法来获取当前线程。这是一个 native 方法,同时也是一个 HotSpot intrinsic。在 X86_64 体系架构中R13 寄存器存放着当前线程的指针。因此,对该方法的调用将被即时编译器替换为一个特殊 IR 节点,并最终生成读取 R13 寄存器指令。</p>
<h2>已有 intrinsic 简介</h2>
<p>最新版本的 HotSpot 虚拟机定义了三百多个 intrinsic。</p>
<p>在这三百多个 intrinsic 中,有三成以上是<code>Unsafe</code>类的方法。不过,我们一般不会直接使用<code>Unsafe</code>类的方法,而是通过<code>java.util.concurrent</code>包来间接使用。</p>
<p>举个例子,<code>Unsafe</code>类中经常会被用到的便是<code>compareAndSwap</code>方法Java 9+ 更名为<code>compareAndSet</code><code>compareAndExchange</code>方法)。在 X86_64 体系架构中,对这些方法的调用将被替换为<code>lock cmpxchg</code>指令,也就是原子性更新指令。</p>
<p>除了<code>Unsafe</code>类的方法之外HotSpot 虚拟机中的 intrinsic 还包括下面的几种。</p>
<ol>
<li><code>StringBuilder</code><code>StringBuffer</code>类的方法。HotSpot 虚拟机将优化利用这些方法构造字符串的方式,以尽量减少需要复制内存的情况。</li>
<li><code>String</code>类、<code>StringLatin1</code>类、<code>StringUTF16</code>类和<code>Arrays</code>类的方法。HotSpot 虚拟机将使用 SIMD 指令single instruction multiple data即用一条指令处理多个数据对这些方法进行优化。
举个例子,<code>Arrays.equals(byte[], byte[])</code>方法原本是逐个字节比较,在使用了 SIMD 指令之后,可以放入 16 字节的 XMM 寄存器中(甚至是 64 字节的 ZMM 寄存器中)批量比较。</li>
<li>基本类型的包装类、<code>Object</code>类、<code>Math</code>类、<code>System</code>类中各个功能性方法,反射 API、<code>MethodHandle</code>类中与调用机制相关的方法,压缩、加密相关方法。这部分 intrinsic 则比较简单,这里就不详细展开了。如果你有感兴趣的,可以自行查阅资料,或者在文末留言。</li>
</ol>
<p>如果你想知道 HotSpot 虚拟机定义的所有 intrinsic那么你可以直接查阅 OpenJDK 代码 [2]。(该链接是 Java 12 的 intrinsic 列表。Java 8 的 intrinsic 列表可以查阅这一链接 [3]。)</p>
<h2>总结与实践</h2>
<p>今天我介绍了 HotSpot 虚拟机中的 intrinsic。</p>
<p>HotSpot 虚拟机将对标注了<code>@HotSpotIntrinsicCandidate</code>注解的方法的调用,替换为直接使用基于特定 CPU 指令的高效实现。这些方法我们便称之为 intrinsic。</p>
<p>具体来说intrinsic 的实现有两种。一是不大常见的桩程序,可以在解释执行或者即时编译生成的代码中使用。二是特殊的 IR 节点。即时编译器将在方法内联过程中,将对 intrinsic 的调用替换为这些特殊的 IR 节点,并最终生成指定的 CPU 指令。</p>
<p>HotSpot 虚拟机定义了三百多个 intrinsic。其中比较特殊的有<code>Unsafe</code>类的方法,基本上使用 java.util.concurrent 包便会间接使用到<code>Unsafe</code>类的 intrinsic。除此之外<code>String</code>类和<code>Arrays</code>类中的 intrinsic 也比较特殊。即时编译器将为之生成非常高效的 SIMD 指令。</p>
<p>今天的实践环节,你可以体验一下<code>Integer.bitCount</code> intrinsic 带来的性能提升。</p>
<pre><code>// time java Foo
public class Foo {
public static int bitCount(int i) {
// HD, Figure 5-2
i = i - ((i &gt;&gt;&gt; 1) &amp; 0x55555555);
i = (i &amp; 0x33333333) + ((i &gt;&gt;&gt; 2) &amp; 0x33333333);
i = (i + (i &gt;&gt;&gt; 4)) &amp; 0x0f0f0f0f;
i = i + (i &gt;&gt;&gt; 8);
i = i + (i &gt;&gt;&gt; 16);
return i &amp; 0x3f;
}
public static void main(String[] args) {
int sum = 0;
for (int i = Integer.MIN_VALUE; i &lt; Integer.MAX_VALUE; i++) {
sum += bitCount(i); // In a second run, replace with Integer.bitCount
}
System.out.println(sum);
}
}
</code></pre>
<p>[1] <a href="https://openjdk.java.net/jeps/254">http://openjdk.java.net/jeps/254</a>
[2] <a href="http://hg.openjdk.java.net/jdk/hs/file/46dc568d6804/src/hotspot/share/classfile/vmSymbols.hpp#l727">http://hg.openjdk.java.net/jdk/hs/file/46dc568d6804/src/hotspot/share/classfile/vmSymbols.hpp#l727</a>
[3] <a href="http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/2af8917ffbee/src/share/vm/classfile/vmSymbols.hpp#l647">http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/2af8917ffbee/src/share/vm/classfile/vmSymbols.hpp#l647</a></p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/深入拆解Java虚拟机/21 方法内联(下).md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/深入拆解Java虚拟机/23 逃逸分析.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"70997a093c443cfa","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>