编译小 Java 文件的最快方法
Fastest way to compile small Java files
我希望以编程方式编译(不是通过 IDE)一些小的 类 例如:
public class Sum {
public int sum(int[] nums) {
int total = 0;
for (int i = 0; i < nums.length; i++) {
total += nums[i];
}
return total;
}
}
运行 javac Sum.java
在 macOS(2.3 GHz Intel Core i5)上需要 429ms
,但在具有 3VCPU 和 6G RAM 的 Kubernetes 容器(t3.xlarge)中,它需要 640ms
。造成这种差异的原因是什么?
我尝试了不同的 java 版本并尝试使用 javax.tools.JavaCompiler
但是文件很少,编译 3 个像这样的小文件最多需要 2 秒。
要最快地编译这些小文件,hardware/software 的最佳配置是什么?
Wahaaaaaay 回到遥远的过去,我们有 'incremental compilers'。这些将 运行 持续 。您可以在一天开始或其他时间启动它们一次,然后永远离开它们 运行ning。然后他们要么等待任何键盘输入(回车),要么设置一些文件监视(当给定目录中的任何文件被修改时触发的挂钩)并在每次需要时重新编译。这样可以节省 VM 预热和 VM 引导加载。
我认为随着构建工具(尤其是 maven 和 gradle)的流行,它们大多消失了。该模型不能很好地与构建系统相结合;构建系统 本身 应该是增量单元(应该 运行 一次并保持加载),因此还必须内部化编译器以避免必须初始化和预热 VM每次都需要编译。
鉴于您要编译一个简单的单个小文件,VM 初始化和编译器预热最有可能占 640 毫秒的 99%。增量编译器正是您所需要的。
增量编译器仍然存在,但我所知道的唯一现代编译器是 eclipse:如果您在 eclipse 中保存文件,eclipse 几乎会立即使用它自己的内置编译器编译它。那是..显然不是你要找的。
如果增量的东西仍然存在并且维护得很好,您可以在网上搜索。您还应该能够手动执行此操作,也许:有一个 java 应用程序可以执行以下操作:监听系统输入中的任何完整行(例如,使用 Scanner 和 .useDelimiter("\r?\n"); .next()
,将这些行视为文件或dirs,将扫描这些文件以查找任何新的或更改的文件,对所有更改的内容调用 javax.tools.JavaCompiler
,然后将 hashmap 映射文件更新为时间戳,然后返回睡眠等待更多 System.in 流量,使用该地图决定缓存。或者忘记地图并采用旧样式:找到 class 文件,将其时间戳与源文件的时间戳进行比较,如果 class 文件的时间戳较新,则没有需要重新编译它。这并不能完全映射到 java 的工作方式(1 个源文件可以生成多个 class 文件,并且无法从 class 文件名中确定哪个源文件生成了它,虽然如果你添加足够的调试信息并打开它你可能可以,但是 class 文件是一种非常复杂的格式所以这样做是 non-trivial) - 所以我会做 hashmap , 即使合作昔日的 mpilers 使用 'compare class file last-modified with source file' 选项。
或者它可以打开一个 TCP/IP 端口。
请注意,eclipse 也以 'language server' 形式出现,这正是 VSCode 的工作方式:当您在 VSCode 中编辑 java 代码时,VS 启动无头 eclipse 并且用它来做 'intelligent' 关于 java 的所有事情:重构脚本、实时编译、错误、警告和一般 linting 服务、导航服务(例如 'open type'、'find callers' )、调试器等。编译是 eclipse-as-a-language-server 提供的众多服务的一部分,并且 ecj 比 javac 启动快 4 到 10 倍(VM 初始化和预热后的实际编译速度可能只有百分之几那 640 毫秒,因此不相关,但我不知道你需要多快)。
我刚凭经验得到的数字是5ms/file...
如果您有很多 Java 个文件要编译,那么您可能会关心这个。但如果是这样的话,那么你所说的时间数字就不现实了。我写这个程序来编译你的测试程序 1000 次(我创建了 1000 java 文件定义 类 Sum0 到 Sum999.
import javax.tools.JavaCompiler;
import javax.tools.ToolProvider;
public class T {
public static void main(String[] args) {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
int N = 1000;
String[] files = new String[N];
for (int i = 0 ; i < N ; i++) {
files[i] = String.format("stage/Sum%d.java", i);
}
int result = compiler.run(null, null, null, files);
System.out.println("Compile result code = " + result);
}
}
我运行它,这是运行的时间数字:
> ls stage/*.class | wc -l
ls: stage/*.class: No such file or directory
0
> time java T
Compile result code = 0
real 0m2.511s
user 0m4.737s
sys 0m0.549s
> ls stage/*.class | wc -l
1000
因此总 运行时间为 4.737 秒/1000 = .005 秒 = 5 毫秒/文件。我能想到的是,您一次只编译一个文件,在这种情况下,所有时间都是某种 startup/teardown 成本,谁在乎呢。
最重要的是,您可以在几秒钟内编译任意数量的 Java 文件,所以不用担心这个。
这是 运行 几代以前的 MacBook Pro。
我希望以编程方式编译(不是通过 IDE)一些小的 类 例如:
public class Sum {
public int sum(int[] nums) {
int total = 0;
for (int i = 0; i < nums.length; i++) {
total += nums[i];
}
return total;
}
}
运行 javac Sum.java
在 macOS(2.3 GHz Intel Core i5)上需要 429ms
,但在具有 3VCPU 和 6G RAM 的 Kubernetes 容器(t3.xlarge)中,它需要 640ms
。造成这种差异的原因是什么?
我尝试了不同的 java 版本并尝试使用 javax.tools.JavaCompiler
但是文件很少,编译 3 个像这样的小文件最多需要 2 秒。
要最快地编译这些小文件,hardware/software 的最佳配置是什么?
Wahaaaaaay 回到遥远的过去,我们有 'incremental compilers'。这些将 运行 持续 。您可以在一天开始或其他时间启动它们一次,然后永远离开它们 运行ning。然后他们要么等待任何键盘输入(回车),要么设置一些文件监视(当给定目录中的任何文件被修改时触发的挂钩)并在每次需要时重新编译。这样可以节省 VM 预热和 VM 引导加载。
我认为随着构建工具(尤其是 maven 和 gradle)的流行,它们大多消失了。该模型不能很好地与构建系统相结合;构建系统 本身 应该是增量单元(应该 运行 一次并保持加载),因此还必须内部化编译器以避免必须初始化和预热 VM每次都需要编译。
鉴于您要编译一个简单的单个小文件,VM 初始化和编译器预热最有可能占 640 毫秒的 99%。增量编译器正是您所需要的。
增量编译器仍然存在,但我所知道的唯一现代编译器是 eclipse:如果您在 eclipse 中保存文件,eclipse 几乎会立即使用它自己的内置编译器编译它。那是..显然不是你要找的。
如果增量的东西仍然存在并且维护得很好,您可以在网上搜索。您还应该能够手动执行此操作,也许:有一个 java 应用程序可以执行以下操作:监听系统输入中的任何完整行(例如,使用 Scanner 和 .useDelimiter("\r?\n"); .next()
,将这些行视为文件或dirs,将扫描这些文件以查找任何新的或更改的文件,对所有更改的内容调用 javax.tools.JavaCompiler
,然后将 hashmap 映射文件更新为时间戳,然后返回睡眠等待更多 System.in 流量,使用该地图决定缓存。或者忘记地图并采用旧样式:找到 class 文件,将其时间戳与源文件的时间戳进行比较,如果 class 文件的时间戳较新,则没有需要重新编译它。这并不能完全映射到 java 的工作方式(1 个源文件可以生成多个 class 文件,并且无法从 class 文件名中确定哪个源文件生成了它,虽然如果你添加足够的调试信息并打开它你可能可以,但是 class 文件是一种非常复杂的格式所以这样做是 non-trivial) - 所以我会做 hashmap , 即使合作昔日的 mpilers 使用 'compare class file last-modified with source file' 选项。
或者它可以打开一个 TCP/IP 端口。
请注意,eclipse 也以 'language server' 形式出现,这正是 VSCode 的工作方式:当您在 VSCode 中编辑 java 代码时,VS 启动无头 eclipse 并且用它来做 'intelligent' 关于 java 的所有事情:重构脚本、实时编译、错误、警告和一般 linting 服务、导航服务(例如 'open type'、'find callers' )、调试器等。编译是 eclipse-as-a-language-server 提供的众多服务的一部分,并且 ecj 比 javac 启动快 4 到 10 倍(VM 初始化和预热后的实际编译速度可能只有百分之几那 640 毫秒,因此不相关,但我不知道你需要多快)。
我刚凭经验得到的数字是5ms/file...
如果您有很多 Java 个文件要编译,那么您可能会关心这个。但如果是这样的话,那么你所说的时间数字就不现实了。我写这个程序来编译你的测试程序 1000 次(我创建了 1000 java 文件定义 类 Sum0 到 Sum999.
import javax.tools.JavaCompiler;
import javax.tools.ToolProvider;
public class T {
public static void main(String[] args) {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
int N = 1000;
String[] files = new String[N];
for (int i = 0 ; i < N ; i++) {
files[i] = String.format("stage/Sum%d.java", i);
}
int result = compiler.run(null, null, null, files);
System.out.println("Compile result code = " + result);
}
}
我运行它,这是运行的时间数字:
> ls stage/*.class | wc -l
ls: stage/*.class: No such file or directory
0
> time java T
Compile result code = 0
real 0m2.511s
user 0m4.737s
sys 0m0.549s
> ls stage/*.class | wc -l
1000
因此总 运行时间为 4.737 秒/1000 = .005 秒 = 5 毫秒/文件。我能想到的是,您一次只编译一个文件,在这种情况下,所有时间都是某种 startup/teardown 成本,谁在乎呢。
最重要的是,您可以在几秒钟内编译任意数量的 Java 文件,所以不用担心这个。
这是 运行 几代以前的 MacBook Pro。