服务器端图像处理



我需要在许多层上运行一组相当简单的函数(平均,加权平均/模糊,向下移动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除法快,但不如整数计算快。

最新更新