Java 的 JIT 编译器 "expand" 是否有简单的循环?
Does Java's JIT compiler "expand" trivial loops?
我正在编写我的应用程序的性能敏感部分,我很好奇 JIT 编译器(如果有的话)将如何优化以下方法:
private static int alphaBlend(int foreground, int background) {
int alpha = (foreground >> 24) & 0xFF;
int subAlpha = 0xFF - alpha;
return ((((((foreground >> 16) & 0xFF) * alpha) + (((background >> 16) & 0xFF)) * subAlpha) >> 8) << 16)
| ((((((foreground >> 8) & 0xFF) * alpha) + (((background >> 8) & 0xFF)) * subAlpha) >> 8) << 8)
| ((((foreground & 0xFF) * alpha) + ((background & 0xFF)) * subAlpha) >> 8);
}
private static int alphaBlendLoop(int foreground, int background) {
int alpha = (foreground >> 24) & 0xFF;
int subAlpha = 0xFF - alpha;
int blended = 0;
for (int shift = 16; shift >= 0; shift -= 8) {
blended |= (((((foreground >> shift) & 0xFF) * alpha) + (((background >> shift) & 0xFF)) * subAlpha) >> 8) << shift;
}
return blended;
}
这些方法执行 alpha 混合。基本上,它们将前景 RGBA 像素与背景 RGB 像素组合在一起,背景 RGB 像素的 RGB 分量值已预先乘以 alpha 值。
这两种方法 return 相同输入的相同值,但它们的实现不同。 就我个人而言,我发现后者的实现更易于阅读,但我担心它的性能可能较低。对于那些感兴趣的人,下面包含了这两种实现的字节码(它是使用 IntelliJ 的 "Show Bytecode" 视图生成的):
private static alphaBlend(II)I
L0
LINENUMBER 95 L0
ILOAD 0
BIPUSH 24
ISHR
SIPUSH 255
IAND
ISTORE 2
L1
LINENUMBER 96 L1
SIPUSH 255
ILOAD 2
ISUB
ISTORE 3
L2
LINENUMBER 97 L2
ILOAD 0
BIPUSH 16
ISHR
SIPUSH 255
IAND
ILOAD 2
IMUL
ILOAD 1
BIPUSH 16
ISHR
SIPUSH 255
IAND
ILOAD 3
IMUL
IADD
BIPUSH 8
ISHR
BIPUSH 16
ISHL
ILOAD 0
BIPUSH 8
ISHR
SIPUSH 255
IAND
ILOAD 2
IMUL
ILOAD 1
BIPUSH 8
ISHR
SIPUSH 255
IAND
ILOAD 3
IMUL
IADD
BIPUSH 8
ISHR
BIPUSH 8
ISHL
IOR
ILOAD 0
SIPUSH 255
IAND
ILOAD 2
IMUL
ILOAD 1
SIPUSH 255
IAND
ILOAD 3
IMUL
IADD
BIPUSH 8
ISHR
IOR
IRETURN
L3
LOCALVARIABLE foreground I L0 L3 0
LOCALVARIABLE background I L0 L3 1
LOCALVARIABLE alpha I L1 L3 2
LOCALVARIABLE subAlpha I L2 L3 3
MAXSTACK = 4
MAXLOCALS = 4
private static alphaBlendLoop(II)I
L0
LINENUMBER 103 L0
ILOAD 0
BIPUSH 24
ISHR
SIPUSH 255
IAND
ISTORE 2
L1
LINENUMBER 104 L1
SIPUSH 255
ILOAD 2
ISUB
ISTORE 3
L2
LINENUMBER 105 L2
ICONST_0
ISTORE 4
L3
LINENUMBER 106 L3
BIPUSH 16
ISTORE 5
L4
FRAME FULL [I I I I I I] []
ILOAD 5
IFLT L5
L6
LINENUMBER 107 L6
ILOAD 4
ILOAD 0
ILOAD 5
ISHR
SIPUSH 255
IAND
ILOAD 2
IMUL
ILOAD 1
ILOAD 5
ISHR
SIPUSH 255
IAND
ILOAD 3
IMUL
IADD
BIPUSH 8
ISHR
ILOAD 5
ISHL
IOR
ISTORE 4
L7
LINENUMBER 106 L7
IINC 5 -8
GOTO L4
L5
LINENUMBER 109 L5
FRAME CHOP 1
ILOAD 4
IRETURN
L8
LOCALVARIABLE shift I L4 L5 5
LOCALVARIABLE foreground I L0 L8 0
LOCALVARIABLE background I L0 L8 1
LOCALVARIABLE alpha I L1 L8 2
LOCALVARIABLE subAlpha I L2 L8 3
LOCALVARIABLE blended I L3 L8 4
MAXSTACK = 4
MAXLOCALS = 6
直觉上,循环需要更多 "work" 和操作(跳转、评估条件、递减等)。但是,循环也很容易预测;它总是恰好执行三次并且在其范围内定义的变量将始终具有相同的三个值。
在这种情况下,JIT 编译器(或 更智能的 静态编译器?)是否能够通过将其扩展为可能很长的循环来优化像这样的微不足道的循环-在 alphaBlend
实现中看到的班轮?还是循环通常无法以这种方式进行优化?
对于 Oracle VM,您可以使用 -XX:+PrintAssembly 检查 JIT 输出。
(要启动 JIT,您的测试程序应至少调用感兴趣的方法 10 000 次。)
是的,HotSpot JIT 支持循环展开和持续传播优化,可以将alphaBlendLoop
转换成类似于手动展开 alphaBlend
.
我个人更喜欢第三种选择:一个小的辅助函数,使代码更具可读性:
private static int blend(int foreground, int background, int alpha, int shift) {
int fg = (foreground >>> shift) & 0xff;
int bg = (background >>> shift) & 0xff;
return (fg * alpha + bg * (256 - alpha)) >>> 8 << shift;
}
public static int alphaBlend(int foreground, int background) {
int alpha = foreground >>> 24;
int R = blend(foreground, background, alpha, 0);
int G = blend(foreground, background, alpha, 8);
int B = blend(foreground, background, alpha, 16);
return R | G | B;
}
我做了一个 JMH benchmark 来验证所有 3 个选项在性能上是否相似。
在 Java 8u77 x64.
上测试
Benchmark Mode Cnt Score Error Units
Blend.alphaBlendInline avgt 10 7,831 ± 0,045 ns/op
Blend.alphaBlendLoop avgt 10 7,860 ± 0,025 ns/op
Blend.alphaBlendMethod avgt 10 7,769 ± 0,056 ns/op
我正在编写我的应用程序的性能敏感部分,我很好奇 JIT 编译器(如果有的话)将如何优化以下方法:
private static int alphaBlend(int foreground, int background) {
int alpha = (foreground >> 24) & 0xFF;
int subAlpha = 0xFF - alpha;
return ((((((foreground >> 16) & 0xFF) * alpha) + (((background >> 16) & 0xFF)) * subAlpha) >> 8) << 16)
| ((((((foreground >> 8) & 0xFF) * alpha) + (((background >> 8) & 0xFF)) * subAlpha) >> 8) << 8)
| ((((foreground & 0xFF) * alpha) + ((background & 0xFF)) * subAlpha) >> 8);
}
private static int alphaBlendLoop(int foreground, int background) {
int alpha = (foreground >> 24) & 0xFF;
int subAlpha = 0xFF - alpha;
int blended = 0;
for (int shift = 16; shift >= 0; shift -= 8) {
blended |= (((((foreground >> shift) & 0xFF) * alpha) + (((background >> shift) & 0xFF)) * subAlpha) >> 8) << shift;
}
return blended;
}
这些方法执行 alpha 混合。基本上,它们将前景 RGBA 像素与背景 RGB 像素组合在一起,背景 RGB 像素的 RGB 分量值已预先乘以 alpha 值。
这两种方法 return 相同输入的相同值,但它们的实现不同。 就我个人而言,我发现后者的实现更易于阅读,但我担心它的性能可能较低。对于那些感兴趣的人,下面包含了这两种实现的字节码(它是使用 IntelliJ 的 "Show Bytecode" 视图生成的):
private static alphaBlend(II)I
L0
LINENUMBER 95 L0
ILOAD 0
BIPUSH 24
ISHR
SIPUSH 255
IAND
ISTORE 2
L1
LINENUMBER 96 L1
SIPUSH 255
ILOAD 2
ISUB
ISTORE 3
L2
LINENUMBER 97 L2
ILOAD 0
BIPUSH 16
ISHR
SIPUSH 255
IAND
ILOAD 2
IMUL
ILOAD 1
BIPUSH 16
ISHR
SIPUSH 255
IAND
ILOAD 3
IMUL
IADD
BIPUSH 8
ISHR
BIPUSH 16
ISHL
ILOAD 0
BIPUSH 8
ISHR
SIPUSH 255
IAND
ILOAD 2
IMUL
ILOAD 1
BIPUSH 8
ISHR
SIPUSH 255
IAND
ILOAD 3
IMUL
IADD
BIPUSH 8
ISHR
BIPUSH 8
ISHL
IOR
ILOAD 0
SIPUSH 255
IAND
ILOAD 2
IMUL
ILOAD 1
SIPUSH 255
IAND
ILOAD 3
IMUL
IADD
BIPUSH 8
ISHR
IOR
IRETURN
L3
LOCALVARIABLE foreground I L0 L3 0
LOCALVARIABLE background I L0 L3 1
LOCALVARIABLE alpha I L1 L3 2
LOCALVARIABLE subAlpha I L2 L3 3
MAXSTACK = 4
MAXLOCALS = 4
private static alphaBlendLoop(II)I
L0
LINENUMBER 103 L0
ILOAD 0
BIPUSH 24
ISHR
SIPUSH 255
IAND
ISTORE 2
L1
LINENUMBER 104 L1
SIPUSH 255
ILOAD 2
ISUB
ISTORE 3
L2
LINENUMBER 105 L2
ICONST_0
ISTORE 4
L3
LINENUMBER 106 L3
BIPUSH 16
ISTORE 5
L4
FRAME FULL [I I I I I I] []
ILOAD 5
IFLT L5
L6
LINENUMBER 107 L6
ILOAD 4
ILOAD 0
ILOAD 5
ISHR
SIPUSH 255
IAND
ILOAD 2
IMUL
ILOAD 1
ILOAD 5
ISHR
SIPUSH 255
IAND
ILOAD 3
IMUL
IADD
BIPUSH 8
ISHR
ILOAD 5
ISHL
IOR
ISTORE 4
L7
LINENUMBER 106 L7
IINC 5 -8
GOTO L4
L5
LINENUMBER 109 L5
FRAME CHOP 1
ILOAD 4
IRETURN
L8
LOCALVARIABLE shift I L4 L5 5
LOCALVARIABLE foreground I L0 L8 0
LOCALVARIABLE background I L0 L8 1
LOCALVARIABLE alpha I L1 L8 2
LOCALVARIABLE subAlpha I L2 L8 3
LOCALVARIABLE blended I L3 L8 4
MAXSTACK = 4
MAXLOCALS = 6
直觉上,循环需要更多 "work" 和操作(跳转、评估条件、递减等)。但是,循环也很容易预测;它总是恰好执行三次并且在其范围内定义的变量将始终具有相同的三个值。
在这种情况下,JIT 编译器(或 更智能的 静态编译器?)是否能够通过将其扩展为可能很长的循环来优化像这样的微不足道的循环-在 alphaBlend
实现中看到的班轮?还是循环通常无法以这种方式进行优化?
对于 Oracle VM,您可以使用 -XX:+PrintAssembly 检查 JIT 输出。
(要启动 JIT,您的测试程序应至少调用感兴趣的方法 10 000 次。)
是的,HotSpot JIT 支持循环展开和持续传播优化,可以将alphaBlendLoop
转换成类似于手动展开 alphaBlend
.
我个人更喜欢第三种选择:一个小的辅助函数,使代码更具可读性:
private static int blend(int foreground, int background, int alpha, int shift) {
int fg = (foreground >>> shift) & 0xff;
int bg = (background >>> shift) & 0xff;
return (fg * alpha + bg * (256 - alpha)) >>> 8 << shift;
}
public static int alphaBlend(int foreground, int background) {
int alpha = foreground >>> 24;
int R = blend(foreground, background, alpha, 0);
int G = blend(foreground, background, alpha, 8);
int B = blend(foreground, background, alpha, 16);
return R | G | B;
}
我做了一个 JMH benchmark 来验证所有 3 个选项在性能上是否相似。
在 Java 8u77 x64.
Benchmark Mode Cnt Score Error Units
Blend.alphaBlendInline avgt 10 7,831 ± 0,045 ns/op
Blend.alphaBlendLoop avgt 10 7,860 ± 0,025 ns/op
Blend.alphaBlendMethod avgt 10 7,769 ± 0,056 ns/op