我需要在许多层上运行一组相当简单的函数(平均,加权平均/模糊,向下移动n个空间等),每个层都是一个二维原语数组(int或float)。输入最多为8k × 4k,但通常小于2k × 1k。输入数据是不可变的,并且在每个单元格被处理后,将被输出替换,然后整个过程再次运行。我不期望它能实时运行,但确实需要每秒处理大约10张图像。所有的输入函数可以采样多个单元格,但只写入一个单元格,并且不维护状态(一个片段着色器,为了所有的意图和目的)。
在客户端这样做,我将把它卸载给GPU,每秒几千次迭代没有问题。然而,这需要在没有GPU的服务器(可能是虚拟机)上运行。服务器通常是基于rh的Linux,但我宁愿避免为此添加C库。
我目前的测试,使用本文要点中的代码,在4k × 2k的图像上,单线程最多每秒8张图像。这是可接受性能的最低限度,多线程可能会使其可接受(仍在测试中)。JMH给:
Benchmark Mode Samples Score Score error Units
t.ShaderFunc.testProcess thrpt 40 5.506 0.478 ops/s
t.ShaderFunc.testProcessInline thrpt 40 7.657 0.561 ops/s
t.ShaderFunc.testProcessProc thrpt 40 5.685 0.350 ops/s
具有基本功能:
public int blur(final int[][] data, final int x, final int y) {
float accumulator = 0;
int[] col = data[x];
accumulator += col[y];
if (x > 0) {
accumulator += data[x-1][y] * 0.5f;
}
if (x < data.length - 1) {
accumulator += data[x+1][y] * 0.5f;
}
if (y > 0) {
accumulator += col[y-1] * 0.5f;
}
if (y < col.length - 1) {
accumulator += col[y+1] * 0.5f;
}
return Math.round(accumulator / 3f);
}
作为我将要处理的一个更简单的函数,这让我很担心。
我更希望处理函数可以是一个接口(在Java 8中,可能是lambda),但是一些初始基准测试显示,当处理函数内联在循环中时,性能提高了大约30%。考虑到循环的紧密程度,我可以想象调用开销是一个问题。
是否有任何库、内在函数或其他技术可以显著改变性能?是否有任何技术可以用来向量化这段代码(我不确定这是否可能,考虑到函数的工作方式)?
更新:在整合Stuart Marks的修改和Aleksey Shipilev的建议后,结果大不相同。请注意,这是在我的工作机器(联想W530, i7-3840QM,在VMware Workstation 10上的CentOS 6.5 VM中)和我的家用机器(Surface Pro 2,在Win 8.1上)上进行的。我正在使用JDK 1.8更新11.
原始主旨结果:
Benchmark Mode Samples Score Score error Units
c.s.q.ShaderFunc.testProcess thrpt 40 9.098 0.054 ops/s
c.s.q.ShaderFunc.testProcessInline thrpt 40 11.337 1.603 ops/s
c.s.q.ShaderFunc.testProcessProc thrpt 40 11.706 0.105 ops/s
与建议的更改(代码在这里):
Benchmark Mode Samples Score Score error Units
c.s.q.ShaderFunc.testProcess thrpt 40 40.890 5.772 ops/s
c.s.q.ShaderFunc.testProcessInline thrpt 40 44.032 2.389 ops/s
c.s.q.ShaderFunc.testProcessProc thrpt 40 44.378 2.153 ops/s
这是精彩的表演,远远超出了我的预期。
更新2:在将代码更改为更接近我想要的API之后,使用lambda或类调用的开销似乎已经返回。我们对代码做了很多修改,以吸收已经做出的更改并进行清理,并且我根据一些JMH建议重构了基准测试。根据本文要点中的代码,结果如下:
Benchmark Mode Samples Score Score error Units
c.s.q.ShaderBench.testProcessInline thrpt 200 40.860 0.184 ops/s
c.s.q.ShaderBench.testProcessLambda thrpt 200 22.603 0.159 ops/s
c.s.q.ShaderBench.testProcessProc thrpt 200 22.792 0.117 ops/s
在我计划使用的服务器类型的虚拟机上:
Benchmark Mode Samples Score Score error Units
c.s.q.ShaderBench.testProcessInline thrpt 200 40.685 0.224 ops/s
c.s.q.ShaderBench.testProcessLambda thrpt 200 16.077 0.113 ops/s
c.s.q.ShaderBench.testProcessProc thrpt 200 23.827 0.088 ops/s
在最便宜的数字海洋节点上:
Benchmark Mode Samples Score Score error Units
c.s.q.ShaderBench.testProcessInline thrpt 200 24.425 0.506 ops/s
c.s.q.ShaderBench.testProcessLambda thrpt 200 9.643 0.140 ops/s
c.s.q.ShaderBench.testProcessProc thrpt 200 13.733 0.134 ops/s
所有可接受的性能(在我的工作机和专用+VM上,相当出色的性能)。我感兴趣的是弄清楚为什么调用有如此大的开销,以及可以做些什么来优化它。目前正在试验不同的参数集,并发布了一个更具体的问题。
感谢您提供要点;这使得摆弄代码以查看性能效果变得非常容易。另外,使用JMH +1。
首先,这里是我的机器(2009 MacBook Pro, 2.8GHz Core2Duo, JDK 8u5)的基线结果:
Benchmark Mode Samples Score Score error Units
c.s.q.ShaderFunc.testProcess thrpt 5 7.191 1.140 ops/s
c.s.q.ShaderFunc.testProcessInline thrpt 5 7.592 0.465 ops/s
c.s.q.ShaderFunc.testProcessProc thrpt 5 7.326 1.242 ops/s
(郭瑞昭。q为com.stackoverflow.questions
)
在我的运行中,两种技术之间的差异较小,尽管错误更高一些,内联版本仍然是最快的。由于结果非常接近,我开始优化testProcess
,这是直接调用blur
方法的一个,因为这是您在这里包含的代码。为方便其他读者,调用blur
方法的代码如下:
int width = 4000;
int height = 2000;
int[][] nextData = new int[width][height];
for (int i = 0; i < width; ++i) {
for (int j = 0; j < height; ++j) {
nextData[i][j] = blur(blurData, i, j);
}
}
我的第一个观察是,blur
方法中有很多条件避免步离矩阵的边缘。方便的是,如果"边缘外"的值为零,在边缘处进行的累积具有相同的结果(我认为大多数图像处理内核都是如此)。这意味着,如果我们用0填充矩阵的边缘,并运行从1到极限的循环,而不是从0到极限,我们可以去掉条件。循环变成这样:
int width = 4002;
int height = 2002;
int[][] nextData = new int[width][height];
for (int i = 1; i < width-1; ++i) {
for (int j = 1; j < height-1; ++j) {
nextData[i][j] = blur(blurData, i, j);
}
}
(您还必须对生成输入数据的randomMatrix
函数进行相应的更改。)如果您从blur
方法中删除条件,现在看起来像这样:
public int blur(final int[][] data, final int x, final int y) {
float accumulator = 0;
int[] col = data[x];
accumulator += col[y];
accumulator += data[x-1][y] * 0.5f;
accumulator += data[x+1][y] * 0.5f;
accumulator += col[y-1] * 0.5f;
accumulator += col[y+1] * 0.5f;
return Math.round(accumulator / 3f);
}
这个版本的结果可能快15%:
Benchmark Mode Samples Score Score error Units
c.s.q.ShaderFunc.testProcess thrpt 5 8.424 1.035 ops/s
现在让我们仔细看看计算。输入都是int
的数据,但是我们累加到一个float
变量中。然后输出也是一个int
。而不是重复乘以0.5f
,我们可以累积两倍的数量,然后在最后除以6f
。(但是,如果输入数据在20亿范围内,则存在溢出的可能性。)通过一些额外的简化,修改后的代码看起来像这样:
public int blur(final int[][] data, final int x, final int y) {
int[] col = data[x];
int accumulator = 2 * col[y]
+ data[x-1][y]
+ data[x+1][y]
+ col[y-1]
+ col[y+1];
return Math.round(accumulator / 6f);
}
结果快了80%以上!
Benchmark Mode Samples Score Score error Units
c.s.q.ShaderFunc.testProcess thrpt 5 15.397 1.897 ops/s
对于简化的blur
方法,让我们重新考虑内联。我不会重新生成代码,因为它只是获取blur
方法的主体并将其明显地重构到上面嵌套的for
循环中(调整变量名称等)。这样做会得到以下结果:
Benchmark Mode Samples Score Score error Units
c.s.q.ShaderFunc.testProcessInline thrpt 5 15.619 1.607 ops/s
只是快了一点,但在误差范围内,所以很难确定。如果保持函数独立可以更容易地插入不同的算法,那么内联可能就不值得了。
最大的优点是摆脱了浮点运算,特别是浮点乘法。许多多核系统可用的整数硬件比浮点硬件多,因此在这样的系统上避免使用FP仍然是有帮助的。
啊,这让我有了另一个想法。我们能摆脱Math.round
调用和FP划分吗?同样,根据输入的数字范围,我们可以进行基于整数的舍入。而不是
Math.round(accumulator / 6f)
我们可以做一些或多或少类似的事情,如:
(1000 * accumulator + 500) / 6000
这个改变的结果又提高了25% !
Benchmark Mode Samples Score Score error Units
c.s.q.ShaderFunc.testProcessInline thrpt 5 19.517 2.894 ops/s
今天的课程:为了加快速度,用整数计算代替浮点数。当然,您必须注意溢出和整数截断问题。但如果你能成功,那就值得了。
根据Alexey Shipilev (JMH的作者)的建议,我运行了一个替代版本,它乘以6.0f的倒数,而不是除以。代码是:
static final float ONE_SIXTH = 1.0f / 6.0f;
...
Math.round(accumulator * ONE_SIXTH);
用浮点乘法替换浮点除法。结果如下:
Benchmark Mode Samples Score Score error Units
c.s.q.ShaderFunc.testProcessInline thrpt 5 17.144 0.869 ops/s
明显比FP除法快,但不如整数计算快。