Java:微优化数组操作

时间:2010-06-08 00:02:37

标签: java performance optimization neural-network micro-optimization

我正在尝试建立一个简单的前馈神经网络的Java端口 这显然涉及大量的数值计算,所以我试图尽可能地优化我的中心循环。结果应该在float数据类型的限制范围内正确。

我当前的代码如下所示(错误处理和初始化已删除):

/**
 * Simple implementation of a feedforward neural network. The network supports
 * including a bias neuron with a constant output of 1.0 and weighted synapses
 * to hidden and output layers.
 * 
 * @author Martin Wiboe
 */
public class FeedForwardNetwork {
private final int outputNeurons;    // No of neurons in output layer
private final int inputNeurons;     // No of neurons in input layer
private int largestLayerNeurons;    // No of neurons in largest layer
private final int numberLayers;     // No of layers
private final int[] neuronCounts;   // Neuron count in each layer, 0 is input
                                // layer.
private final float[][][] fWeights; // Weights between neurons.
                                    // fWeight[fromLayer][fromNeuron][toNeuron]
                                    // is the weight from fromNeuron in
                                    // fromLayer to toNeuron in layer
                                    // fromLayer+1.
private float[][] neuronOutput;     // Temporary storage of output from previous layer


public float[] compute(float[] input) {
    // Copy input values to input layer output
    for (int i = 0; i < inputNeurons; i++) {
        neuronOutput[0][i] = input[i];
    }

    // Loop through layers
    for (int layer = 1; layer < numberLayers; layer++) {

        // Loop over neurons in the layer and determine weighted input sum
        for (int neuron = 0; neuron < neuronCounts[layer]; neuron++) {
            // Bias neuron is the last neuron in the previous layer
            int biasNeuron = neuronCounts[layer - 1];

            // Get weighted input from bias neuron - output is always 1.0
            float activation = 1.0F * fWeights[layer - 1][biasNeuron][neuron];

            // Get weighted inputs from rest of neurons in previous layer
            for (int inputNeuron = 0; inputNeuron < biasNeuron; inputNeuron++) {
                activation += neuronOutput[layer-1][inputNeuron] * fWeights[layer - 1][inputNeuron][neuron];
            }

            // Store neuron output for next round of computation
            neuronOutput[layer][neuron] = sigmoid(activation);
        }
    }

    // Return output from network = output from last layer
    float[] result = new float[outputNeurons];
    for (int i = 0; i < outputNeurons; i++)
        result[i] = neuronOutput[numberLayers - 1][i];

    return result;
}

private final static float sigmoid(final float input) {
    return (float) (1.0F / (1.0F + Math.exp(-1.0F * input)));
}
}

我使用-server选项运行JVM,到目前为止,我的代码比类似的C代码慢25%到50%。我该怎么做才能改善这种状况?

谢谢,

Martin Wiboe

编辑#1:在看到大量回复后,我应该澄清一下我们场景中的数字。在典型运行期间,该方法将使用不同的输入调用约50.000次。典型的网络将具有numberLayers = 3层,分别具有190,2和1个神经元。因此,最内层的循环将具有大约2*191+3=385次迭代(当计算层0和1中添加的偏置神经元时)

编辑#1:在此线程中实现各种建议后,我们的实现几乎与C版本一样快(在~2%之内)。感谢您的帮助!所有的建议都很有帮助,但由于我只能将一个答案标记为正确的答案,因此我会将它给予@Durandal建议数组优化并且是唯一一个预先计算for循环标头的答案。 / p>

8 个答案:

答案 0 :(得分:8)

一些提示。

  • 在你最内层的循环中,考虑如何遍历CPU缓存并重新排列矩阵,以便顺序访问最外层的数组。这将导致您按顺序访问缓存而不是跳到整个地方。缓存命中可以比缓存未命中快两个级别。 例如重组fWeights,以便将其作为
  • 访问

激活+ = neuronOutput [layer-1] [inputNeuron] * fWeights [layer - 1] [neuron] [inputNeuron];

  • 不要在循环内执行工作(每次),这可以在循环外完成(一次)。每次可以将它放在局部变量中时,不要执行[layer -1]查找。您的IDE应该能够轻松地重构它。

  • Java中的多维数组并不像C中那样高效。它们实际上是多层单维数组。您可以重新构建代码,因此您只使用单维数组。

  • 当您可以将结果数组作为参数传递时,不返回新数组。 (保存每次调用时创建一个新对象。)

  • 而不是在整个地方执行第1层,为什么不将layer1用作第1层并使用layer1 + 1而不是layer。

