這是由 Java 官方發佈,Oracle JDK 研發 Nipafx 製作的節目,包含 JDK 近期的研發進展和新特性展望和使用,這裏加上個人譯製的字幕搬運而來。我把 Nipafx 的擴展資料詳細研讀並提取精華做了個人詳細解讀:視頻地址(熟肉)
⎯⎯⎯⎯⎯⎯ Chapters ⎯⎯⎯⎯⎯⎯
- 0:00 - Intro
- 0:33 - Vector API
- 0:56 - Vector API - SIMD and Vector Instructions
- 2:22 - Vector API - Current State
- 3:10 - Vector API - More Inside Java podcast Ep. 7
- 3:59 - Records Serialization
- 5:22 - JDK 17 - Enhanced Pseudo-Random Number Generators
- 6:06 - Outro
這一節的內容不是很多,但是都比較有意思。
Vector API
相關 JEP:
- JEP 338: Vector API (Incubator)
- JEP 414: Vector API (Second Incubator):Java 17 中的
- JEP 417: Vector API (Third Incubator):Java 18 中的
其中最主要的應用就是使用了 CPU 的 SIMD(單指令多數據)處理,它提供了通過程序的多通道數據流,可能有 4 條通道或 8 條通道或任意數量的單個數據元素流經的通道。並且 CPU 一次在所有通道上並行組織操作,這可以極大增加 CPU 吞吐量。通過 Vector API,Java 團隊正在努力讓 Java 程序員使用 Java 代碼直接訪問它;過去,他們必須在彙編代碼級別對向量數學進行編程,或者使用 C/C++ 與 Intrinsic 一起使用,然後通過 JNI 提供給 Java。
一個主要的優化點就是循環,過去的循環(標量循環),一次在一個元素上執行,那很慢。現在,您可以使用 Vector API 將標量算法轉換爲速度更快的數據並行算法。一個使用 Vector 的例子:
//測試指標爲吞吐量
@BenchmarkMode(Mode.Throughput)
//需要預熱,排除 jit 即時編譯以及 JVM 採集各種指標帶來的影響,由於我們單次循環很多次,所以預熱一次就行
@Warmup(iterations = 1)
//單線程即可
@Fork(1)
//測試次數,我們測試10次
@Measurement(iterations = 10)
//定義了一個類實例的生命週期,所有測試線程共享一個實例
@State(value = Scope.Benchmark)
public class VectorTest {
private static final VectorSpecies<Float> SPECIES =
FloatVector.SPECIES_256;
final int size = 1000;
final float[] a = new float[size];
final float[] b = new float[size];
final float[] c = new float[size];
public VectorTest() {
for (int i = 0; i < size; i++) {
a[i] = ThreadLocalRandom.current().nextFloat(0.0001f, 100.0f);
b[i] = ThreadLocalRandom.current().nextFloat(0.0001f, 100.0f);
}
}
@Benchmark
public void testScalar(Blackhole blackhole) throws Exception {
for (int i = 0; i < a.length; i++) {
c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;
}
}
@Benchmark
public void testVector(Blackhole blackhole) {
int i = 0;
//高於數組長度的 SPECIES 一次處理數據長度的倍數
int upperBound = SPECIES.loopBound(a.length);
//每次循環處理 SPECIES.length() 這麼多的數據
for (; i < upperBound; i += SPECIES.length()) {
// FloatVector va, vb, vc;
var va = FloatVector.fromArray(SPECIES, a, i);
var vb = FloatVector.fromArray(SPECIES, b, i);
var vc = va.mul(va)
.add(vb.mul(vb))
.neg();
vc.intoArray(c, i);
}
for (; i < a.length; i++) {
c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;
}
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder().include(VectorTest.class.getSimpleName()).build();
new Runner(opt).run();
}
}
注意使用處於孵化的 Java 特性需要加上額外的啓動參數將模塊暴露,這裏是--add-modules jdk.incubator.vector
,需要在 javac 編譯和 java 運行都加上這些參數,使用 IDEA 即:
測試結果:
Benchmark Mode Cnt Score Error Units
VectorTest.testScalar thrpt 10 7380697.998 ± 1018277.914 ops/s
VectorTest.testVector thrpt 10 37151609.182 ± 1011336.900 ops/s
其他使用,請參考:fizzbuzz-simd-style,這是一篇比較有意思的文章(雖然這個性能優化感覺不只由於 SIMD,還有算法優化的功勞,哈哈)
關於一些更加詳細的使用,以及設計思路,可以參考這個音頻:https://www.youtube.com/watch?v=VYo3p4R66N8&t=427s
Records Serialization
關於 Java Record 的序列化,我也寫過一篇文章進行分析,參考:Java Record 的一些思考 - 序列化相關
其中,最重要的是一些主流的序列化框架的兼容
由於 Record 限制了序列化與反序列化的唯一方式,所以其實兼容起來很簡單,比起 Java Class 改個結構,加個特性導致的序列化框架更改來說還要簡單。
這三個框架中實現對於 Record 的兼容思路都很類似,也比較簡單,即:
- 實現一個針對 Record 的專用的 Serializer 以及Deserializer。
- 通過反射(Java Reflection)或者句柄(Java MethodHandle)驗證當前版本的 Java 是否支持 Record,以及獲取 Record 的規範構造函數(canonical constructor)以及各種 field 的 getter 進行反序列化和序列化。
JDK 17 - Enhanced Pseudo-Random Number Generators
Java 17 針對隨機數生成器做了統一接口封裝,並且內置了 Xoshiro 算法以及自己研發的 LXM 算法,可以參考我的這個系列文章:
這裏截取一部分分析:
根據之前的分析,應該還是 SplittableRandom 在單線程環境下最快,多線程環境下使用 ThreadLocalRandom 最快。新增的隨機算法實現類,Period 約大需要的計算越多, LXM 的實現需要更多計算,加入這些算法是爲了適應更多的隨機應用,而不是爲了更快。不過爲了滿足大家的好奇心,還是寫了如下的代碼進行測試,從下面的代碼也可以看出,新的 RandomGenerator API 使用更加簡便:
package prng;
import java.util.random.RandomGenerator;
import java.util.random.RandomGeneratorFactory;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
//測試指標爲吞吐量
@BenchmarkMode(Mode.Throughput)
//需要預熱,排除 jit 即時編譯以及 JVM 採集各種指標帶來的影響,由於我們單次循環很多次,所以預熱一次就行
@Warmup(iterations = 1)
//線程個數
@Threads(10)
@Fork(1)
//測試次數,我們測試50次
@Measurement(iterations = 50)
//定義了一個類實例的生命週期,所有測試線程共享一個實例
@State(value = Scope.Benchmark)
public class TestRandomGenerator {
@Param({
"Random", "SecureRandom", "SplittableRandom", "Xoroshiro128PlusPlus", "Xoshiro256PlusPlus", "L64X256MixRandom",
"L64X128StarStarRandom", "L64X128MixRandom", "L64X1024MixRandom", "L32X64MixRandom", "L128X256MixRandom",
"L128X128MixRandom", "L128X1024MixRandom"
})
private String name;
ThreadLocal<RandomGenerator> randomGenerator;
@Setup
public void setup() {
final String finalName = this.name;
randomGenerator = ThreadLocal.withInitial(() -> RandomGeneratorFactory.of(finalName).create());
}
@Benchmark
public void testRandomInt(Blackhole blackhole) throws Exception {
blackhole.consume(randomGenerator.get().nextInt());
}
@Benchmark
public void testRandomIntWithBound(Blackhole blackhole) throws Exception {
//注意不取 2^n 這種數字,因爲這種數字一般不會作爲實際應用的範圍,但是底層針對這種數字有優化
blackhole.consume(randomGenerator.get().nextInt(1, 100));
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder().include(TestRandomGenerator.class.getSimpleName()).build();
new Runner(opt).run();
}
}
測試結果:
Benchmark (name) Mode Cnt Score Error Units
TestRandomGenerator.testRandomInt Random thrpt 50 276250026.985 ± 240164319.588 ops/s
TestRandomGenerator.testRandomInt SecureRandom thrpt 50 2362066.269 ± 1277699.965 ops/s
TestRandomGenerator.testRandomInt SplittableRandom thrpt 50 365417656.247 ± 377568150.497 ops/s
TestRandomGenerator.testRandomInt Xoroshiro128PlusPlus thrpt 50 341640250.941 ± 287261684.079 ops/s
TestRandomGenerator.testRandomInt Xoshiro256PlusPlus thrpt 50 343279172.542 ± 247888916.092 ops/s
TestRandomGenerator.testRandomInt L64X256MixRandom thrpt 50 317749688.838 ± 245196331.079 ops/s
TestRandomGenerator.testRandomInt L64X128StarStarRandom thrpt 50 294727346.284 ± 283056025.396 ops/s
TestRandomGenerator.testRandomInt L64X128MixRandom thrpt 50 314790625.909 ± 257860657.824 ops/s
TestRandomGenerator.testRandomInt L64X1024MixRandom thrpt 50 315040504.948 ± 101354716.147 ops/s
TestRandomGenerator.testRandomInt L32X64MixRandom thrpt 50 311507435.009 ± 315893651.601 ops/s
TestRandomGenerator.testRandomInt L128X256MixRandom thrpt 50 187922591.311 ± 137220695.866 ops/s
TestRandomGenerator.testRandomInt L128X128MixRandom thrpt 50 218433110.870 ± 164229361.010 ops/s
TestRandomGenerator.testRandomInt L128X1024MixRandom thrpt 50 220855813.894 ± 47531327.692 ops/s
TestRandomGenerator.testRandomIntWithBound Random thrpt 50 248088572.243 ± 206899706.862 ops/s
TestRandomGenerator.testRandomIntWithBound SecureRandom thrpt 50 1926592.946 ± 2060477.065 ops/s
TestRandomGenerator.testRandomIntWithBound SplittableRandom thrpt 50 334863388.450 ± 92778213.010 ops/s
TestRandomGenerator.testRandomIntWithBound Xoroshiro128PlusPlus thrpt 50 252787781.866 ± 200544008.824 ops/s
TestRandomGenerator.testRandomIntWithBound Xoshiro256PlusPlus thrpt 50 247673155.126 ± 164068511.968 ops/s
TestRandomGenerator.testRandomIntWithBound L64X256MixRandom thrpt 50 273735605.410 ± 87195037.181 ops/s
TestRandomGenerator.testRandomIntWithBound L64X128StarStarRandom thrpt 50 291151383.164 ± 192343348.429 ops/s
TestRandomGenerator.testRandomIntWithBound L64X128MixRandom thrpt 50 217051928.549 ± 177462405.951 ops/s
TestRandomGenerator.testRandomIntWithBound L64X1024MixRandom thrpt 50 222495366.798 ± 180718625.063 ops/s
TestRandomGenerator.testRandomIntWithBound L32X64MixRandom thrpt 50 305716905.710 ± 51030948.739 ops/s
TestRandomGenerator.testRandomIntWithBound L128X256MixRandom thrpt 50 174719656.589 ± 148285151.049 ops/s
TestRandomGenerator.testRandomIntWithBound L128X128MixRandom thrpt 50 176431895.622 ± 143002504.266 ops/s
TestRandomGenerator.testRandomIntWithBound L128X1024MixRandom thrpt 50 198282642.786 ± 24204852.619 ops/s
在之前的結果驗證中,我們已經知道了 SplittableRandom 的在單線程中的性能最好,多線程環境下表現最好的是算法與它類似但是做了多線程優化的 ThreadLocalRandom.
如何選擇隨機算法
原則是,看你的業務場景,所有的隨機組合到底有多少個,在什麼範圍內。然後找大於這個範圍的 Period 中,性能最好的算法。例如,業務場景是一副撲克除了大小王 52 張牌,通過隨機數決定發牌順序:
- 第一張牌:
randomGenerator.nextInt(0, 52)
,從剩餘的 52 張牌選 - 第二張牌:
randomGenerator.nextInt(0, 51)
,從剩餘的 51 張牌選 - 以此類推
那麼一共有 52! 這麼多結果,範圍在 2^225 ~ 2^226 之間。如果我們使用的隨機數生成器的 Period 小於這個結果集,那麼某些牌的順序,我們可能永遠生成不了。所以,我們需要選擇一個 Period > 54! 的隨機數生成器。
微信搜索“我的編程喵”關注公衆號,每日一刷,輕鬆提升技術,斬獲各種offer: