feat: Add SM3MessageDigest JNI implementation with OpenSSL."#13
根据原来的实现和SM3算法的实现细节,由于之前是纯Java实现算法,每个操作都是JVM字节码执行的,要经过64轮循环,每次循环要包括多次位运算、数组访问、函数调用,有很明显的性能限制。所以产生了现在的需求——为现有的 SM3MessageDigest 实现添加基于 JNI 和 OpenSSL 3.5.0 的高性能支持,在保持兼容的前提下提供显著的性能提升。
创建目录: src/java.base/share/native/libsm3digest/
文件1: src/java.base/share/native/libsm3digest/sm3_digest.h // JNI头文件,包含函数声明和结构体定义 文件2: src/java.base/share/native/libsm3digest/sm3_digest.c // JNI实现,使用OpenSSL 3.5.0的EVP接口
本实现基于 JNI 将 OpenSSL 的 SM3 哈希算法封装为 Java 的 MessageDigest 接口,主要遵循了三种设计模式:Wrapper Pattern、Resource Management 和 Error Handling。通过Wrapper Pattern,我们将 OpenSSL 的 EVP API 封装为 JNI 接口,使得 Java 可以方便地调用 C/C++ 编写的加密算法。资源管理方面,使用RAII 模式 来管理 OpenSSL 的上下文,确保在 JNI 层创建的资源能够得到及时释放。异常处理方面,采用统一的异常处理机制,确保当出现问题时,能够通过抛出适当的 Java 异常来反馈错误。核心的依赖包括 JNI 接口(jni.h)、OpenSSL 的高级接口(evp.h)、以及 OpenSSL 的错误处理(err.h)。这些依赖构成了 SM3 哈希算法的底层实现基础。
MessageDigest
jni.h
evp.h
err.h
在 SM3 的 JNI 实现中,关键常量包括 SM3 哈希长度(SM3_DIGEST_LENGTH,即 32 字节)以及 小数据优化阈值(SMALL_DATA_THRESHOLD,即 512 字节)。在性能优化方面,对于 小数据(长度不超过 512 字节),采用 栈缓冲区 来避免 JNI 调用的开销,而对于大数据,则直接通过 JNI 数组指针进行处理,避免不必要的内存拷贝。此外,内存管理也做了优化,确保及时释放资源,以避免内存泄漏。
SM3_DIGEST_LENGTH
SMALL_DATA_THRESHOLD
在 JNI 层实现的核心方法包括初始化、数据更新、最终计算等,所有这些方法都通过 OpenSSL EVP 接口进行加密计算。每个 JNI 方法都附带详细的异常处理逻辑,确保在资源分配失败或操作异常时,能够抛出合适的 Java 异常并进行资源清理。例如,在初始化过程中,如果 OpenSSL 的上下文创建失败,代码会抛出 OpenSSL 异常并回滚。
构建配置, 修改文件: make/lib/Lib-java.base.gmk
# 添加libsm3digest库的构建规则 $(eval $(call SetupJdkLibrary, BUILD_LIBSM3DIGEST, \ NAME := sm3digest, \ CFLAGS := $(CFLAGS_JDKLIB) $(OPENSSL_CFLAGS), \ LIBS := $(OPENSSL_LIBS), \ ))
目前已完善基准测试路径为:/Users/dinghaoran02/txJDK_lab/TencentKona-17/test/micro/org/openjdk/bench/java/security/SM3JMHTest.java(已提交)
JNI加速效果随数据大小递增 1KB: 25.0% 性能提升 64KB: 39.0% 性能提升 1MB: 40.3% 性能提升
可以看出随着数据量提升,性能随之提升
mkdir -p jmh-libs cd jmh-libs curl -L -o jmh-core-1.37.jar https://repo1.maven.org/maven2/org/openjdk/jmh/jmh-core/1.37/jmh-core-1.37.jar curl -L -o jmh-generator-annprocess-1.37.jar https://repo1.maven.org/maven2/org/openjdk/jmh/jmh-generator-annprocess/1.37/jmh-generator-annprocess-1.37.jar curl -L -o jopt-simple-5.0.4.jar https://repo1.maven.org/maven2/net/sf/jopt-simple/jopt-simple/5.0.4/jopt-simple-5.0.4.jar curl -L -o commons-math3-3.6.1.jar https://repo1.maven.org/maven2/org/apache/commons/commons-math3/3.6.1/commons-math3-3.6.1.jar cd ..
curl -L -o jmh-generator-bytecode-1.37.jar https://repo1.maven.org/maven2/org/openjdk/jmh/jmh-generator-bytecode/1.37/jmh-generator-bytecode-1.37.jar curl -L -o jmh-generator-reflection-1.37.jar https://repo1.maven.org/maven2/org/openjdk/jmh/jmh-generator-reflection/1.37/jmh-generator-reflection-1.37.jar
./build/macosx-aarch64-server-release/images/jdk/bin/javac \ -cp jmh-libs/jmh-core-1.37.jar:jmh-libs/jmh-generator-annprocess-1.37.jar \ test/micro/org/openjdk/bench/java/security/SM3JMHTest.java
./build/macosx-aarch64-server-release/images/jdk/bin/java \ -cp jmh-generator-bytecode-1.37.jar:jmh-generator-reflection-1.37.jar:jmh-libs/jmh-core-1.37.jar:test/micro \ org.openjdk.jmh.generators.bytecode.JmhBytecodeGenerator test/micro test/micro test/micro
./build/macosx-aarch64-server-release/images/jdk/bin/javac \ -cp jmh-libs/jmh-core-1.37.jar:test/micro \ --add-exports=java.base/sun.security.provider=ALL-UNNAMED \ test/micro/org/openjdk/bench/java/security/jmh_generated/*.java
./build/macosx-aarch64-server-release/images/jdk/bin/java \ -cp test/micro:jmh-libs/jmh-core-1.37.jar:jmh-libs/jopt-simple-5.0.4.jar:jmh-libs/commons-math3-3.6.1.jar \ --add-exports=java.base/sun.security.provider=ALL-UNNAMED \ org.openjdk.jmh.Main "SM3JMHTest"
[0.006s][info][gc] Using G1 [0.006s][info][gc,init] Version: 17.0.15-internal+0-adhoc.dinghaoran02.TencentKona-17 (release) [0.006s][info][gc,init] CPUs: 8 total, 8 available [0.006s][info][gc,init] Memory: 16384M [0.006s][info][gc,init] Large Page Support: Disabled [0.006s][info][gc,init] NUMA Support: Disabled [0.006s][info][gc,init] Compressed Oops: Enabled (Zero based) [0.006s][info][gc,init] Heap Region Size: 2M [0.006s][info][gc,init] Heap Min Capacity: 8M [0.006s][info][gc,init] Heap Initial Capacity: 256M [0.006s][info][gc,init] Heap Max Capacity: 4G [0.006s][info][gc,init] Pre-touch: Disabled [0.006s][info][gc,init] Parallel Workers: 8 [0.006s][info][gc,init] Concurrent Workers: 2 [0.006s][info][gc,init] Concurrent Refinement Workers: 8 [0.006s][info][gc,init] Periodic GC: Disabled [0.010s][info][gc,metaspace] CDS archive(s) mapped at: [0x0000009000000000-0x0000009000bc0000-0x0000009000bc0000), size 12320768, SharedBaseAddress: 0x0000009000000000, ArchiveRelocationMode: 1. [0.010s][info][gc,metaspace] Compressed class space mapped at: 0x0000009001000000-0x0000009041000000, reserved size: 1073741824 [0.010s][info][gc,metaspace] Narrow klass base: 0x0000009000000000, Narrow klass shift: 0, Narrow klass range: 0x100000000 [10.574s][info][gc,start ] GC(0) Pause Young (Normal) (G1 Evacuation Pause) [10.574s][info][gc,task ] GC(0) Using 6 workers of 8 for evacuation [10.575s][info][gc,phases ] GC(0) Pre Evacuate Collection Set: 0.1ms [10.575s][info][gc,phases ] GC(0) Merge Heap Roots: 0.0ms [10.575s][info][gc,phases ] GC(0) Evacuate Collection Set: 1.3ms [10.575s][info][gc,phases ] GC(0) Post Evacuate Collection Set: 0.1ms [10.576s][info][gc,phases ] GC(0) Other: 0.2ms [10.576s][info][gc,heap ] GC(0) Eden regions: 11->0(17) [10.576s][info][gc,heap ] GC(0) Survivor regions: 0->1(2) [10.576s][info][gc,heap ] GC(0) Old regions: 0->0 [10.576s][info][gc,heap ] GC(0) Archive regions: 2->2 [10.576s][info][gc,heap ] GC(0) Humongous regions: 1->1 [10.576s][info][gc,metaspace] GC(0) Metaspace: 3479K(3648K)->3479K(3648K) NonClass: 3094K(3136K)->3094K(3136K) Class: 385K(512K)->385K(512K) [10.576s][info][gc ] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 25M->5M(260M) 1.880ms [10.576s][info][gc,cpu ] GC(0) User=0.00s Sys=0.00s Real=0.00s [25.694s][info][gc,start ] GC(1) Pause Young (Normal) (G1 Evacuation Pause) [25.695s][info][gc,task ] GC(1) Using 6 workers of 8 for evacuation [25.696s][info][gc,phases ] GC(1) Pre Evacuate Collection Set: 0.1ms [25.696s][info][gc,phases ] GC(1) Merge Heap Roots: 0.1ms [25.696s][info][gc,phases ] GC(1) Evacuate Collection Set: 1.0ms [25.696s][info][gc,phases ] GC(1) Post Evacuate Collection Set: 0.2ms [25.696s][info][gc,phases ] GC(1) Other: 0.1ms [25.696s][info][gc,heap ] GC(1) Eden regions: 17->0(75) [25.696s][info][gc,heap ] GC(1) Survivor regions: 1->1(3) [25.696s][info][gc,heap ] GC(1) Old regions: 0->0 [25.696s][info][gc,heap ] GC(1) Archive regions: 2->2 [25.696s][info][gc,heap ] GC(1) Humongous regions: 1->1 [25.696s][info][gc,metaspace] GC(1) Metaspace: 3485K(3648K)->3485K(3648K) NonClass: 3098K(3136K)->3098K(3136K) Class: 386K(512K)->386K(512K) [25.696s][info][gc ] GC(1) Pause Young (Normal) (G1 Evacuation Pause) 39M->5M(260M) 1.509ms [25.696s][info][gc,cpu ] GC(1) User=0.00s Sys=0.00s Real=0.00s [45.374s][info][gc,heap,exit] Heap [45.374s][info][gc,heap,exit] garbage-first heap total 266240K, used 60154K [0x0000000700000000, 0x0000000800000000) [45.374s][info][gc,heap,exit] region size 2048K, 28 young (57344K), 1 survivors (2048K) [45.374s][info][gc,heap,exit] Metaspace used 3584K, committed 3776K, reserved 1114112K [45.374s][info][gc,heap,exit] class space used 397K, committed 512K, reserved 1048576K
日志中包含了两次较为显著的 年轻代回收信息。
采用JMH的profilers进行观测(7.23):
你的commit似乎没有修改JDK的make文件。 请同时提供性能测试用的JMH程序,并展示修改前后的性能对比。
看起来,这是一个测试文件。但它的位置并不正确。 目前SM3的测试文件在目录test/jdk/sm/crypto/SM3中。
test/jdk/sm/crypto/SM3
好的导师,在新的提交中我将删去
目前已完善基准测试,并移至pr描述之中。
集成使用JDK本身的make文件,确保在构建JDK同时,也能够自动地构建出这个JNI库。 只需要支持Linux x86_64和aarch64平台。
我之前的提交对Lib.gmk做了如下更改,请问是否满足导师您的要求呢:
ifneq ($(filter $(call isTargetOs, linux) $(call isTargetOs, macosx), true), ) ifneq ($(filter x86_64 aarch64, $(OPENJDK_TARGET_CPU)), ) $(eval $(call SetupJdkLibrary, BUILD_LIBSM3JNI, \ NAME := sm3jni, \ OPTIMIZATION := HIGH, \ CFLAGS := $(CFLAGS_JDKLIB) -O3 \ $(addprefix -I, $(call FindSrcDirsForLib, java.base, sm3jni)) \ $(if $(KONA_OPENSSL_HOME), -I$(KONA_OPENSSL_HOME)/include), \ LDFLAGS := $(LDFLAGS_JDKLIB) \ $(call SET_SHARED_LIBRARY_ORIGIN) \ $(if $(KONA_OPENSSL_HOME), -L$(KONA_OPENSSL_HOME)/lib), \ LIBS_linux := -lcrypto -lssl, \ LIBS_macosx := -lcrypto -lssl, \ ))
SMEntries注册了SM3MessageDigest。
SMEntries
SM3MessageDigest
新提交中已移除
参考其它的JNI实现,是否有必要为JNI函数提供.h文件?
感谢导师提醒🙏。经过查阅资料,我看到OpenJDK构建系统在编译过程中使用javac-h自动生成JNI头文件。其原理为当OpenJDK编译包含本机方法的Java类时,它会使用-h标志自动生成相应的C头文件。比如像WinCAPISeedGenerator.c的头文件就没找到。
@johnjiang 目前已提交Lib.gmk文件,这个文件集成了SM3 JNI库到JDK构建系统
我刚才提交了如下的commit, johnshajiang@3682ce31
3682ce31
它支持了在构建JDK时使用--with-openssl指定一个本地的OpenSSL目录。 并且提供了OpenSSLUtil用于帮助加载libcrypto.so。在运行时,需要使用系统属性jdk.openssl.cryptoLibPath去指定该文件的位置。 这样,你的PR只需要关注JNI部分,而不需要关注OpenSSL的加载了。
--with-openssl
OpenSSLUtil
libcrypto.so
jdk.openssl.cryptoLibPath
请先同步代码,然后再更新你的PR。
我刚才提交了如下的commit, johnshajiang@3682ce31 它支持了在构建JDK时使用--with-openssl指定一个本地的OpenSSL目录。 并且提供了OpenSSLUtil用于帮助加载libcrypto.so。在运行时,需要使用系统属性jdk.openssl.cryptoLibPath去指定该文件的位置。 这样,你的PR只需要关注JNI部分,而不需要关注OpenSSL的加载了。 请先同步代码,然后再更新你的PR。
@johnjiang 已同步相关代码
我的commit不应该成为你的PR的一部分。
@johnjiang 收到🫡
@johnjiang 已移除您的提交,并将基准测试结果等内容整合至pr描述之中
./build/macosx-aarch64-server-release/images/jdk/bin/java -cp test/micro:jmh-libs/jmh-core-1.37.jar:jmh-libs/jopt-simple-5.0.4.jar:jmh-libs/commons-math3-3.6.1.jar --add-exports=java.base/sun.security.provider=ALL-UNNAMED org.openjdk.jmh.Main "SM3JMHTest"
其实,OpenJDK的make是可以直接执行microbenchmark的。
之前以为已经合并了我的commit,刚才合并进去了 :-( 9d01dbc6
9d01dbc6
@johnjiang 好嘞,我将继续根据您对该issue下pr的点评优化代码
-cp test/micro:jmh-libs/jmh-core-1.37.jar:jmh-libs/jopt-simple-5.0.4.jar:jmh-libs/commons-math3-3.6.1.jar \ --add-exports=java.base/sun.security.provider=ALL-UNNAMED \ org.openjdk.jmh.Main "SM3JMHTest" 其实,OpenJDK的make是可以直接执行microbenchmark的。
-cp test/micro:jmh-libs/jmh-core-1.37.jar:jmh-libs/jopt-simple-5.0.4.jar:jmh-libs/commons-math3-3.6.1.jar \ --add-exports=java.base/sun.security.provider=ALL-UNNAMED \ org.openjdk.jmh.Main "SM3JMHTest"
@johnjiang 收到;我使用“-Xlog:gc*”参数观测了基准测试的gc情况,测试结果放在了pr的概述;我想请问下,我需要对两种实现情况分别基准测试来观测gc情况嘛,还是有其他更好的方案呢?感谢导师解答🙏
684c2011 上面的commit在test目录下增加了OpenSSL的crypto lib,可用于测试。
684c2011
我使用“-Xlog:gc*”参数观测了基准测试的gc情况,测试结果放在了pr的概述;我想请问下,我需要对两种实现情况分别基准测试来观测gc情况嘛,还是有其他更好的方案呢?感谢导师解答
不是使用GC log。JMH提供的profilers中支持展示gc时间。你可以去查一下。
@johnjiang 好的导师,结果已放在pr详情🙏
@johnjiang(jiangsha) 导师好,有空帮忙code review一下看看有什么问题,我继续改进🙏
可以使用make执行JMH测试,并不需要main方法。
sun.security.ec.SM2KeyAgreement目前正在使用SM3Engine。 如果删除了public,编译不会报错吗?
public
SM2Engine也会使用,从目前PR的改动来看,不能删除public,否则会编译报错
SM2Engine
看了下SM2KeyAgreement 确实在直接使用 SM3Engine(第59行 private final SM3Engine sm3 = new SM3Engine(); )感觉可以和实现SM2KeyAgreement 的同学探讨下更好的方案🙏
我认为理想的解决方案是重构 SM2KeyAgreement 使用 MessageDigest 接口而不是直接依赖 SM3Engine 实现类,但是考虑到那边的同学也在进行开发,可以先告知他们这个信息。
我在另一个PR中也提到了使用MessageDigest以对SM3Engine解偶。
你可能直接修改SM2KeyAgreement和SM2Engine,而不用关心其它的PR会作何种修改。 这种潜在的冲突在现实工作中本来就是存在的。 如果你的PR先合入了,其他人就得先同步你的代码,解决文件冲突;反之亦然。
没有导入这个类。
修改代码之后,请务必进行构建与测试。
@cnb.awOE5sooQFA(momo) 由于 #12 已经解决了issue #7,故关闭该PR。 仍然非常感谢你的关注与工作!
[Implement SM3 with OpenSSL]开发流程(已完成,优化中)
1. 需求概述
(一)功能需求描述
根据原来的实现和SM3算法的实现细节,由于之前是纯Java实现算法,每个操作都是JVM字节码执行的,要经过64轮循环,每次循环要包括多次位运算、数组访问、函数调用,有很明显的性能限制。所以产生了现在的需求——为现有的 SM3MessageDigest 实现添加基于 JNI 和 OpenSSL 3.5.0 的高性能支持,在保持兼容的前提下提供显著的性能提升。
2. 开发流程
(一) Java层实现
javaApplyCancelmap.put("MessageDigest.SM3", "sun.security.provider.SM3MessageDigest");
map.put("Alg.Alias.MessageDigest.1.2.156.10197.1.401", "SM3");
(二) Native层实现
创建目录: src/java.base/share/native/libsm3digest/
文件1: src/java.base/share/native/libsm3digest/sm3_digest.h
// JNI头文件,包含函数声明和结构体定义
文件2: src/java.base/share/native/libsm3digest/sm3_digest.c
// JNI实现,使用OpenSSL 3.5.0的EVP接口
总体实现概述
本实现基于 JNI 将 OpenSSL 的 SM3 哈希算法封装为 Java 的
MessageDigest接口,主要遵循了三种设计模式:Wrapper Pattern、Resource Management 和 Error Handling。通过Wrapper Pattern,我们将 OpenSSL 的 EVP API 封装为 JNI 接口,使得 Java 可以方便地调用 C/C++ 编写的加密算法。资源管理方面,使用RAII 模式 来管理 OpenSSL 的上下文,确保在 JNI 层创建的资源能够得到及时释放。异常处理方面,采用统一的异常处理机制,确保当出现问题时,能够通过抛出适当的 Java 异常来反馈错误。核心的依赖包括 JNI 接口(jni.h)、OpenSSL 的高级接口(evp.h)、以及 OpenSSL 的错误处理(err.h)。这些依赖构成了 SM3 哈希算法的底层实现基础。大数据小数据的优化
在 SM3 的 JNI 实现中,关键常量包括 SM3 哈希长度(
SM3_DIGEST_LENGTH,即 32 字节)以及 小数据优化阈值(SMALL_DATA_THRESHOLD,即 512 字节)。在性能优化方面,对于 小数据(长度不超过 512 字节),采用 栈缓冲区 来避免 JNI 调用的开销,而对于大数据,则直接通过 JNI 数组指针进行处理,避免不必要的内存拷贝。此外,内存管理也做了优化,确保及时释放资源,以避免内存泄漏。错误处理相关完善
在 JNI 层实现的核心方法包括初始化、数据更新、最终计算等,所有这些方法都通过 OpenSSL EVP 接口进行加密计算。每个 JNI 方法都附带详细的异常处理逻辑,确保在资源分配失败或操作异常时,能够抛出合适的 Java 异常并进行资源清理。例如,在初始化过程中,如果 OpenSSL 的上下文创建失败,代码会抛出 OpenSSL 异常并回滚。
构建配置, 修改文件: make/lib/Lib-java.base.gmk
3. 性能测试
测试文件路径
目前已完善基准测试路径为:/Users/dinghaoran02/txJDK_lab/TencentKona-17/test/micro/org/openjdk/bench/java/security/SM3JMHTest.java(已提交)
测试效果
JNI加速效果随数据大小递增
1KB: 25.0% 性能提升
64KB: 39.0% 性能提升
1MB: 40.3% 性能提升
测试步骤
日志中包含了两次较为显著的 年轻代回收信息。
采用JMH的profilers进行观测(7.23):