答案 1 :(得分:5)

首先,不要这样做:

// Copy input values to input layer output
for (int i = 0; i < inputNeurons; i++) {
    neuronOutput[0][i] = input[i];
}

但是这个:

System.arraycopy( input, 0, neuronOutput[0], 0, inputNeurons );

答案 2 :(得分:5)

忽略实际的数学,Java中的数组索引本身就是一种性能损失。考虑到Java没有真正的多维数组,而是将它们实现为数组数组。在最里面的循环中,可以访问多个索引,其中一些索引在该循环中实际上是常量。部分数组访问可以移出循环:

final int[] neuronOutputSlice = neuronOutput[layer - 1];
final int[][] fWeightSlice = fWeights[layer - 1];
for (int inputNeuron = 0; inputNeuron < biasNeuron; inputNeuron++) {
    activation += neuronOutputSlice[inputNeuron] * fWeightsSlice[inputNeuron][neuron];
}

服务器JIT可能执行类似的代码不变运动,找到的唯一方法是更改​​和分析它。在客户端JIT上,无论如何都应该提高性能。 您可以尝试的另一件事是预先计算for循环退出条件,如下所示:

for (int neuron = 0; neuron < neuronCounts[layer]; neuron++) { ... }
// transform to precalculated exit condition (move invariant array access outside loop)
for (int neuron = 0, neuronCount = neuronCounts[layer]; neuron < neuronCount; neuron++) { ... }

JIT可能已经为您做了这个,所以如果它有帮助,请进行配置。

有没有必要乘以1.0F才能让我在这里徘徊?:

float activation = 1.0F * fWeights[layer - 1][biasNeuron][neuron];

其他可能以可读性为代价提高速度的事情:手动内联sigmoid()函数(JIT对内联有一个非常严格的限制,函数可能更大)。 向后运行循环可能稍快一些(当然它不会改变结果),因为测试循环索引对零比检查局部变量要便宜一点(最里面的循环又是一个强大的候选者,但是不要期望输出在所有情况下都是100%相同,因为添加浮点数a + b + c可能与a + c + b不同。

答案 3 :(得分:3)

我要研究的第一件事是看Math.exp是否会减慢你的速度。有关本地替代方案,请参阅this post on a Math.exp approximation

答案 4 :(得分:3)

用整数步传递函数替换昂贵的浮点sigmoid传递函数。

S形传递函数是有机模拟突触学习的模型,而这似乎是阶梯函数的模型。

这一历史先例是,Hinton直接从认知科学理论关于真实突触的第一原理设计了反向支持算法,而真正的突触反过来又基于真实的模拟测量,结果证明是sigmoid。

但是S形传递函数似乎是数字阶跃函数的有机模型,当然不能直接有机地实现。

不是对模型建模,而是使用阶梯函数的直接数字实现(小于零= -1,大于零= +1)替换有机S形传递函数的昂贵浮点实现。

enter image description here 大脑不能这样做,但backprop可以!

这不仅可以线性地大幅度提高单个学习迭代的性能,还可以减少培训网络所需的学习迭代次数:支持学习本质上是数字化的证据。

同样支持计算机科学本身就很酷的论点。

答案 5 :(得分:1)

纯粹基于代码检查,你的内部循环必须计算对三维参数的引用并且它已经完成了很多。根据您的数组维度,您可能会遇到缓存问题,因为每次循环迭代都必须跳过内存。也许你可以重新排列尺寸,以便内循环试图访问彼此更接近的记忆元素?

在任何情况下,在进行任何更改之前都要对代码进行概要分析,并查看真正的瓶颈所在。

答案 6 :(得分:1)

我建议使用定点系统而不是浮点系统。几乎所有使用int的处理器都比float快。最简单的方法就是将所有剩余的东西移动一定量(4或5是良好的起点)并将最后4位视为小数。

你最内层的循环正在进行浮点运算,所以这可能会给你带来很大的提升。

答案 7 :(得分:0)

优化的关键是首先测量花费的时间。通过调用System.nanoTime()来包围算法的各个部分:

long start_time = System.nanoTime();
doStuff();
long time_taken = System.nanoTime() - start_time;

我猜想使用System.arraycopy()会有所帮助,你会在内循环中找到真正的成本。

根据您的发现,您可以考虑使用整数运算替换浮点运算。