【问题标题】:Java: micro-optimizing array manipulationJava:微优化数组操作
【发布时间】:2011-02-28 22:56:27
【问题描述】:

我正在尝试制作一个简单的前馈神经网络的 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%。我可以做些什么来改善这种情况?

谢谢,

马丁·维博

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

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

【问题讨论】:

  • 你分析过它吗?知道它大部分时间都花在哪里会很有趣。
  • 同意分析。不要目不转睛地猜测需要改进的地方。
  • 这样的代码容易并行化吗?如果是这样,编写它的多线程版本将大大拥有单线程版本。我去过那里,用 Java 重写了一个正确的多线程快速排序。在 16 核机器上观看是一种乐趣:stackoverflow.com/questions/2210185(它粉碎了默认的 Java API 排序算法 big 次)。除此之外,我看到了一些微优化,但我对神经网络的了解还不够,无法提供太大帮助。 (顺便说一句,最近单 CPU 机器变得很难买到,例如我不知道苹果是否还在销售单 CPU Mac)
  • 你确定不只是 JVM 预热?
  • @CurtainDog JVM 在我得到最好的测量值时会预热(比 C 慢 25%-50%)。 @Webinator 好建议(令人印象深刻的算法!)。我可以并行化整个任务并同时运行此方法,所以我不确定我是否看到拆分 compute() 的好处。 @Donnie 和 Brendan Profiling 绝对是要走的路,我只是没有从 jvisualvm 得到任何有意义的结果。明天将尝试另一个分析器。

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


【解决方案1】:

一些提示。

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

激活 += 神经元输出[layer-1][inputNeuron] * fWeights[layer - 1][neuron][inputNeuron];

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

  • Java 中的多维数组不如 C 中的高效。它们实际上是多层单维数组。您可以重组代码,以便只使用一维数组。

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

  • 与其到处做layer-1,不如把layer1当作layer-1,用layer1+1代替layer。

【讨论】:

  • 哇 - 优化阵列访问减少了 20% 的运行时间。谢谢。
  • 交换 fWeights 的最后两个索引也将允许 inputNeuron 上的 activation += neuronOutput[layer-1][inputNeuron] * fWeights[layer - 1][inputNeuron][neuron]; 循环使用 SSE2 或 AVX(甚至 FMA)进行矢量化,如果 Java(或您的特定 JVM)有任何类型-ffast-math 选项。使用 SIMD 将跨步访问变为连续访问是一个巨大的优势。
【解决方案2】:

忽略实际的数学运算,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) 不同。

【讨论】:

  • 数组和预计算似乎将整体运行时间提高了 25% :) 谢谢。
【解决方案3】:

首先,不要这样做:

// 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 );

【讨论】:

  • 当然,但是在这个算法中不是只复制了两个数组,一个复制输入,一个复制结果吗?真正的成本更有可能在嵌套的 for 循环中。
  • @W 和 V - 没错,但这不是瓶颈所在。
  • 好建议 - 会这样做:) 但是该方法的运行时间是由内部循环决定的,所以很遗憾,它不会节省时间。 (inputNeurons 约为 200,所以应该不会有那么大的差异)
【解决方案4】:

我首先要调查的是Math.exp 是否会拖慢您的速度。请参阅 this post on a Math.exp approximation 了解本地替代方案。

【讨论】:

  • 我在想整个 sigmoid() 函数的查找表可能是值得的,但如果不知道该函数花费了多少时间,就很难说。
  • 几乎可以肯定,查找表会大大提高该函数的速度,并可能帮助您重新获得从 C 到 Java 的 25% 损失。如果您怀疑在那里花费了多少时间,请使用一些分析工具来确定花费了这么长时间。但由于它至少是在计算层*神经元次数,因此很有可能这是一个可以轻松消除的瓶颈。
  • 我曾尝试使用该近似值,但不幸的是结果太不准确。你知道有什么方法可以通过牺牲一些速度来提高准确性吗? @Brendan Long 和 drharris 查找表很可能是一种选择 - 我将进行数百万次计算。如何实现一个使用浮点数作为键的线程安全查找表?
  • 好吧,看看这篇较早的帖子,关于如何改进典型 Sigmoid 函数的许多示例:stackoverflow.com/questions/412019/math-optimization-in-c
  • 如果您的问题是“我如何让这段代码运行得更快”,在更深入地优化速度和准确性之间的权衡之前,我很好奇是否推动了不准确(但更快)的方法你更接近可接受的运行时间?如果不是,也许我们在寻找错误的区域。
