【问题标题】:Java AES-GCM very slow compared to AES-CTR与 AES-CTR 相比,Java AES-GCM 非常慢
【发布时间】:2019-07-06 16:21:16
【问题描述】:

考虑以下代码:

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.IvParameterSpec;
import java.security.SecureRandom;

public class AES_Mod_Speed {
    // AES parameters
    private static final int AES_KEY_SIZE = 128; // in bits
    private static final int AES_COUNTER_SIZE = 16; // in bytes
    private static final int GCM_NONCE_LENGTH = 12; // in bytes. 12 is the recommended value.
    private static final int GCM_TAG_LENGTH = 16 * 8; // in bits

    public static void main(String[] args) throws Exception {
        SecureRandom sr = new SecureRandom();

        KeyGenerator kg = KeyGenerator.getInstance("AES");
        kg.init(AES_KEY_SIZE);
        SecretKey key = kg.generateKey();

        byte[] counter = new byte[AES_COUNTER_SIZE];
        Cipher aes_ctr = Cipher.getInstance("AES/CTR/NoPadding");

        byte[] nonce = new byte[GCM_NONCE_LENGTH];
        Cipher aes_gcm = Cipher.getInstance("AES/GCM/NoPadding");

        for (int i = 0; i < 10; i++) {
            sr.nextBytes(counter);
            aes_ctr.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(counter));
            speedTest(aes_ctr);
        }

        System.out.println("-----------------------------------------");

        for (int i = 0; i < 10; i++) {
            sr.nextBytes(nonce);
            aes_gcm.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(GCM_TAG_LENGTH, nonce));
            speedTest(aes_gcm);
        }

    }

    private static void speedTest(Cipher cipher) throws Exception {
        byte[] ptxt = new byte[1 << 26];
        long start, end;

        start = System.nanoTime();
        cipher.doFinal(ptxt);
        end = System.nanoTime();


        System.out.printf("%s took %f seconds.\n",
                cipher.getAlgorithm(),
                (end - start) / 1E9);
    }
}

结果(Java 11.0.2):


AES/CTR/NoPadding took 0.259894 seconds.
AES/CTR/NoPadding took 0.206136 seconds.
AES/CTR/NoPadding took 0.247764 seconds.
AES/CTR/NoPadding took 0.196413 seconds.
AES/CTR/NoPadding took 0.181117 seconds.
AES/CTR/NoPadding took 0.194041 seconds.
AES/CTR/NoPadding took 0.181889 seconds.
AES/CTR/NoPadding took 0.180970 seconds.
AES/CTR/NoPadding took 0.180546 seconds.
AES/CTR/NoPadding took 0.179797 seconds.
-----------------------------------------
AES/GCM/NoPadding took 0.961051 seconds.
AES/GCM/NoPadding took 0.952866 seconds.
AES/GCM/NoPadding took 0.963486 seconds.
AES/GCM/NoPadding took 0.963280 seconds.
AES/GCM/NoPadding took 0.961424 seconds.
AES/GCM/NoPadding took 0.977850 seconds.
AES/GCM/NoPadding took 0.961449 seconds.
AES/GCM/NoPadding took 0.957542 seconds.
AES/GCM/NoPadding took 0.967129 seconds.
AES/GCM/NoPadding took 0.959292 seconds.

这很奇怪,因为 GCM 几乎比 CTR 慢 5 倍(用于加密 1&lt;&lt;26 字节,即 64 MB)。通过 OpenSSL 1.1.1a 进行速度测试,我发出命令openssl speed -evp aes-128-ctropenssl speed -evp aes-128-gcm,得到以下结果:

The 'numbers' are in 1000s of bytes per second processed.
type             16 bytes     64 bytes    256 bytes   1024 bytes   8192 bytes  16384 bytes
aes-128-ctr     463059.16k  1446320.32k  3515070.12k  5182218.92k  6063797.59k  6210150.19k
aes-128-gcm     480296.99k  1088337.47k  2531854.17k  4501395.11k  5940079.27k  6087589.89k

可以看出,GCM 只比 CTR 慢一点,尤其是对于较大的明文。

为什么 AES-GCM 的 Java 实现比 AES-CTR 慢?我错过了什么吗?

PS:我也使用Java JMH 进行微基准测试,结果相似。

另请参阅this answer,其中 OP 解释了早期 JDK 中如何解决 AES 性能问题。

【问题讨论】:

  • 您是否消除了所有异常值?例如。这是调试版本吗?在开始记录和平均之前,您是否每次运行基准测试几次?可能是您正在记录一个 O(1) 设置步骤,该步骤不会随加密数据的大小而扩展。
  • @LukeJoshuaPark:感谢您的及时回复。这不是调试版本,我在 for 循环中运行代码 10 次以确保热身没有问题。结果是一致的。请看修改后的代码。

标签: java performance encryption


【解决方案1】:

这与this answer中描述的问题相同。

调用加密方法的次数不足以编译 JIT。您看到的是纯解释执行的结果。尝试测量加密较小数组的更多迭代。或者只是添加虚拟循环来“预热”编译器。

例如,在主基准测试循环之前插入以下循环。它将执行 doFinal 足够的次数以确保它被编译。

    // Warm-up
    for (int i = 0; i < 100000; i++) {
        sr.nextBytes(nonce);
        aes_gcm.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(GCM_TAG_LENGTH, nonce));
        aes_gcm.doFinal(new byte[16]);
    }

只要 JIT 编译器完成其工作,后续基准测试的结果就会好得多。实际上,关键的AES加密方式在JDK中是intrinsics; HotSpot JVM 为它们提供了特殊的实现,用优化的汇编语言编写并具有 AVX 和 AES-NI 指令集。

在我的笔记本电脑上,预热后基准测试速度提高了一个数量级:

AES/GCM/NoPadding took 0.108993 seconds.
AES/GCM/NoPadding took 0.089832 seconds.
AES/GCM/NoPadding took 0.063606 seconds.
AES/GCM/NoPadding took 0.061044 seconds.
AES/GCM/NoPadding took 0.073603 seconds.
AES/GCM/NoPadding took 0.063733 seconds.
AES/GCM/NoPadding took 0.058680 seconds.
AES/GCM/NoPadding took 0.058996 seconds.
AES/GCM/NoPadding took 0.058327 seconds.
AES/GCM/NoPadding took 0.058664 seconds.

【讨论】:

  • 很好的答案,谢谢!执行java -XX:+PrintFlagsFinal -version | grep CompileThreshold,我注意到在我的环境中,CompileThreshold = 10,000Tier3AOTCompileThreshold = Tier4CompileThreshold = 15,000。总和为 10,000 + 15,000 + 15,000 = 40,000。我尝试了 40,000 次迭代的热身,一切正常。但任何低于这个值的东西要么“总是太低”,要么“有时太低”。我的推理正确吗?
  • @M.S.Dousti CompileThreshold does not matter。 C1 编译通常在几百次调用之后开始,C2 在几千次之后开始。但是编译器在后台工作,而主程序继续在解释器中运行。安装编译代码的时间取决于许多因素,包括 CodeCache 容量、可用 CPU 数量和当前系统负载。无法保证确保切换到已编译代码的调用次数。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-08-08
  • 2018-07-31
  • 1970-01-01
  • 1970-01-01
  • 2020-08-13
  • 2015-06-09
相关资源
最近更新 更多