Java 內幕新聞第二期深度解讀

這是由 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:

其中最主要的應用就是使用了 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 即:

image

image

測試結果:

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 的兼容思路都很類似,也比較簡單,即:

  1. 實現一個針對 Record 的專用的 Serializer 以及Deserializer。
  2. 通過反射(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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章