【解决方案5】:

用整数阶跃传递函数代替昂贵的浮点 sigmoid 传递函数。

sigmoid 传递函数是有机模拟突触学习的模型,反过来又似乎是阶跃函数的模型。

这方面的历史先例是 Hinton 直接根据关于真实突触的认知科学理论的第一原理设计了反向传播算法,而这些原理又基于真实的模拟测量,结果证明是 sigmoid。

但是sigmoid传递函数好像是数字阶跃函数的有机模型,当然不能直接有机实现。

与其建模模型,不如用阶跃函数的直接数字实现(小于零 = -1,大于零 = +1)替换有机 sigmoid 传递函数的昂贵浮点实现。

大脑无法做到这一点,但反向传播可以!

这不仅线性且显着提高了单次学习迭代的性能,还减少了训练网络所需的学习迭代次数:证明学习本质上是数字化的。

也支持计算机科学天生就很酷的论点。

【讨论】:

    【解决方案6】:

    纯粹基于代码检查,您的最内层循环必须计算对 3D 参数的引用,并且需要做很多工作。根据您的数组尺寸,您可能会遇到缓存问题,因为每次循环迭代都必须在内存中跳转。也许您可以重新排列维度,以便内部循环尝试访问比现在更接近的内存元素?

    无论如何,在进行任何更改之前分析您的代码,看看真正的瓶颈在哪里。

    【讨论】:

    • 分析肯定会有所帮助。我将尝试切换 fWeights[layer - 1][inputNeuron][neuron] 中的最后两个索引,使变化的 inputNeuron 成为第三个索引。
    【解决方案7】:

    我建议使用定点系统而不是浮点系统。在几乎所有使用 int 的处理器上都比 float 快。最简单的方法是将所有内容左移一定量(4 或 5 是很好的起点)并将低 4 位视为小数。

    你最里面的循环是做浮点数学,所以这可能会给你很大的提升。

    【讨论】:

    • 总的来说,这是一个很好的观点(事实上,许多确实需要固定精度的系统都是错误的,因为它们天真地使用了 FP)。但是在这种情况下,我认为 sigmoid 函数不适合这种技术。
    • 使用现代硬件,一条 FP 指令比在定点中执行相同操作所需的多条整数指令要快。 (特别是对于乘法,您需要转移以将点放在正确的位置;加/减更便宜。)
    • 整数非常适合处理最初为整数的像素,因为通常每个元素 16 位就足够了。因此,每个 SIMD 向量可以获得两倍于浮点数的元素,并且有一些 SSE 指令专为像素上经常需要的东西而设计。因此,当您有多个 16 位元素要并行执行时,使用整数非常有用,尤其是。如果它节省了与浮点数的转换。对于其他情况,通常不值得。
    • 最新的 CPU 甚至支持向量 FP fused-multiply-add 与一条指令,但不支持整数。因此,您实际上可以在最新的 Intel/AMD CPU 上使用 FP 完成更多工作。 (在编写此答案后引入了 FMA 指令,但它们可能对运行该 sum 循环的 JVM 有用。)
    【解决方案8】:

    优化的关键是首先衡量时间花在了哪里。调用 System.nanoTime() 包围算法的各个部分:

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

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

    根据您的发现,您可能会考虑将浮点运算替换为整数运算。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-06-14
      • 2017-02-14
      • 2016-10-09
      • 2014-03-31
      • 2019-04-17
      • 2020-12-10
      • 1970-01-01
      • 2016-02-18
      相关资源
      最近更新 更多