提问人:JANO 提问时间:11/15/2023 最后编辑:JANO 更新时间:11/15/2023 访问量:58
Java Base64 解码比大文件的编码慢得多
Java Base64 Decoding much slower than Encoding for large files
问:
目前,我正在研究一个现有的应用程序,对高达 100MB 的文件进行编码和解码,并将其与 Base64 进行解码。我注意到使用 Java 的 Base64 类进行编码和解码在性能上存在巨大差异。解码速度可能比编码慢 100 倍以上。根据我的理解,从 Base64 解码相同内容时,没有增加复杂性(除了稍微大 (~33%) 的输入文件)。
我为三种文件大小的编码/解码计时,并将其与在我的 Ubuntu 终端上使用预装的 bash64 命令进行编码/解码进行了比较。这表明使用我的终端时,编码和解码之间并没有真正的区别。然而,Java Base64 解码似乎明显不合时宜。
我的测量:
文件大小 | 100 KB | 1 兆字节 | 61兆字节 |
---|---|---|---|
Java Base64 编码 | 5 毫秒 | 36 毫秒 | 325 毫秒 |
Java Base64 解码 | 151 毫秒 | 1 522 毫秒 | 83 134 毫秒 |
终端 Base64 编码 | 8 毫秒 | 21 毫秒 | 257 毫秒 |
终端 Base64 解码 | 10 毫秒 | 46毫秒 | 385毫秒 |
Java 的解码方法这么慢有什么具体原因吗?我不确定是什么会使 Java 的解码方法比终端命令的 base64 解码更复杂。我认为“大”文件的编码和解码不是典型的用例,但我仍然希望获得合理的性能。
我将不胜感激任何见解或建议来解决我的问题!
Java 复制器:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Base64;
public class Base64Test {
public static void main(String[] args) throws IOException {
String inputFileName = "input.txt";
String outputFileName = "encoded.txt";
String outputFileName2 = "decoded.txt";
long time1 = System.nanoTime();
try (FileInputStream in = new FileInputStream(inputFileName);
FileOutputStream out = new FileOutputStream(outputFileName)) {
in.transferTo(Base64.getEncoder().wrap(out));
}
System.out.println("Base64 encoding : " + outputFileName + " Time: " + (System.nanoTime() - time1) / 1000 / 1000);
long time2 = System.nanoTime();
try (FileInputStream fis = new FileInputStream(outputFileName);
FileOutputStream out = new FileOutputStream(outputFileName2)) {
InputStream in = Base64.getDecoder().wrap(fis);
in.transferTo(out);
}
System.out.println("Base64 decoding: " + outputFileName + " Time: " + (System.nanoTime() - time2) / 1000 / 1000);
}
}
终端命令:
$ time base64 input.txt > encoded.txt
$ time base64 --decode encoded.txt > decoded.txt
系统:
- Ubuntu 22.04
- 16GB 内存
- OpenJDK 17.0.9 版本
- 智能 2023.2.5
输入文件由我用键盘输入的随机字符组成,然后复制直到达到特定的文件大小。我还尝试了使用 JDK 17 的不同 Ubuntu 笔记本,并得到了相同的结果。
编辑:缓冲结果:
文件大小 | 100 KB | 1 兆字节 | 61兆字节 | 100 兆字节 |
---|---|---|---|---|
Java Base64 编码 | 5 毫秒 | 30 毫秒 | 311 毫秒 | 423 毫秒 |
Java Base64 解码 | 12 毫秒 | 46 毫秒 | 1684 毫秒 | 2875 毫秒 |
答:
这里有多个问题。
缓冲区
调用(例如,读取单个字节)可能在某种程度上或非常低效,具体取决于基础资源。它可能需要向操作系统询问一个字节,这涉及一些需要一点时间的基础设施,但如果你为每个字节都这样做,它真的会加起来。如果底层资源是基于块的,则需要花费大量时间,因此读取整个块,然后抛弃除您想要的一个字节之外的所有内容。如果执行此操作:.read()
.read()
for (int i = 0; i < 1024; i++) in.read();
并表示在 1024 大小的块中工作的资源,它将读取 1024 个字节,扔掉除第一个字节之外的所有字节,然后返回该字节。冲洗并重复:读取 1024*1024 字节。从字面上看,它比 慢 1024 。in
byte[] b = new byte[1024]; in.read(b);
如果你想编写的代码只是更易于编写,而不是 ,没问题!将资源包装在 中,BIS 将负责制作一个小缓冲区。 在 BIS 上导致底层资源,然后对 BIS 的进一步调用只是全内存、全 java 事务,BIS 只是从其缓冲区返回字节,直到您获得所有字节,然后下一次调用才会再次到达底层资源。in.read()
in.read(buffer)
new BufferedInputStream()
.read()
read(sizableBuffer)
.read()
.read()
transferTo
不需要缓冲器;它本身是“智能”的,并且可以在补丁中传输。
但是,深入研究 的来源,甚至其实现都需要底层资源。奇怪的是,编码器确实缓冲了更多(大约 8192 字节,这至少可以真正消除边缘,对于大多数基于块的系统来说已经绰绰有余了)。Base64.getDecoder().wrap(someInputStream)
.read(sizableByteArray)
in.read()
只需将它们包裹起来,这个问题就会消失,这会给你带来 100 到 1000 倍的加速。new BufferedInputStream
new BufferedOutputStream
微基准测试
那些关于计算机如何工作的简单解释是彻头彻尾的谎言。他们根本不是这样工作的。CPU 执行流水线和预测性指针前工作,JVM 运行速度很慢(甚至比您想象的还要慢:例如,它执行一些簿记,例如添加代码来计算某个触发频率和跳过频率)。这是因为绝大多数应用程序实际上将 99% 的时间花在 1% 的代码上,因此缓慢运行代码绝对没有任何区别。除了那 1%,它有很大的不同。这种簿记让 JVM 知道 1% 是什么,而分支的东西意味着 JVM 可以为它制作非常低效的字节码,它会这样做,然后运行它。这称为JIT(Just-in-time)/热点。if
问题是,在启动时,JVM 还不知道。因此,“热路径”的性能(过于简化)如下所示:
- 迭代 1-1000:每次迭代需要 50 毫秒。
- 迭代 1001:需要 800 毫秒,因为 JVM 编译了一个由所有簿记告知的机器代码版本。
- 迭代 1002+:每次迭代需要 4 毫秒。
您希望迭代足够多,以使迭代 1-1001 的影响消失在噪音中。但是你怎么知道你已经过了“JIT预热阶段”呢?这很棘手。而这只是性能测量比“启动计时器、运行代码、停止计时器”复杂得多的众多原因之一。
除非你花 5 年时间研究 JVM 和 CPU 设计,否则你将对此毫无希望,无法胜任这项工作。幸运的是,您不必这样做:您可以使用现有工具为您执行性能测试。最常用的微基准测试工具是JMH。按照本教程操作。
Base64 和磁盘的性质
对 Base64 进行编码将每个 3 字节的块转换为 4 个字节。这意味着你写的比你读的多 33%。(读取 3MB,写入 4MB)。
解码将每个 4 字节的块变成 3 个字节。这意味着你阅读的比你写的多 33%。(读取 4MB,写入 3MB)。
如果底层源具有不同的读取和写入性能特征,则其中一个源自然会比另一个慢。
但。。这是一个很大的差距!
我会解决上述所有问题(添加缓冲,将所有这些代码挂到 JMH harnes 中),但与命令行相比,10x+ 因素仍然很大。以下是我将采取的后续步骤:base64
- 在新下载的 OpenJDK21 安装上运行此命令。
- 使用番石榴的解码器而不是内置的解码器。
如果差异消失了,您现在可以安全地“责怪”劣质实现。
评论